I’m an electronics amateur (noob). Recently, I’ve been building a microcontroller project that communicates with some devices via the I2C serial protocol.
I wanted to reverse-engineer the setup of one of the devices. I had the source code and was able to get 90% of the way there, but it wasn’t working and I was flying blind.
Then I remembered that my new Siglent oscilloscope claimed it could decode serial data from protocols including I2C. It took me the better part of the day to capture the data, decode it, and match it up with the software instructions. But it worked and was very, very cool.
I believe these instructions apply to all models in the SDS1000X-E series which includes the SDS1202X-E, SDS1104X-E, and SDS1204X-E. I have the 1202X-E, which is the 2 channel 200Mhz model (the cheapest of the three).
Connect the probes
I2C only needs two channels - the clock and data lines.
In the image above, you can see that I’ve connected three wires to the I2C bus on my breadboard project:
Blue wire - I2C data - connected to the Channel 1 (yellow) probe
Yellow wire - I2C clock - connected to the Channel 2 (purple) probe
Black wire - ground - connected to both probe ground alligator clips
The important thing is to pick a channel for the clock and data and remember which is which.
I often have to remind myself that an oscilloscope is really just a fancy voltmeter that samples voltages over time. To measure voltage, you need to measure the difference between two points in the circuit. The "ground" connection is vital because it is the reference by which the clock and data voltages are measured - this is just as true of the devices communicating as it is of the oscilloscope.
If at all possible, I highly recommend that you set your circuit to send a continuous stream of I2C data…and ideally a repeating pattern that you can easily identify when you see it decoded. (Like a series of 0x00 0x55 0x00 …) That will make it much easier to get your trigger and decoding set up.
My scope defaults to "auto" trigger, which usually does a fantastic job. Here we can see that the clock and data voltages are getting caught by the trigger, but we’re zoomed in so far that they appear to be nearly vertical lines.
To capture I2C data, I found that we need to set up the trigger manually and capture the signal in "Normal" mode.
Normal trigger, serial mode
In the above image, you can see that I’ve selected the Normal button on the Trigger panel. I’ve also adjusted the vertical magnitude and position of the two channels so that they appear on top of each other.
You’ll need to visit every item in the trigger controls using the soft menu (row of buttons below the screen) and "universal knob" (the one with the glowing "Intensity | Adjust" label in the top cluster).
The important things are to set:
The clock and data channels
The threshold voltages
Trigger: "Start" is a good one to start with (pun intended)
Trigger signal thresholds
It will be easiest (or in my case, possible) to set the signal thresholds if you can see the I2C communication with nice, visible waveforms like in my display above.
You want to get the threshold lines for each channel so that they are in the vertical middle of the waveform (between the highest and lowest voltages).
Decode I2C data
As you can see in the trigger threshold adjustment image above, I’ve turned on the Decode ability (which is why the table grid lines now appear at the top of the screen).
With any luck, you may start to see I2C data being decoded in the table and at the bottom of the screen below the waveforms.
At first, my data was nonsense…
One of the crucial things that it took me a while to understand is that the Trigger threshold settings and the Decode settings are separate! So you’ll need to adjust the channels and their thresholds in the Decode menu as well before you’ll get correctly decoded data.
As near as I can tell, the scope will only decode what you capture on the screen. If you look closely at the image above, you’ll see that below the captured waveforms, the scope is showing decoded data (the green hex number is the device address, (w) means "write" and (r) means "read" from the point of view of the I2C master device).
In my case, I was sending the repeated ASCII characters "foo" (0x66 0x6F 0x6F).
And if you look very closely, you can see this same data in the first line of the table above.
If the entire stream of data you’re trying to capture doesn’t fit on the screen, the scope won’t decode it. So this is where you use the Horizontal magnitude and position knobs to get as much of the signal to fit as you need. This takes some patience!
It took me a while to realize that I could zoom way out with the Horizontal controls and capture a long stream of data.
In Normal mode, the scope is continuously recording whatever is captured by your trigger settings. Once the buffer fills up, it begins to overwrite the oldest captures with new ones (like a circular buffer, if you’re familar with that term).
So if you can make your device send whatever you want to capture and then not send anything else, that’s ideal. That’s the exact opposite of what I suggest to get your thresholds and decoding working initially. If you’re quick enough, I guess you could also hit the "Run | Stop" to prevent overwriting the data you want as well.
What’s really cool is that you can record entire sequences (called "Frames" in the scope menus) and then play them back via the History menu (the scope stops recording while you’re viewing history).
In the above image, you can see that there were multiple I2C transactions captured in this single frame. That’s what the table is for at the top! You can even scroll the table for long lists using the Decode menu.
The History menu also lets you view entire separate frames (you can see that this screenful is Frame 3 of 3) of captured communication.
We live in amazing times when a general-purpose test instrument this powerful is (relatively speaking) cheap enough for an amateur like myself to justify owning.
Without this ability, I was blind to the error in the communication from my microcontroller (a Raspberry Pi Pico) to my I2C device (an Adafruit NeoKey 1x4 switch/LED package with a "seesaw"-programmed microcontroller).
As soon as I could see what the vendor’s library code was sending, I could immediately see the error in what I was sending with my own code. Rad!