Saturday, April 23, 2016

Booting A Kindle DX Graphite without a Battery.


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:

SMBus protocol on my oscilloscope. Clock is CH2 and Data byte is CH1.









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.
As you can see above, this is the Bus Pirate in I2C mode. The discovered address space is the read and write addresses (0x98 and 0x99) of the Kindle. 
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 20x0000 0xffff  
0x01 R/W RemainingCapacityAlarm unsigned int 2065535mAh
0x02 R/W RemainingTimeAlarm unsigned int 2065535min 
0x03 R/W BatteryMode hex 20x0000 0xe383  
0x04 R/W AtRate signed int 2-3276832767mAh
0x05 AtRateTimeToFull unsigned int 2065534min 
0x06 AtRateTimeToEmpty unsigned int 2065534min 
0x07 AtRateOK unsigned int 2065535 
0x08 Temperature unsigned int 20655350.1K
0x09 Voltage unsigned int 2065535mV 
0x0a Current signed int 2-3276832767mA 
0x0b AverageCurrent signed int 2-3276832767mA 
0x0c MaxError unsigned int 10100
0x0d RelativeStateOfCharge unsigned int 10100
0x0e AbsoluteStateOfCharge unsigned int 10100+ 
0x0f R/W RemainingCapacity unsigned int 2065535mAh
0x10 FullChargeCapacity unsigned int 2065535mAh
0x11 RunTimeToEmpty unsigned int 2065534min 
0x12 AverageTimeToEmpty unsigned int 2065534min 
0x13 AverageTimeToFull unsigned int 2065534min 
0x14 ChargingCurrent unsigned int 2065534mA 
0x15 ChargingVoltage unsigned int 2065534mV 
0x16 BatteryStatus unsigned int 20x0000 0xdbff  
0x17 R/W CycleCount unsigned int 2065535 
0x18 R/W DesignCapacity unsigned int 2065535mAh
0x19 R/W DesignVoltage unsigned int 2065535mV 
0x1a R/W SpecificationInfo hex 20x0000 0xffff  
0x1b R/W ManufactureDate unsigned int 2ASCII 
0x1c R/W SerialNumber hex 20x0000 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 CellVoltage4 unsigned int 2065535mV 
0x3d CellVoltage3 unsigned int 2065535mV 
0x3e CellVoltage2 unsigned int 2065535mV 
0x3f CellVoltage1 unsigned int 2065535mV

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.pdf

5 comments:

  1. Good stuff, Neil!

    I 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.

    ReplyDelete
  2. 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
  3. "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."

    Great 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

    ReplyDelete
  4. 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.

    Here 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

    ReplyDelete
  5. This little trick seems to be the fastest way to get out of this critical battery situation:
    http://epistemist.com/critical-battery-on-kindle-2-hard-reset/

    ReplyDelete