I have an old Kindle DX Graphite lying around. The Lithium Polymer battery on it was well below charge and the safety on it was refusing to allow a recharge via the Kindle's USB port.
I opened up the Kindle, set my bench-top power supply up to 3.7 V and connected it to the battery headers, that I had soldered some jumper wire to. The Kindle refused to boot!
Hmm I'm feeding you power, why won't you boot? |
I wasn't prepared to buy a new battery for this unit, I just wanted to play around with it and get some files off it.
I decided to jump start the battery with my PSU, but I also wanted to break Amazon's restrictions and get the Kindle to boot without the battery.
I did some Googling but found very little. So here is what I learned along the way:
Scope detective work
Using my oscilloscope I was able to capture a byte when turning on the Kindle. Further investigation revealed that the Kindle is talking SMBus with the battery. SMBus is derived from I2C and is very similar. Below is an example capture from my scope:
The two inside terminals on the Kindle's battery are marked D and C. These are Data and Clock.
So it appears that the Kindle is checking the overall health of the battery. If the battery is dead, or not present then the Kindle refuses to boot.
Enter the Bus Pirate
Connecting Dangerous Prototype's Bus Pirate. I was able to sniff the bus using the I2C sniffer.
Bus Pirate Connected to Kindle and my PC |
Bus Pirate sniffing the Kindle's SMBus. The [0xAA-] is it trying to talk to the battery's address. |
I2C uses 7 bits for device addressing, the LSB 8th bit determines if an address is write 0 or read 1. Hence the two addresses for one device.
Putting the Bus Pirate into sniffing mode and turning on the Kindle showed it looking for the battery [0xAA-] [0xAA-] [0xAA-]. The brackets are the start and stop bits for I2C/SMBus. 0xAA is the battery's write address. The '-' is a NACK (not Acknowledged) so the battery is not responding to the call. (At this point the battery is still dead and has been removed from the Kindle.
I charged the battery under constant supervision using my bench top PSU. (NOTE: This can be dangerous, please don't do this unless you know what you are doing as there is a fire risk!)
Close up of Kindle with battery installed and Bus Pirate connected. |
Above is the Bus Pirate connected in close-up. The Green clip is the lead to the internal pull up resistors on the Bus Pirate and is connected to the 3.3V rail (red). This is required as I2C uses an open drain so the data and clock lines need to be pulled up.
With the battery now charged and the kindle powered up the following data was captured by the Bus Pirate between the battery and the Kindle:
Conversation cleaned up in Vi for legibility |
The first two lines are 'are you there' calls to the battery from the Kindle [0xAA+] the '+' is the ACK sent back by the battery. So things are looking good!
The line reading [0xAA+0x09+[0xAB+0x0E-] is an important one!
This is the Kindle getting the battery to report its voltage.
The I2C/SMBus protocol, to read a byte, is as follows:
This is how the Bus Pirate represents the I2C protocol:
[0xAA+0x09+[0xAB+0x0E-]
[ Start conversation
0xAA Calling device by it's write address 0xAA
+ The device is ACKing back
0x09 Write the following byte to the device's receive buffer: 0x09 is the voltage level address
+ The battery acknowledges that the address has been received.
[ Repeated start condition
0xAB The Kindle calls the read address of the battery. The battery reads the data from address 0x09 and places it in its buffer.
+ The battery pulls the clock line high again to signify it has finished writing to buffer(ACK).
0x0E The Kindle's master bus, clocks out the byte from the battery reading a value of 0x0E.
- The Kindle master sends a NACK to say it is done receiving data.
] The stop character signaling end of conversation.
So what is with 0x0E? That's just the number 14 in hexadecimal.
The voltage should read somewhere between 3.7V and 4.2V!
So what is going on?
Reading the documentation on SMBus shows that this 0x09 command is comprised of two bytes. As a dead LIPO battery is below 3V the Kindle only cares about the first byte. If you add a second byte, eg. 0x00 to the first byte you get 0x0900 which in decimal is 3580mV which equates to 3.58 V. (yep battery isn't fully charged anymore at this point)
Table: Standard SMBUS COMMANDS http://www.szgrn.com/smbus-command.html | |||||||
SBS Cmd | Mode | Name | Format | Size in Bytes | Min Value | Max Value | Unit |
0x00 | R/W | ManufacturerAccess | hex | 2 | 0x0000 | 0xffff | |
0x01 | R/W | RemainingCapacityAlarm | unsigned int | 2 | 0 | 65535 | mAh |
0x02 | R/W | RemainingTimeAlarm | unsigned int | 2 | 0 | 65535 | min |
0x03 | R/W | BatteryMode | hex | 2 | 0x0000 | 0xe383 | |
0x04 | R/W | AtRate | signed int | 2 | -32768 | 32767 | mAh |
0x05 | R | AtRateTimeToFull | unsigned int | 2 | 0 | 65534 | min |
0x06 | R | AtRateTimeToEmpty | unsigned int | 2 | 0 | 65534 | min |
0x07 | R | AtRateOK | unsigned int | 2 | 0 | 65535 | |
0x08 | R | Temperature | unsigned int | 2 | 0 | 65535 | 0.1K |
0x09 | R | Voltage | unsigned int | 2 | 0 | 65535 | mV |
0x0a | R | Current | signed int | 2 | -32768 | 32767 | mA |
0x0b | R | AverageCurrent | signed int | 2 | -32768 | 32767 | mA |
0x0c | R | MaxError | unsigned int | 1 | 0 | 100 | % |
0x0d | R | RelativeStateOfCharge | unsigned int | 1 | 0 | 100 | % |
0x0e | R | AbsoluteStateOfCharge | unsigned int | 1 | 0 | 100+ | % |
0x0f | R/W | RemainingCapacity | unsigned int | 2 | 0 | 65535 | mAh |
0x10 | R | FullChargeCapacity | unsigned int | 2 | 0 | 65535 | mAh |
0x11 | R | RunTimeToEmpty | unsigned int | 2 | 0 | 65534 | min |
0x12 | R | AverageTimeToEmpty | unsigned int | 2 | 0 | 65534 | min |
0x13 | R | AverageTimeToFull | unsigned int | 2 | 0 | 65534 | min |
0x14 | R | ChargingCurrent | unsigned int | 2 | 0 | 65534 | mA |
0x15 | R | ChargingVoltage | unsigned int | 2 | 0 | 65534 | mV |
0x16 | R | BatteryStatus | unsigned int | 2 | 0x0000 | 0xdbff | |
0x17 | R/W | CycleCount | unsigned int | 2 | 0 | 65535 | |
0x18 | R/W | DesignCapacity | unsigned int | 2 | 0 | 65535 | mAh |
0x19 | R/W | DesignVoltage | unsigned int | 2 | 0 | 65535 | mV |
0x1a | R/W | SpecificationInfo | hex | 2 | 0x0000 | 0xffff | |
0x1b | R/W | ManufactureDate | unsigned int | 2 | ASCII | ||
0x1c | R/W | SerialNumber | hex | 2 | 0x0000 | 0xffff | |
0x20 | R/W | ManufacturerName | String | 11+1 | ASCII | ||
0x21 | R/W | DeviceName | String | 7+1 | ASCII | ||
0x22 | R/W | DeviceChemistry | String | 4+1 | ASCII | ||
0x23 | R/W | ManufacturerData | String | 14+1 | ASCII | ||
0x2f | R/W | Authenticate | String | 20+1 | ASCII | ||
0x3c | R | CellVoltage4 | unsigned int | 2 | 0 | 65535 | mV |
0x3d | R | CellVoltage3 | unsigned int | 2 | 0 | 65535 | mV |
0x3e | R | CellVoltage2 | unsigned int | 2 | 0 | 65535 | mV |
0x3f | R | CellVoltage1 | unsigned int | 2 | 0 | 65535 | mV |
OK so now we have most of the data we need. How do we trick the Kindle?
Several Microchip PICs in the Enhanced Mid Range can talk I2C/SMBus without having to bit bash. This uses the MSSP (Master Synchronous Serial Port) which can also do SPI.
These PIC's also lend themselves quite nicely to programming with C rather than assembler for the base range. I chose a PIC16F1824 which was only around $2 from RS. Much cheaper than a new LIPO battery! This PIC also has a serial UART which can help for debugging.
For low level simple I/O you can't beat assembly on a PIC to know what is going on. But seeing as you are going to have to write up the I2C protocol in your code and debug it, C is going to be the easier option!
So, without wanting to write from scratch; I poured over several examples of PIC I2C code in C.The examples made no sense and didn't appear to behave as per the SMBus protocol. I still don't quite understand what those code examples are trying to do. Looks like sequentially reading and writing to EEPROM rather than reading and writing to specific addresses on a device. I read Microchip's AN734 and AN736 PDF's and downloaded Microchip example code but they were the same. They don't seem to follow the I2C protocol, and didn't work for me at all.
So I grabbed the best parts of the example code and rewrote it until it did the right job:
Also using a Raspberry Pi with i2cdump installed I was able to read the battery's entire 128 bytes of data:
I pasted the entire data dump into an array in the source code. This should now fool the Kindle into believing the battery is really there, and the PIC will reply with the same data for any request that the battery would have.
Below is that complete code:
/*
* File: kindlebat.c
* Author: Neil
*
* Created on March 25, 2016, 8:50 PM
*/
/* Example of I2C reading 2 bytes (rr) using Bus Pirate
Write address
I2C>[0xaa 0x00 [0xab rr]
I2C START BIT
WRITE: 0xAA ACK
WRITE: 0x00 ACK
I2C START BIT
WRITE: 0xAB ACK
READ: 0xAA
READ: ACK 0xEE
NACK
I2C STOP BIT
Example of writing using Bus Pirate
I2C>[0xaa 0x00 0x10]
I2C START BIT
WRITE: 0xAA ACK
WRITE: 0x00 ACK
WRITE: 0x10 ACK
I2C STOP BIT
*/
// CONFIG1
#pragma config FOSC = INTOSC // INTOSC oscillator: I/O function on CLKIN pin
#pragma config WDTE = ON // Watchdog Timer Enable (WDT enabled)
#pragma config PWRTE = ON // Power-up Timer Enable (PWRT enabled)
#pragma config MCLRE = ON // MCLR/VPP pin function is MCLR
#pragma config CP = ON // Flash Program Memory Code Protection enabled
#pragma config CPD = ON // Data memory code protection is enabled
#pragma config BOREN = ON // Brown-out Reset enabled
#pragma config CLKOUTEN = OFF // CLKOUT function is disabled.
// I/O or oscillator function on the CLKOUT pin
#pragma config IESO = ON // Internal/External Switch-over mode is enabled
#pragma config FCMEN = ON // Fail-Safe Clock Monitor is enabled
// CONFIG2
#pragma config WRT = OFF // Flash Memory Self-Write Protection off
#pragma config PLLEN = ON // PLL Enable (4x PLL enabled)
#pragma config STVREN = ON // Stack Overflow/Underflow will cause a Reset
#pragma config BORV = LO // Brown-out Reset Voltage (Vbor),
// low trip point selected.
#pragma config LVP = ON // Low-voltage programming enabled
#include <xc.h>
#include <pic.h>
#include <pic16f1824.h>
#include "uart.h" // Only need for debugging
#define I2C_address 0xAA /* I2C address of the slave node */
#define RX_ELMNTS 128
// array for master to read from
volatile unsigned char I2C_Array[RX_ELMNTS] =
{0x00,0x40,0xb6,0x17,0x26,0x00,0xa1,0x04,0x75,0x0f,0x04,0x63,0xb7,0x0f,
0xd8,0x0f,0xb7,0x0f,0xd8,0x0f,0x67,0x00,0x27,0x09,0xff,0xff,0x67,0x00,0x27,
0x09,0x00,0x00,0x35,0x0e,0x0f,0x00,0x0f,0x00,0x82,0x08,0x02,0x00,0x0c,0x00,
0x63,0xe0,0xa0,0x07,0x21,0x00,0xf8,0x9b,0xc3,0x1b,0xff,0xff,0xa8,0xf6,0xff,0x00,
0xe3,0x03,0x4b,0x4e,0x00,0x00,0x05,0x00,0x00,0x00,0x21,0xa9,0xc7,0x48,0x38,0x85,
0x7f,0x66,0x6c,0x46,0xdf,0x20,0x36,0x09,0x00,0x01,0x52,0xa4,0xcf,0x0f,0x3f,0x24,
0x00,0x00,0x71,0xff,0x91,0xff,0x62,0xff,0x8d,0x04,0xd9,0x05,0x00,0x00,0x00,0x01,
0x00,0x07,0x00,0x00,0x2d,0x78,0x37,0x6d,0x7a,0xbb,0x21,0xa9,0xc7,0x48,0x38,0x85,
0x7f,0x66,0x00,0xa9};
unsigned char dbuf = 0; // index pointer
unsigned char junk = 0; // used to place unnecessary data
unsigned char clear = 0x00;
unsigned int first = 0;
void initialize(void);
void main(void) {
// UART_Init(9600);
initialize();
while(1)
{
asm("CLRWDT"); // clear Watch Dog Timer
}
}
void initialize(void)
{
//uC SET UP
OSCCON = 0b01111010; // Internal OSC @ 16MHZ
OPTION_REG = 0b11010111; // WPU disabled, INT on rising edge, FOSC/4
// Prescaler assigned to TMR0, rate @ 1:256
WDTCON = 0b00010111; // Prescaler 1:65536
// period is 2 sec (RESET value)
PORTC = 0x00; // Clear PORTC
LATC = 0x00; // Clear PORTC latches
ANSELC = 0x00; // Set analogue ports to digital
TRISC = 0b00011011; // Set RC0(SCL), RC1(SDA) as inputs for I2C
//I2C SLAVE MODULE SET UP
SSP1STAT = 0b11000000; // Slew rate control disabled for standard
// speed mode (100 kHz and 1 MHz)
// SMbus input logic
SSP1CON1 = 0b00110110; // Enable serial port, I2C slave mode,
// 7-bit address
SSP1CON2bits.SEN = 0; // Clock stretching is enabled
//SSP1CON2bits.GCEN = 1; // General Call Enable
SSP1CON3bits.BOEN = 1; // SSPBUF is updated and NACK is generated
// for a received address/data byte,
// ignoring the state of the SSPOV bit
// only if the BF bit = 0
SSP1CON3bits.SDAHT = 1; // Minimum of 300 ns hold time on SDA after
// the falling edge of SCL
SSP1CON3bits.SBCDE = 0; // Enable slave bus collision detect interrupts
SSP1ADD = I2C_address; // Load the slave address
SSP1IF = 0; // Clear the serial port interrupt flag
BCL1IF = 0; // Clear the bus collision interrupt flag
BCL1IE = 1; // Enable bus collision interrupts
SSP1IE = 1; // Enable serial port interrupts
PEIE = 1; // Enable peripheral interrupts
GIE = 1; // Enable global interrupts
}//end initialize
/*************************** ISR ROUTINE **************************************/
void interrupt ISR(void)
{
if(SSP1IF) // check to see if SSP interrupt
{
if(!SSP1STATbits.R_nW) // master write (R_nW = 0)
{
// STATE 1
if(!SSP1STATbits.D_nA) // last byte was an address (D_nA = 0)
{
junk = SSP1BUF; // read buffer to clear BF
SSP1CON1bits.CKP = 1; // release CLK
}
// STATE 2
if(SSP1STATbits.D_nA) // last byte was data (D_nA = 1)
{
if (SSP1STATbits.BF){
// first time around data is an index pointer to array
if (first == 0){
dbuf = SSP1BUF;
first++;
}
// second time around data is data - so write to array using previous index
else {
I2C_Array[dbuf] = SSP1BUF;
first = 0;
}
}
if(SSP1CON1bits.WCOL) // Did a write collision occur?
{
SSP1CON1bits.WCOL = 0; // clear WCOL bit
junk = SSP1BUF; // clear SSPBUF
}
SSP1CON1bits.CKP = 1; // release CLK
}
// STATE 5
else {
SSP1CON1bits.CKP = 1;
dbuf = 0;
first = 0;
}
}
// STATE 3
if(SSP1STATbits.R_nW) // Master read (R_nW = 1)
{
if(!SSP1STATbits.D_nA) // last byte was an address (D_nA = 0)
{
junk = SSP1BUF; // dummy read to clear BF bit
SSP1BUF = I2C_Array[dbuf]; // load SSPBUF with data
first = 0;
SSP1CON1bits.CKP = 1; // release CLK
}
// STATE 4
if(SSP1STATbits.D_nA) // last byte was data
{
if (dbuf++ < RX_ELMNTS)
{ // Does data exceed number of allocated bytes?
SSP1BUF = I2C_Array[dbuf++]; // load SSPBUF with data
} // and increment index
else
{
junk = SSP1BUF; // dummy read to clear BF bit
}
first = 0;
SSP1CON1bits.CKP = 1; // release CLK
}
}
}
if(BCL1IF) // Did a bus collision occur?
{
junk = SSP1BUF; // clear SSPBUF
BCL1IF = 0; // clear BCL1IF
SSP1CON1bits.CKP = 1; // Release CLK
}
SSP1IF = 0; // clear SSP1IF
}
It helps to have PICKIT 3 or similar to do in circuit debugging, as it can get quite difficult to see what's happening on the PIC otherwise, I tried to get all this going with a PICKIT 2 but was flying blind.
I included some messages written out to UART to let me know the value of SSP1STAT and the SSP1BUF registers but the PICKIT 3 really helped me the most.
Note you'll need a logic inverter to interface the UART with RS232 to your PC.
Example of insight I was able to gain from UART debugging the output of SSP1STAT register and the SSP1BUF register for each byte communicated
[0xAA 0x09 [0xAB r]
SSP1STAT
11001000 11101000 11001101 11101100
Address Data Address Data
Write Write Read Read
Buffer 0 Buffer 0 Buffer 1 Buffer 0
SSP1BUF
0xAA 0x09 0x0f 0x00
Success!
It works the Kindle boots without a battery!:
What's next?
Next step is to wire the up the USB to power the Kindle rather than attaching a PSU to the battery headers.
Then it's time to delve into some OS hacking on the Kindle and see if I can get an old laptop Wifi card to replace the 3G card inside.
Links to additional resources:
I do my PIC development on the Gooligum Dev board (shown below).
Great place to learn about programming PICs
http://www.gooligum.com.au/
Above and below is the PIC's UART output being inverted using a Hex Inverter SN74HC04
http://ww1.microchip.com/downloads/en/AppNotes/00736a.pdfAbove and below is the PIC's UART output being inverted using a Hex Inverter SN74HC04
Good stuff, Neil!
ReplyDeleteI have ten of these with broken screens. I took the coward's way out and just bought batteries for them. But my plan was to run them from solar panels via the USB port, so I'm very interested in your further progress on USB power.
Thanks. There is of course another way. I've been playing around with the OS and it is very easy to hack into. There is quite a lot of info online on how to do this. I thought the battery monitoring was done via a micro controller, however the Linux OS on the Kindle is actually doing the monitoring. You can just disable the gas-gauge process to get it to boot without a battery.
ReplyDelete"I thought the battery monitoring was done via a micro controller, however the Linux OS on the Kindle is actually doing the monitoring. You can just disable the gas-gauge process to get it to boot without a battery."
ReplyDeleteGreat discovery, Neil! Can you please detail how to disable the battery check in Linux? I sometimes use a rooted Kindle DX as a second monitor, and the battery is almost dead. Aloha, Miles
Hello again, Neil. It turns out that /etc/rc2.d/S50battcheck offers all sorts of possibilities for skipping the battery check. I elected to simply change the two lines reading "_SKIP_CHECK=0" to "_SKIP_CHECK=1" - worked a treat.
ReplyDeleteHere is some partial output from /var/local/log/really_long_name.gz before the change when starting with a dead battery:
init: Entering runlevel: 2
...
system: I S50battcheck:def:running
system: I S50battcheck:def:current voltage = 3738mV
system: I S50battcheck:def:gasgauge capacity=2% volts=3794 mV current=179 mA
system: W S50battcheck:def:battery low. Not booting
...
system: I S50battcheck:def:current voltage = 3882mV
system: I S50battcheck:def:gasgauge capacity=4% volts=3864 mV current=363 mA
system: I S50battcheck:def:battery sufficient, booting to normal runlevel
init: Switching to runlevel: 5
and here is the output after editing /etc/rc2.d/S50battcheck as described above and rebooting:
init: Entering runlevel: 2
...
system: I S50battcheck:def:running
system: I S50battcheck:def:skipping battery check
init: Switching to runlevel: 5
This comment has been removed by the author.
DeleteThis little trick seems to be the fastest way to get out of this critical battery situation:
ReplyDeletehttp://epistemist.com/critical-battery-on-kindle-2-hard-reset/
anyone tested this pic mod with a kindle4 nt?
ReplyDelete