Monday, May 31, 2010

Using an Arduino as a simple logic analyzer (part 3)

In my previous two posts on this topic I outlined how a Arduino could be used to snoop on a I2C bus with the aim of enhancing a HL168Y blood pressure monitor.  Part 1 was about recording logic levels, part 2 interpreted those levels into I2C signals (START, STOP, data). Those signals where hand interpreted to reverse engineer the memory map.

In this post I'll go one step further and automatically interpret the bus signals into memory read and write operations and output to the serial port.

Fortunately the HL168Y reads and writes to the EEPROM one byte at a time (the EEPROM supports page read/write which would complicate matters). The output format is one operation per line:
"R aaa vv" or "W aaa vv", where R indicates a read operation and W a write operation. The second parameter "aaa" is the memory location in hex (from 000 to 3fff) and "vv" is the 8 bit value in hex read from / written to the memory location.

On taking a blood pressure measurement the following bus activity was observed after the reading as complete:

W 010 05
W 011 1e
W 012 86
W 013 28
W 014 10
W 015 11
W 016 82
W 017 44

Referring to the record format in part 2, that's month 5, day of month 0x1e (ie 30),  hour is 6 with the PM flag on (ie 6pm), minutes 0x28 (40) and BP 111 / 82 with heart rate of 0x44 (68).

Signals to Read/Write flowchart


Signals to Read/Write Arduino C/C++ program

/**
 * 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.
 * This version will decode read and write operations to 
 * EEPROM outputting to serial port as
 * "R aaa vv" or "W aaa vv" records (one per line) where
 * aaa is 10 address bits as hex digits and vv is value as
 * hex digits.
 */

int SCL = 2;
int SDA = 3;

char hexval[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

#define LOG_SIZE 255
unsigned char datalog[LOG_SIZE];
unsigned char logptr;

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

void loop()                     
{
  unsigned char s, b, byteCounter, bitCounter, rwFlag;
  unsigned char addr_hi, addr_lo;
  unsigned int t = 0;
  
  logptr = 0;
  
  waitForStart:
 
   // Expect both SLC 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) {
    if ( (--t == 0) &&  (logptr > 0) ) {
      writeData();
    }
  }
  
  firstBit:
  
  byteCounter = 0;
  bitCounter = 0;
  
  nextBit:
 
  // If SCL high, wait for SCL low
  while ( (PIND & 0b00000100) != 0) ;
    
  // Wait for SCL to transition high. Nothing of interest happens when SCL is low.
  while ( (PIND & 0b00000100) == 0) ;
    
  // Sample SDA at the SCL low->high transition point. Don't know yet if this is a
  // data bit or a STOP or START condition.
  s = PIND & 0b00001000;
    
  // Wait for SCL to transition low while monitoring SDA for a transition.
  // No transition means we have data or ACK bit (sample in 's'). A hight to
  // low SDA = START, a low to high SDA transition = STOP.
  if (s == 0) {
    // loop while SCL high and SDA low
    while ( (PIND & 0b00001100) == 0b00000100) ;
    if ( (PIND & 0b00001100) == 0b00001100) {
         // STOP condition detected
         if (logptr > LOG_SIZE - 20) {
           writeData();
         }
         goto waitForStart;
    }
  } else {
    // loop while SCL high and SDA high
    while ( (PIND & 0b00001100) == 0b00001100) ;
    if ( (PIND & 0b00001100) == 0b00000100) {
        // START condition.
        goto firstBit;
    }
  }
  
  // OK. This is a data bit.
  bitCounter++;
  
  if (bitCounter < 9) {    
    b <<= 1;
    // if data bit is '1' set it in LSB position (will default to 0 after the shift op)
    if (s != 0) {
      b |= 0b00000001;
    }
    
    goto nextBit;
  }
  
  // 9th bit (ack/noack)
  
  bitCounter = 0;
  byteCounter++;
  
  switch (byteCounter) {
    case 1: // 1010AAAW where AAA upper 3 bits of address, W = 0 for writer, 1 for read
    if ( (b & 0b11110000) != 0b10100000) {
      goto waitForStart;
    }
    // Set A9,A8 bits of address
    addr_hi = (b>>1) & 0b00000011;
    rwFlag = b & 0b00000001;
    break;
    
    case 2: // data if rwFlag==1 else lower 8 bits of address
    if (rwFlag == 1) {
      // data read from eeprom. Expect this to be the last byte before P
      //datalog[logptr++] = ' ';
      datalog[logptr++] = 'R';
      datalog[logptr++] = ' ';
      datalog[logptr++] = hexval[addr_hi];
      datalog[logptr++] = hexval[addr_lo>>4];
      datalog[logptr++] = hexval[addr_lo & 0b00001111];
      datalog[logptr++] = ' ';
      datalog[logptr++] = hexval[b >> 4];
      datalog[logptr++] = hexval[b & 0b00001111];
      datalog[logptr++] = '\n';
    } else {
      addr_lo = b;
    }
    break;
   
    case 3: // only have 3rd byte if rwFlag==0. This will be the data.
    if (rwFlag == 0) {
      //datalog[logptr++] = ' ';
      datalog[logptr++] = 'W';
      datalog[logptr++] = ' ';
      datalog[logptr++] = hexval[addr_hi];
      datalog[logptr++] = hexval[addr_lo>>4];
      datalog[logptr++] = hexval[addr_lo & 0b00001111];
      datalog[logptr++] = ' ';
      datalog[logptr++] = hexval[b>>4];
      datalog[logptr++] = hexval[b & 0b00001111];
      datalog[logptr++] = '\n';
      
      if (logptr > LOG_SIZE - 10) {
        writeData();
      }
      
      break;
    }
    
  } // end switch
 
  goto nextBit;
  
}

void writeData () {
  for (int i = 0; i < logptr; i++) {
    Serial.print(datalog[i]);
    datalog[i] = 0;
  }
  logptr=0;
  Serial.println ("\n");
}

Converting read/writes operations into BP measurement records

I wrote a small JSP script to make the conversion.

<%
 String SEP = "\t";
 if ( request.getMethod().equals("GET") ) { 
%>
<form method="POST">
<textarea name="data" rows="20" cols="40"></textarea>
<br />
<input type="submit" value="Submit" /></form>
<%
 } else {
  
  response.setContentType("text/plain");
  
  int[][] records = new int[40][8];
  String[] lines = request.getParameter("data").split("\r\n");
  for (String line : lines) {
   if ( ! (line.startsWith("R") || line.startsWith("W"))) {
    continue;
   }
   String[] p = line.split(" ");
   int addr = Integer.parseInt(p[1],16);
   int value = Integer.parseInt(p[2],16);
   // Addresses under 16 are not for BP readings
   if (addr < 16) {
    continue;
   }
   int recordIndex = (addr - 16)/8;
   int col = addr%8;
   records[recordIndex][col] = value;
  }
  
  for (int i = 0; i < 40; i++) {
   if (records[i][0] == 0) {
    continue;
   }
   
   // European format
   //out.write (records[i][1] + "/" + records[i][0] + "/2010 ");
   
   // US/spreadsheet format
   out.write (records[i][0] + "/" + records[i][1] + "/2010 ");
   
   // Hours
   // Stored in 12 hour clock value (lower nibble) and bit 7 = PM flag.
   int h = records[i][2];
   h =  h > 127  ?  (h & 0xf) + 12 : (h & 0xf);
   
   if (h < 10) {
    out.write ("0");
   }
   out.write (h + ":");
   
   // Minutes
   int m = records[i][3];
   if (m < 10) {
    out.write ("0");
   }
   out.write ("" + m + SEP);
   
   // Systolic
   int sys = ((records[i][4] & 0xf0)>>4) * 100;
   sys +=  ((records[i][5] & 0xf0)>>4) * 10;
   sys +=  ((records[i][5] & 0x0f));
   out.write ("" + sys + SEP);
   
   // Diastolic
   int dia =  (records[i][4] & 0x0f) * 100;
   dia +=  ((records[i][6] & 0xf0)>>4) * 10;
   dia +=  ((records[i][6] & 0x0f));
   out.write ("" + dia + SEP);
   
   // Heart rate
   out.write ("" + records[i][7]);
   out.write ("\n");
  }
 }
%>

Running this with the write records above results in the following BP record:
5/30/2010 18:40 111 82 68

I've chosen US date format as this works best with spreadsheets.

4 comments:

Koen De Voegt said...

Hey,

I've read your post but can't get your code to work for me. Any change you can help me debug? I've got one Arduino sending stuff using the regular Wire functions another one using you code. Adding some extra println learns me that the code passes the main loop whenever something is send from the other Arduino but I never recognize a START or STOP. Any ideas?

Regards,
Koen

koendevoegt TA gmail TOD com

Joe Desbonnet said...

Koen,

Sorry - only spotted your comment just now. Can you email me at jdesbonnet (at) gmail (dot) com and I'll see if I help you.

Joe.

Unknown said...

Hi Joe, I'm getting this to basically work, however the data reported while snooping are inconsistent. For example, out of the 8 Write ('W') elements of a BP reading the program will sometimes print out all eight, but other times only five or six. In other words some of the "write" elements are missing, but inconsistently. Is this what you are seeing, or are all elements of a blood pressure meeting consistently there? I'm thinking maybe I have a bad solder - it was really tricky to solder to those little test pads.

thanks,
andrew

Unknown said...

Hello

i have a problem with your code with when i combined with the LCD display this program cannot run.

And, i have buy the blood pressure automatic monitoring brand microlife but i cannot know how to identify SDA and SCI...please help me!!!!