Listening in on two devices communicating over i2c

I have an old handheld transceiver that stores information (tx/rx frequencies, tx power levels, sub-audible tone frequencies, etc) on an eeprom. I don't have a programmer for this radio so my thought was to just read and write to the eeprom directly rather than going through a programmer. Before I start trying to read and write the eeprom I want to listen to the data going back and forth between the eeprom and the transceiver's controller uC. To do this I wrote an arduino sketch that uses interrupts on the scl and sda lines of the eeprom to determine whether there is a start condition, a stop condition, a data bit, or an ACK. I have attached the sketch below. My logic is as follows:

  1. wait for a rising edge on SCL or a change on SDA

  2. if SDA changes while SCL is high then we have a start or stop condition (start if SDA is pulled low and stop if SDA is high)

  3. if we got a rising edge on SCL then read the SDA line and then wait for one of two things to happen. We know that SCL is high so we watch SDA to see if it changes and we watch SCL to see if we get a falling edge. If SDA changes before we get a falling edge on SCL then we have a start or stop condition that we can evaluate. If there is a falling edge on SCL first then it means that we have a bit of data ready to take in.

When I run this sketch I just get a series of 'start' indications with no data to follow. I know that there is data passing between the two devices that I am monitoring because I set up another Arduino with a sketch that just reads data from a spare eeprom that I am testing this all with. I can monitor that one through the serial port and see that it is indeed reading data from the eeprom.

Can anyone see an issue with my logic? I have two sketches posted. The first is the Arduino that is communicating directly with the eeprom. The second sketch is for the arduino that is snooping on the first one.

#include <Wire.h>


void setup()
{
  //byte chanOne[] = {0x01,0x0B};
  Wire.begin();        // join i2c bus (address optional for master)
  Serial.begin(9600);  // start serial for output

  Wire.beginTransmission(0x50);
  Wire.write(0x00);   //write word address.  This is where we will begin reading from
  Wire.endTransmission(false);
  
}

void loop()
{
  for(int x=0;x<512;x++)
  {
    Wire.requestFrom(0x50, 1);    // request 1 byte from slave device #2
  
    while (Wire.available())   // slave may send less than requested
    {
      char c = Wire.read(); // receive a byte as character
      Serial.print(c, HEX);         // print the character
    }
    Serial.println();
    //while(1)
  }
  
  delay(1500);
}
int sda = 2;
int scl = 3;
volatile byte i = 0;
volatile byte dataBit = 0;
volatile byte dataByte = 0;
volatile boolean start = false;
volatile boolean _stop = false;
volatile boolean bit_ready = false;


void setup() {
  Serial.begin(115200);
  // put your setup code here, to run once:
  pinMode(scl, INPUT);
  pinMode(sda, INPUT);
  //I'm not sure if I am waiting for SCL to rise or SDA to change
  //so I'll just watch for both of them.
  attachInterrupt(digitalPinToInterrupt(scl),scl_rising, RISING);
  attachInterrupt(digitalPinToInterrupt(sda),sda_change, CHANGE);
}

void loop() {
  
  while(!bit_ready) {}  //Just sit here until we know that we have a bit
                        //or a 'start' or 'stop' condition
 
  if(start)
    {
      Serial.println("*");  //we'll call this the start symbol
      start = false;
      //reset our variables
      dataBit = 0;
      dataByte = 0;
    }

  else if(_stop)
  {
    Serial.println("**"); //we'll call this the stop symbol
    _stop = false;
  }

  else
  {
    //I need to 'or' the databit with my databyte
    Serial.print("h"); //inserted for debugging....but we never get here
    dataByte = (dataByte << 1) | dataBit;
    i++;            //counter to keep track of the full byte
    if(i==7)
      {
        Serial.println(dataByte,HEX);
        dataByte = 0;
      }

    if(i == 8)
      {
        //see if we get an acknowledge
        if(dataByte) Serial.println("ACK");
        dataByte = 0;
        i=0;
      }
  }

  bit_ready = false;

    
  
                     
}

void scl_rising()
{
  //data MIGHT be valid so read the sda line into data variable
  //enable sda change interrupt to monitor for start and stop condition
  //enable sclk_falling interrupt.  We wait until sclk comes back down
  //before doing anything so that we can catch 'start' and 'stop' conditions
  detachInterrupt(digitalPinToInterrupt(scl));
  detachInterrupt(digitalPinToInterrupt(sda));
  dataBit = digitalRead(sda);

  //so here we are waiting to see what happens first.  If SDA changes first
  //then we have a start or stop condition.  If SCL falls first then
  //we have a data bit ready to go.  Whichever one fires first, the interrupt
  //handler will disable the other one to avoid having them both fire.
  attachInterrupt(digitalPinToInterrupt(sda), sda_change, CHANGE);
  attachInterrupt(digitalPinToInterrupt(scl), scl_falling, FALLING);
  
}

void scl_falling()
{
  //sda didn't change while sckl was high so we know it wasn't a start
  //or stop condition.  First disable sda_change and re-enable sclk_rising
  //for the next bit of data or next start condition.  Assuming we don't have
  //a start condition we can set bit_ready to true and add the bit to our byte
  //of data
  detachInterrupt(digitalPinToInterrupt(sda));
  detachInterrupt(digitalPinToInterrupt(scl));
  bit_ready = true; //no start or stop signal so this is actual data

  //I don't need to attach the sda_change interrupt because the clock
  //has to rise before a start/stop/or data transfer is sent
  attachInterrupt(digitalPinToInterrupt(scl),scl_rising,RISING);
  
  
  
}

void sda_change()
{
  //if sda is low then we have a start condition.  Set all of our variables
  //to zero so that we can start receiving data
  //if sda is high then we have a stop condition

  //SDA changed before SCL had a falling edge
  detachInterrupt(digitalPinToInterrupt(scl)); //we aren't interested in tracking
  detachInterrupt(digitalPinToInterrupt(sda));
  if(digitalRead(sda))                        //the falling edge now
  {
    _stop = true;   //SDA was released while SCL was high which is a stop
    bit_ready = true;
    //wait for the next start condition by monitoring the SCL and SDA
    attachInterrupt(digitalPinToInterrupt(scl),scl_rising,RISING);
    attachInterrupt(digitalPinToInterrupt(sda),sda_change,CHANGE);
  }

  else

  {
    start = true;  //SDA was pulled low while SCL was high indicating a start
    bit_ready = true;
    //wait for the next rising edge on SCL
    attachInterrupt(digitalPinToInterrupt(scl),scl_rising,RISING);
  }

  
  
}

jerseyguy1996:
When I run this sketch I just get a series of 'start' indications with no data to follow. I know that there is data passing between the two devices that I am monitoring because I set up another Arduino with a sketch that just reads data from a spare eeprom that I am testing this all with. I can monitor that one through the serial port and see that it is indeed reading data from the eeprom.

Can anyone see an issue with my logic? I have two sketches posted. The first is the Arduino that is communicating directly with the eeprom. The second sketch is for the arduino that is snooping on the first one.

You are probably running into a speed issue. At 400kHz I2C, each SCL cycle is only 40 16mhz clock cycles if your program tries to execute more than ~20 assembly op codes per bit the Arduino cannot keep up. Unless you can trigger the I2C hardware to do the snooping I think you are S.O.L.

The only other thing you can do is to Stretch the SCL clock.

  • Setup your Interrupt on SCL Rising.
  • Catch the interrupt
    Go into a calculated delay loop
    Sample SDA a a predetermined offset
    Enable your Interrupt on SCL Falling
  • Catch the Interrupt,
    Drive SCL Low,
    Do your bit/byte processing,
    Setup your Interrupt for SCL Rising
    Release SCL
  • Repeat.

Chuck.

Oh bummer. I didn't realize i2c was that fast. So if I grab SCL and hold it low, will the device that is acting as Master wait until the line is released to transmit the next clock pulse?

Actually it looks like the default frequency for SCL is 100kHz in the Wire library. Doesn't mean that I'm not still running into a speed issue but I wonder if I'm missing something else. It appears that I can go as low as 31kHz. Maybe I will slow down SCL and see if that helps anything. The device that I will be using this on in real life may be going at up to 100kHz because the data sheet on this EEPROM shows a maximum SCL frequency of 100kHz. So if that is the issue I will have to explore some of the other things you suggested.

What happens when the SCL low period is extended, by forcing it low for some more time?
When the master does not abort the transmission, but waits for SCL to go high again before sending the next bit, this would allow to reduce the data rate, so that software can catch the bits better.

Another idea: clock the data bits into a shift register (SPI...), and eventually delay the ACK state by keeping SDA low, until the bits have been processed.

Hmmm....that helped. I'm still not getting valid data but it is catching both start and stop conditions as well as ACK's and occasionally it is printing out a byte of data. It may be causing some other issues though because the Arduino that I have as the Master is currently programmed to just keep requesting data from the i2c slave device, breaking for 1500 ms in between each iteration. It cycles through the program twice and then stops requesting data. I'm thinking that I can improve things by taking out a lot of the attach and detach interrupts and just watch for a start condition to sync everything up and then start counting 9 bits at a time until I get a stop condition.

That's how asynchronous serial works. It is not appropriate for synchronous but it might work for your purposes.

"Attaching and detaching" doesn't sound good. It should be more like "enable and disable." Just copying the state of SDA into a variable on the appropriate edge of the clock should not be too hard.

You have to catch only 8 bits, the 9th is kind of a stop bit.