I wanted to build an application that could sniff i2C transactions. In particular, I was interested in the actual data transactions between the Wii NunChuk and WiiRemote. This particular data interchange utilizes 400KHz i2C protocol. So, I thought this was a good opportunity to use arduino interrupts, especially the change on pin interrupts. What I envisioned is a state-machine that would perform work on transitions of either SCL or SDA – or so I thought.
What I found out was the overhead in the simple version of attaching interrupts was too much to do anything useful at the 400KHz data rate. I changed to edge-sensitive interrupts without any improvements. I had to find out what was going on.
I started looking at the actual assembly code generated from the “attachInterrupt” arduino reference library. Here’s what I found.
Here’s a simple code example:
#define LED_PIN 5 // digital pin #13 (portb)
#define LED_ON() PORTB |= _BV(LED_PIN)
#define LED_OFF() PORTB &= ~_BV(LED_PIN)
void myfunc() {
LED_OFF();
}
void setup() {
pinMode(13, OUTPUT);
attachInterrupt(0, myfunc, RISING);
}
void loop() {
LED_ON();
delayMicroseconds(20);
}
I tied the LED output back to Digital pin #2 and this is running on a 16MHz Arduino. Notice, all this program does is turn an LED on then the interrupt, which responds to the rising of the LED voltage, turns off the LED. Here is a picture of the LED “on” strobe:

As can be seen, the width of the LED “on” is 3.435usec, which is considerable longer than the 600ns that SCL of the i2C protocol is high! So, this won’t work to respond to SCL changes. This is about 55 clock cycles @ 16MHz! What is going on? Also, what about the 20usec delay in the loop?

Notice that the total period for the LED is 26.60usec. That means the off time for the LED is 23.165usec, which is 3.165usec longer than the delay statement request. Let’s now look at what happens under the hood. To do that we have to look at the assembled instructions that came from the program’s compile.
The following is what is generated for the interrupt handler for INT0. Using the atmegas168 datasheet and information about instructions we can count the number of instruction cycles. There are 45 cycles before there is a call to the “myfunc” and there are 35 cycles after return of “myfunc” and return from the interrupt handler. There is also 3 cycles in the pin synchronizer and another 3 cycles for a JMP instruction (which is in the interrupt vector table). So, we have a total of 51 cycles before we start to execute “myfunc”.

Now the “myfunc” has some amount of delay before it sets the LED off. As can be seen, the overhead for a “cbi” instruction is about 1 cycle. We’ve accounted for 52 cycles, there are another in 2 cycles in the “sbi” instruction in the “loop” that actually turns the LED on, and also this blocks interrupts until after the instruction completes. Now we’ve accounted for 54 cycles. The other cycle is likely due to the fact that we are setting the LED synchronously with the same clock as the interrupt sampling; this will add another cycle for synchronization. There we are, all 55 cycles accounted for!

So, the simple to use “attachInterrupt” has a too much of a significant overhead for the application that I would like to do. I’ll have to find another solution.
I made changes to the program. I got rid of the generic “attachInterrupt” and manually enabled the interrupt. I also used the “ISR” construct to declare the interrupt handler. To compile this I had to unfortunately modify Arduino’s “WInterrupts.c” source (located in the \hardware\cores\arduino folder) – I commented out the SIGNAL operations for INT0_vect as this conflicts with the use of “ISR” statement in my application.
#define LED_PIN 5 // digital pin #13 (portb)
#define LED_ON() PORTB |= _BV(LED_PIN)
#define LED_OFF() PORTB &= ~_BV(LED_PIN)
ISR(INT0_vect) {
LED_OFF();
}
void setup() {
pinMode(13, OUTPUT);
// enable INT0 interrupt on change
EICRA = 0x03; // INT0 – rising edge on SCL
EIMSK = 0x01; // enable only int0
}
void loop() {
LED_ON();
delayMicroseconds(20);
}
The changes to the actual waveform on LED were remarkable:

The overhead of 1.118usec is now only about 18 clock cycles. Unfortunately, this is still too much for me to use interrupts to process i2C message. In addition, it may now be obvious that the background interrupts to keep the delay clock alive are also adding cycles everytime that supporting interrupt occurs. This extra delay occuring at random places in your application could be significant and cause the occasional strange behavior.
This is a good example of using the instruction and cycle counting methodology to better understand how to improve critical timing in an application.
cheers!