a quite fast I2C snifer / code share

Hello,
I was in need for a fast I2C snifer.
after buying and trying buspirate, I realized I had no choice writing one.
it works pretty well :slight_smile: and was successfully snifing 400khz streams.
only software, no use of TWI. can snif on any pin.

this is my first C program so please appology for lack of presentation.
guess it is self explaining:

/*******************************************************
/********************* SNIFER I2C **********************
/*** author maxidcx / forum arduino ********************
/*******************************************************/

#define SNIF_SCL (PINC & B010000) // read port value and isolate SCL pin
#define SNIF_SDA (PINC & B100000) // read port value and isolate SDA pin

#define I2C_STOP -30000        // this value will be stored in the circular buffer when receiving stop sequence
#define I2C_EOF  -30001        // this value will be return if snif_GET is involved while buffer is empty

#define snif_word_size 3       // this is the word size for the received data. 
                               // Used only to format printing and count.
                               // 3 in this example which is appropriate for 24bits DSP.
                               // should be 1 or 2 for most usage.
#define snif_buffer_size  800  // buffer size (2 bytes per entry) Be carefull to comply with ATMEGA available RAM
#define snif_buffer_print 700  // will authorize printing if buffer reach 500 !

int snif_RX[snif_buffer_size]; // the buffer
int snif_pos_IN=0;             // position where toputthe next value in the buffer
int snif_pos_OUT=0;            // position where to take the next value from the buffer
int snif_qty=0;                // total number of value in the buffer
long snif_last_time=0;         // used to stamp when the last frame was arrived
int snif_frame_qty=0;          // number of frames in the buffer
int snif_word_count;           // used during printing/formating to identify words and separate them by char 0x20
int snif_frame_size=0;         // used by print function to count number of caracters in the frame displayed

/*************  PRINTING BUFFER *********************/
// print all the caracters from the buffer, 
// if available , and at least x miliseconds 
// after the last frame is received
// calling this function just print 1 caracter from the buffer
/***************************************************/

void snif_PRINT(word delay_min) {

int x;
int count_word;

 if (snif_qty) { // will enter only if some characters are available in the buffer
  if ( ((millis()-snif_last_time) > delay_min) || // wait at least x ms, to avoid loosing frames
       (snif_qty>snif_buffer_print)) { // or buffer becoming full

    x=snif_GET();
    if (x >=0) { snif_frame_size++;
                 if (x & 0x0200) {  // this is an address
                    if (x & 0x0001)  { Serial.print("R @");} // READ
                                else { Serial.print("W @");} // WRITE
                    Serial.print((x & 0x00FE)>>1,HEX);       // print adress
                    snif_word_count=-1;  // initialize word counting
                    } else { // this is a normal data 
                      if ((x & 0x00F0)==0) { Serial.print("0");}  // add a "0" when value < 0x10)
                      Serial.print(x & 0x00FF,HEX); 
                      }
                 if (x & 0x0100) { Serial.print("^");} // NACK
                             else {Serial.print("_");} // ACK
                 if (x & 0x0200) { Serial.print(" "); } // separate adress and data by one 0x20
                 snif_word_count++;
                 if (snif_word_count==snif_word_size) { 
                     Serial.print(" "); // separate each word with a char 0x20 for good visibility
                     snif_word_count=0; 
                     }
               }
      else {
        switch (x){
        case I2C_STOP: Serial.println(""); // next line
                       Serial.print("<STOP ");
                       Serial.print(snif_frame_size);
                       Serial.print("b");
                       count_word = snif_frame_size / snif_word_size;
                       if (count_word) {
                          Serial.print("=");
                          Serial.print(count_word);
                          Serial.print("w");
                          if (snif_frame_size - snif_word_size * count_word) { 
                              Serial.print("+"); 
                              Serial.print(snif_frame_size - snif_word_size*count_word);
                              Serial.print("b");
                              }
                          }
                       Serial.println(">");
                       snif_frame_qty--; // one frame is now completely printed
                       break;

        case I2C_EOF:  Serial.println("<EOF>");break;  // probably never happens !

        default:       Serial.println("");  // this correspond to a Start with time stamp
                       Serial.print("<START ");Serial.print(-x);Serial.print("ms> ");
                       snif_frame_size=-1; // reset frame size (counting without adress)
        }
      }
    }
  }
}

/************** CIRCULAR BUFFER HANDLING ****************/

void snif_PUT(int x) {
if (snif_qty >= snif_buffer_size) {return;}
snif_RX[snif_pos_IN++]=x;
if (snif_pos_IN>=snif_buffer_size) {snif_pos_IN=0;}
snif_qty++;
}

int snif_GET() {
int value;
  if (snif_qty<1) {return I2C_EOF;}
  value = snif_RX[snif_pos_OUT++];
  if (snif_pos_OUT>=snif_buffer_size) {snif_pos_OUT=0;}
  snif_qty--;
  return value;
}

boolean snif_available(){
if (snif_qty>0) { return true;} else {return false;}
}


/************* SENSITIVE PART *******************/

boolean snif_SCL_low(){ // return true if timeout or stop bit otherwise wait clk to go low

  byte SCL;
  byte SDA;
  byte old_SDA;
  byte i;

SDA=SNIF_SDA;
for (i=1; i<200; i++) { // 200 loop max for time out handling
  if (SCL=SNIF_SCL) { // SCL still high
    old_SDA=SDA; 
    if (SDA=SNIF_SDA) { 
       if (old_SDA==LOW) {return true;} // stop signal recognized
      }
    } 
  else { return false;} // SCL is LOW
  }
return true; // too long / timeout
}

int snif_8bits(){ // read 8 bits data + ack/nack flag
int value;
byte SDA;

// when entering this function, the SCL pin is expected to be LOW

value=1; // after 8 loops, this bit0 will become bit8 and will stop the loop

do {
  while (SNIF_SCL==LOW) { } // wait SCL = HIGH 
  if (SDA=SNIF_SDA) {value = (value <<1) + 1; } else {value = value <<1; }
  if (snif_SCL_low()) { return I2C_STOP;}  // wait until SCL=LOW
} while (value<0x100); // until the bit0 comes in postion 8
while (SNIF_SCL==LOW) { } // wait SCL becomes HIGH
if (SNIF_SDA) { return value; } else { return value & 0xFEFF;} // NACK or ACK is stired in bit 8
// when leaving this function, the SCL is HIGH hopefully
}

/******** MAIN USER FUNCTION **********/

void snif_READ(int n){  // check Start condition during n ms then read all bit until Stop condition

int value;  
byte SCL,SDA,old_SCL,old_SDA;
long time_stamp;

while (n>0) {
   time_stamp=millis(); 
   cli(); // stop interupt to avoid loosing start bit condition, during about 1ms
tryagain:
   SCL=SNIF_SCL; SDA=SNIF_SDA;
   for (int i=0; i<1000; i++) { // 1000 seems to be around 1ms . not critical at all 
      old_SCL=SCL;SCL=SNIF_SCL;
      old_SDA=SDA;SDA=SNIF_SDA;
      if (SDA==LOW) {
        if (old_SDA != LOW) {
           if ((old_SCL != LOW) && (SCL != LOW)) { goto startcondition; }
           }
         }
      }
  n--;
  sei();  // restore interuption
}
return; // end of n milisecond

startcondition:    // start condition detected (falling SDA while clk=1)

snif_PUT(-((snif_last_time=time_stamp)& 16383)); // store value in buffer rounded to 16 seconds
snif_frame_qty++;          // one more frame is coming

if (snif_SCL_low()) { goto endofstory;}   // sorry for "goto" but still a good way of handling branching!

value = snif_8bits(); if (value<0){ goto endofstory;} 
snif_PUT(value | 0x0200 ); // this is an adress, set bit 9 for further decoding during printing

frameloop:

  if (snif_SCL_low()) { goto endofstory;} // wait next SCL = 0
  value = snif_8bits();
  if (value<0) { goto endofstory; }
  snif_PUT(value);   // store value in snif buffer
  goto frameloop;    // look for additional caracters
  
endofstory:
snif_PUT(I2C_STOP);
goto tryagain; // that way we look for another start condition imediately, without releasing interupt...
}

void snif_INIT() {
 snif_pos_IN=0;
 snif_pos_OUT=0;
 snif_qty=0;
 snif_frame_qty=0;
 snif_last_time=millis();
}

void setup() {
  
Serial.begin(115200); // preferably high speed to reduce chnace of loosing I2C start bit

snif_INIT();
 
}

void loop() {

snif_READ(5);     //  look for start during 10ms ish
snif_PRINT(100);  // each caractere will be printed one by one after a timeout period (1 sec ) 
                   // you should adjust this value so the printing could occur between some I2C exchange otherwise buffer will get full !

// your other loop code goes here, like trigering events or so

}

enjoy it

Nice project, usefull to say the least. Do you have some sample output to see what we can expect?

yep:

basically there are 2 functions.
one used for waiting the start bit and sthen storing data in a circular buffer till stop bit.
then a print function for extracting characters from buffer and displaying them in a convenient way.

the read function is called with a parameter to define the start-bit wait time in ms, e.g. 5 or 10ms.
then the print function is called with a timeout parameter, so that it start pinting characters only after a certain of "grace period", to avoid or at least limit loosing start bit sequences. of course the code could be enhanced by using interupt but it works fine like this.

it is still possible to implement user code in the loop section, and typically one could use the wire library, in addition, to poke some device and let the snifer control everything.

here is below just an example of a snif from a comunication between 2 processors, copy/pasted from the serial monitor, as-is:

<START 11492ms> W @0_ 09_00_16_ 
<STOP 3b=1w>

<START 11493ms> R @0_ 00_00_89_ 00_16_00_ 00_
<STOP 7b=2w+1b>

<START 11493ms> W @0_ 09_00_16_ 
<STOP 3b=1w>

<START 11494ms> R @0_ 89_00_16_ 00_00_00^ 
<STOP 6b=2w>

the first line indicates we received a start with a time stamp (from millis) and this was a write comand to device at adress 0 for 3 bytes.
the "_" (underscore) means ACK while the "^" means NACK.
then the stop bit happens. The frame is 3bytes which is one word in this configuration (24bits data) but this can be changed.
then a Read came out for 7bytes.

if you just launch this code as-is, put the serial monitor in 115200 bauds. something will apera once a start bit is recognized.

remark: this code should not be considered as a perfect implementation of I2C ! just a example which works for purpose :slight_smile:

hope this helps