Decode I2C data with a Siglent SDS1202X-E Oscilloscope
Updated: 2022-02-01 (Formatted model number list)
I’m an electronics amateur (noob). Recently, I’ve been building a microcontroller project that communicates with some devices via the I2C serial protocol. Update: the multitimer project is complete.
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:
-
SDS1202X-E ← I have this one
-
SDS1104X-E
-
SDS1204X-E
As you might have guessed from the "202", the SDS1202X-E 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.
Auto trigger
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:
-
Type: Serial
-
Protocol: I2C
-
Signal
-
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.
Horizontal

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!
History

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.
Conclusion
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!
The decoding
Here’s the raw notes I wrote/transcribed, which you can find in the
/reference
directory in the repo:
------------------------------------------------------------------------------- 2021 David Gauer - License: CC-BY-SA-4.0 ------------------------------------------------------------------------------- This is the I2C communication I captured by running the Adafruit CircuitPython example (see neokey1x4_example.py in this directory) and Adafruit libraries. It setups up the NeoKey 1x4's "seesaw" microcontroller, the neopixel sub-device, and GPIO for the keyboard switch inputs. NOTE: In my actual project, I did a proper GPIO pin bulk set on all four keyboard switch pins at once. I also enabled interrupts on the four pins and used the NeoKey's interrupt out line. Capture device was a Siglent SDS1202X-E Oscilloscope. See http://ratfactor.com/siglent-i2c for an illustrated guide to setting that up. This was hand-written (I even started on paper!) because there isn't that much of it and, frankly, just having this information at all was awesome and I felt like King Geek getting this far. :-) Adafruit "seesaw" microcontroller setup ------------------------------------------------------------------------------- w 00 7f ff - status swrst w 00 01 - status id r 55 - 'U' is the correct id (0x55) w 00 02 - status version r 13 74 2a a4 - version number response On-board NeoPixel setup ------------------------------------------------------------------------------- w 0e 01 03 - neopixel (0e) pin (01) is 3 w 0e 03 00 0c - neopixel buf_length 000c (0xC = 12 bytes, 3 colors * 4 pixels) GPIO setup (for each of the four keyboard IO pins) ------------------------------------------------------------------------------- w 01 03 00 00 00 10 - gpio dirclr_bulk w 01 0b 00 00 00 10 - gpio pullenset w 01 05 00 00 00 10 - gpio bulk_set w 01 03 00 00 00 20 w 01 0b 00 00 00 20 x3 for each pin w 01 05 00 00 00 20 ... 40 ... 80 Reading the key pin statuses (done in a loop) NOTE: I used interrupts to get notified when a key was pressed and then polled the device in my actual project. ------------------------------------------------------------------------------- w 01 04 - gpio bulk (read) r xx xx xx F0 - Only the F part of F0 are the pins we care about!