Tuesday, May 25, 2010

Using an Arduino Duemilanove as a quick and dandy logic analyzer (part 2)

I've updated my Arduino logic analyzer code to interpret I2C bus signals. My intention is to eavesdrop on write operations to the EEPROM of the Health & Life Co HL168Y Blood Pressure Monitor so that readings can be stored and downloaded automatically to a PC.

The HL168Y uses a ST Microelectronics M24C08 8 kbit (1 kbyte)  serial EEPROM which uses I2C for communications. I2C is a inter-chip communications protocol that requires just two lines (clock and data). The details of the EEPROM I2C communications protocol is beyond the scope of this post, but in essence a memory write operation comprises a START (S) condition followed by 3 x 9 bit bytes (8 data, 1 acknowledgment bit) followed by a STOP (P) condition. First byte is a chip address + two MSB of memory address + R/W bit, second byte is the lower 8 bits of the memory address, and last byte is the content to be written to memory.

The following bus activity was recorded after a blood pressure reading 132 (systolic mmHg), 78 (diastolic mmHg), 86 bpm heart rate at 22:21 on 24 May.

S 1010 0000 0 0000 0111 0 0000 0010 0 P into 0x007 write 0x02 - record counter (this was reading #2)
S 1010 0000 0 0001 1000 0 0000 0101 0 P into 0x018 write 0x05 (5 dec) - month of year
S 1010 0000 0 0001 1001 0 0001 1000 0 P into 0x019 write 0x18 (24 dec) - day of month
S 1010 0000 0 0001 1010 0 1000 1010 0 P into 0x01a write 0x0c - bit7 pm flag, bit3-0 hours (12 hour clock)
S 1010 0000 0 0001 1011 0 0001 0101 0 P into 0x01b write 0x15 (21 dec)  - minutes
S 1010 0000 0 0001 1100 0 0001 0000 0 P into 0x01c write 0x10 - bits7-4: sys decimal digit 2
S 1010 0000 0 0001 1101 0 0011 0010 0 P into 0x01d write 0x32 - bits7-4: sys digit 1,0 (hex as dec)
S 1010 0000 0 0001 1110 0 0111 1000 0 P into 0x01e write 0x78  - dia (hex as dec)
S 1010 0000 0 0001 1111 0 0101 0110 0 P into 0x01f write 0x56 (86) - bpm
S 1010 0000 0 0000 1111 0 0101 0000 0 P into 0x00f write 0x50 (80)

(abbreviations: sys - systolic pressure in mmHg; dia - diastolic pressure in mmHg, bpm - heart rate in beats per minute)

While month, day of month, minutes and heart rate are stored as one would expect (unsigned 8 bit integer), other values are stored a little strangely. The hour of day is stored in 12 hour clock with the MSB indicating AM/PM (1 indicating PM). Bits 3-0 are the 12 hour clock hour.

Blood pressure is also stored in an strange format: the upper nibble of byte 4 is the hundreds digit of systolic pressure. I'm guessing the lower nibble is the hundreds digit of the diastolic pressure (thankfully I can't put that to the test :-). Byte 5 upper nibble is the tens digit of systolic and byte 5 lower nibble is the units digit of systolic. Byte 6 upper nybble is the tens digit of diastolic and byte 5 lower nibble is the units digit of diastolic.

The following program will listen to the I2C bus and gather data in a buffer until MAX_EVENTS events have been recorded at which time it will dump all to the serial port. There is no timeout in this version. The single BP reading was not sufficient to fill the buffer, so it's necessary to press the "Memory" button on the device once or twice to cause enough bus activity to fill the buffer and flush the data.

/**
 * I2C bus snooper. Written to eavesdrop on MCU to EEPROM 
 * communications in a HL168Y blood pressure monitor. SCL 
 * is connected to Arduino pin 2 and SDA to pin 3.
 */
#define MAX_EVENTS 512
int SCL = 2;
int SDA = 3;

char data[MAX_EVENTS];
int dp = 0;

void setup()   {                
  pinMode(SCL, INPUT);  
  pinMode(SDA, INPUT); 
  Serial.begin(115200);
}

void loop()                     
{
  unsigned char s;
  dp = 0;
  
   lookForStart:
   
   // Expect both SCL and SDA to be high
  while ( (PIND & 0b00001100) != 0b00001100) ;
  // both SLC and SDA high at this point
  
  // Looking for START condition. Ie SDA transitioning from 
  // high to low while SLC is high.
  while ( (PIND & 0b00001100) != 0b00000100) ;
  data[dp++] = 'S'; 
   // wait for SCL low
  while ( (PIND & 0b00000100) != 0) ;
  
  lookForData:
  
  while (dp < MAX_EVENTS) {
    
     // wait for SCL low
    while ( (PIND & 0b00000100) != 0) ;
    
    // Wait for SCL to transition high
    while ( (PIND & 0b00000100) == 0) ;
    
    // Sample SDA at the transition point
    s = PIND & 0b00001000;
    data[dp++] = (s == 0 ? '0' : '1');
    
    // Wait for SCL to transition low while looking 
    // for start or stop condition. A START or STOP
    // means the previous bit isn't a data bit. So will
    // write START, STOP condition into the same memory slot
    if (s == 0) {
      while ( (PIND & 0b00000100) != 0) {
        if ( (PIND & 0b00001000) != 0) {
           // detected STOP condition
           data[dp-1] = 'P';
           goto lookForStart;
        }
      }
    } else {
      while ( (PIND & 0b00000100) != 0) {
        if ( (PIND & 0b00001000) == 0) {
           // detected START condition
           data[dp-1] = 'S';
           goto lookForData;
        }
      }
    }
  }
  // have exceed storage. Dump to serial port and restart.
  writeData(); 
}

void writeData () {
  int i;
  for (i = 0; i < dp; i++) {
    Serial.print (data[i]);
    if (data[i] == 'P') {
      Serial.println ("");
    }
  }
  Serial.println ("");
}

No comments: