Close

I2C Monitor/Sniffer

A project log for Breadboard enhancement

Project to add features to an ELV EXSB-1 breadboard.

just-me-nlJust Me NL 05/05/2021 at 16:510 Comments

The project already included an I2C Scanner which scans an I2C bus to determine the slave addresses on the bus but I also wanted to add an I2C monitor or sniffer however the chosen GPIO pins that are broken out are pin 5 & 6, and they are not a I2C bus. The scanner uses the SoftWire library but for sniffing you need to passively listen to the signals on the bus.

My first try was using a pin-change interrupt on GPIO1 & 2 aka digital 5 & 6 aka PortD bit 7 & PortD bit 4 aka SDA and SCL. No need to look at the falling edge of SCL, nothing interesting happening there but the falling edge of SDA indicates a Start condition. So digital 5 became an on change interrupt and digital 6 a rising edge interrupt. I added an Arduino Uno to scan the I2C bus by opening each address between 0x01 and 0x7E for writing and seeing if any device acknowledges the address. The interrupt routines just grasped the complete 1st byte of PortD and placed it, together with the current value of micros() into an array.

This resulted in a perfect capture of the signals on the I2C bus, complete with time-codes. Nice! But then I saw the bus was only running the default 100kHz. Does it also work on a 400kHz bus? Sadly, no. Could I work with just 100kHz? Probably but this Teensy has more capabilities than just raw speed and memory, it also has DMA channels, let’s try that rabbit hole, it’ll be fun they said.

There are no tutorials for how to DMA on the Teensy 3.2. But there are some mentions in the forums and Paul made a nice wrapper in the core: DMAchannels. Here are some of the information sources that helped me figure it out: https://www.nxp.com/docs/en/application-note/AN4419.pdf - An application note on how to use the PIT (periodic interval timer) and two DMA channels to create a PWM pin, this will show you how powerful these DMA channels can be. One thing bothered me though, a line on page 5: “2. The output GPIO pin number is 1 per each GPIO port. 2 output pins from 1 port is not accepted. Because GPIO can accept only 1 DMA trigger.” But thankfully a DMA channel on the other hand  can receive more than one GPIO trigger if they are from the same port. An other useful bit of information is this forumpost (no. 3) from Paul: https://forum.pjrc.com/threads/63353-Teensy-4-1-How-to-start-using-DMA and off course the source code of DMAChannel.h: https://github.com/PaulStoffregen/cores/blob/master/teensy3/DMAChannel.h

So putting all this together:

Create a DMA channel, feed it the PortD input register as source, point it to a buffer as destination, tell it how many bytes to transfer each time and after how many transfers to stop and trigger an interrupt (don’t forget to tell it to stop!) and last but not least: tell it when to do all this. So we take a look at the datasheet of the processor. And we take another and another because reading this datasheet is worse then reading a machine-translated Chinese datasheet with to many pictures in Chinese which can’t be translated. But on page 79 of the datasheet (MK20DX256 Manual) it says that Source number 52 can be used to trigger a DMA channel for port D. But how do I tell the DMA channel and how does it know which pins to trigger on? Easy with DAMChannel.h, just add dmachannel.triggerAtHardwareEvent(DMAMUX_SOURCE_PORTD); and the pins is easy as well: each pin has a configuration register (PORTx_PCRx), looking at that register (page 227 section 11.14.1) we see 4 bits named IRQC and in the table you can see that if we set these bits to 0011 we get an DMA trigger for pin changes. Paul made this easy as well, just issue this command: CORE_PIN5_CONFIG = PORT_PCR_IRQC(3)|PORT_PCR_MUX(1); after declaring it as an INPUT. The input register for PortD (GPIOD_PDIR), is 32 bits wide. So we would need a buffer 4 bytes wide for each bit we want to capture, that is a bit to much and eats too much into our memory. But the DMA channel starts with the LSB so if we say just transfer 1 byte, we get the bytes we need. 

But wait, can we get the time as well? Maybe. We can’t use millis() but maybe we can read the variable millis uses directly from the memory location? But millis() is too coarse and micros() is not a single variable we can read. Luckily the Teensy 3.2 has four 32-bit timers called Periodic Interval Timers (PIT), and they run at F_BUS which is @36MHz with the default 72MHz configuration. Theoretically that results in a resolution of 27,8 nanoseconds and a maximum timerperiod of 119,3 seconds. So if we trigger a DMA channel to store the PIT value in a buffer and set a PIT for an interrupt every 60 seconds, we get the elapsed time at each pin change. And if we store the DMA address on each PIT interrupt in an additional buffer, we can count the minutes so we can capturing periodic bursts with some deadtime in between them and still have fairly accurate timekeeping. I got it working in a separate testsketch and could capture accurate data up to 530kHz (higher became problematic due to the added capacitance of the I2C bus in spite of the added 1k pull-ups). Now to integrate this into the main program.

Discussions