300kHz I2C Sniffer

For the quadracopter code I use the NXP I2C driver which works well with the MEMs sensors however it proved unreliable with the AVR slave. This is a huge issue since this is the radio and motor control path and a failure here will certainly crash the quadracopter. So why does the I2C code work with hardware sensors and not processor based slave devices? The difference is that the hardware solutions are significantly faster in responding to the I2C state changes while the processor is slower because it needs to enter and exit an interrupt routine to process the I2C states.  The I2C protocol allows for clock stretching which means; even though the master is driving the clock the slave is allowed to extend the clock by holding it low until it completes its current operation. The master needs to deal with this behaviour and for the most part it does except at the end of the I2C operation when the STOP bit is set. The NXP processor was too fast in starting the next transaction and the AVR missed the START bit and as a result every second transaction was NAK’d because the AVR wasn’t responding. Figuring this out took some time and it required me to design a piece of test equipment (I2C Sniffer) to isolate the problem.

The I2C sniffer is a circuit that samples both the SDA and SCL lines of an I2C bus and decodes all transactions into text messages. I reused the NXP core processor design as is, and the analyzer becomes just another device in the quadracopter board stack up. There are a number of older designs on the internet however these all use relatively slow 8 bit controllers so there bandwidth is limited to a 100kHz I2C bus. The other issue with these I2C analyzers is that they don’t typically provide timing information which I wanted in order to detect the bus performance of the various I2C devices. In fact without the timing information I would not have discovered why I couldn’t talk to the AVR slave device. The design presented here is inexpensive to build and has the following features.

  • sample and decode 0 to 300kHz I2C bus
  • 1 uS timestamps on all events
  • modern USB serial interface to a host terminal program
  • Cross platform support for Win/Linux/Mac
  • Programming the device only requires a TTL serial port and Flash Magic

An example trace from the I2C sniffer is shown here. The sequence shows the master writing 0x05 to register 1 of the L3G4200D. This is followed by a read of register 0x28 which returns 0x1b as a result. The decimal numbers are the timestamps which indicate how long each transaction took and are measured in microseconds. For the write “35 0x01 Ack” it takes 35uS to transfer the 10 bits that make up the byte or 3.5 us/bit which is ~300kHz.  All I2C transactions begin with the START bit which is immediately followed by the device address. Data transfers are highlighted with an underscore.

23624 Start   96 Wr 0xd0 Ack   35 0x01 Ack   35 0x05 Nak   65 Stop

     470 Start   95 Wr 0xd0 Ack   35 0x28 Nak    5 Start   96 Rd 0xd1 Ack   35 0x1b Nak   66 Stop


I have written code to turn the LPC1769 processor board into an I2C bus sniffer. The great thing about using a 100MHz processor is that I can write all of the code in ‘C’ and still have enough performance to measure a 300kHz I2C bus. The code to handle the bit monitoring of the SDA/SCL lines takes advantage of the pin change interrupt to detect edge transitions. There are three states that need to be handled; these include looking for the START, STOP and bit sampling data.

Sampling the SDA/SCL lines must occur in real time hence the pin change interrupt priority is set higher than the USB interrupts. This avoids nasty timing issues which would occur if the interrupt priorities were the same which is there default state.

NVIC_SetPriority(EINT3_IRQn,3);     //Set the pin change interrupt
NVIC_SetPriority(USB_IRQn,10);      //priority so its greater than USB

The pin change interrupt queues data in a circular buffer which adds very little delay to the ISR. In effect an interrupt will be serviced faster than the maximum I2C edge rate of 400kHz. Whenever the processor is not in the pin change interrupt then a background routine reads data out of the buffer and forwards it to the host computer using the USB connection. I set up the USB to run as a virtual serial port so that data can be displayed on any host computer running a terminal program. The LPC1769 has 32K of SRAM so I dedicate 4K to the circular buffer which will easily keep up with the full 300kHz I2C event rate without the risk of overflow.

void EINT3_IRQHandler(void)
	if((LPC_GPIOINT->IO0IntStatF & SDA)){
		if(LPC_GPIO0->FIOPIN&SCL){			//START on SDA falling with SCL high
			queue_data(TIMESTAMP|('S'<<8));		//print time stamp
			LPC_TIM0->TC=0;				//reset time stamp
			LPC_GPIOINT->IO0IntEnR = SDA|SCL;	//enable STOP and DATA sampling
			sniffer_bit = 0;			//start assembling new byte
	}else if((LPC_GPIOINT->IO0IntStatR & SDA)){
		if(LPC_GPIO0->FIOPIN&SCL){			//STOP on SDA rising with SCL high
			queue_data(TIMESTAMP|('P'<<8));		//print time stamp
			LPC_TIM0->TC=0;				//reset time stamp
			LPC_GPIOINT->IO0IntEnR &= ~SCL; 	//disable DATA sampling
	}else if(LPC_GPIOINT->IO0IntStatR & SCL){//sample data on SCL rising
		if(sniffer_bit++ == 8){				//print the data byte
			queue_data(TIMESTAMP|HANDSHAKE|sniffer_data);//print time stamp
			LPC_TIM0->TC=0;				//reset time stamp
			sniffer_bit = 0;			//prepare for next byte
			if(LPC_GPIO0->FIOPIN&SDA){		//assemble data byte
				sniffer_data = sniffer_data<<1|1;
				sniffer_data = sniffer_data<<1;
	LPC_GPIOINT->IO0IntClr = SDA|SCL;  		        //clear interrupt flag


Prior to optimizing the code for the ISR the longest execution time through the interrupt was ~2.5us. To improve the performance of the sniffer code I changed the start up code so that it initializes the processor for 120MHz operation. I also changed the Flash accelerator code to insert 6 wait states as required by the clock rate change. Finally I changed the GCC code optimization to -Os for size rather than speed(-O3) so that I would use the Flash accelerator more efficiently.

#define PLL0CFG_Val     0x00040063        //N=6 M=100 Fin=12 Fcc=480MHz 120MHz
#define FLASHCFG_Val    0x00005000        //6 wait states 120MHz

The Flash accelerator is an NXP chip specific feature that the GCC compiler does not account for.  The Flash accelerator basically consists of eight 128 bit buffers that are pre-fetched with the next expected Flash data. Using a 128 bit wide Flash allows the 6 Flash wait states to be averaged across what is effectively eight MThumb(16 bit) instructions which gives close to zero wait state code execution. If you were to use 32bit instructions only four instructions would fit in each buffer register which would introduce 6/4 clocks or 0.5 wait states. The choice to optimize the GCC code for size means more of the ISR code is running with zero wait states.

The results of these tweaks allow the same code to execute in 1.75us.  I would have liked to work on getting this down to ~1.25us where I could then trace the full 400kHz I2C rate but I have other priorities and this code works well enough to solve my quadracopter issues.


Leave a Reply

Your email address will not be published. Required fields are marked *