esp32 intermediate 30 min

ESP32: debug with a logic analyzer and protocol decoders

When the Serial.print tells you nothing, the I2C bus tells you everything. Pick a $10 clone or a $400 Saleae, install Sigrok and PulseView, decode I2C/SPI/UART.

Code available for: ESP32 ArduinoArduino C
Published Aug 26, 2026

I spent two days convinced a BME280 sensor was broken because my ESP32 was reading zero humidity. The I2C scanner said the device was there at 0x76. The library was the right one. The wiring matched the datasheet. I was about to order a replacement sensor when I borrowed a logic analyzer, clipped three wires onto SDA, SCL, and GND, and watched the actual bus traffic.

The ESP32 was sending the right address. The sensor was ACKing every byte. Then on the third byte (the humidity config register), the ESP32 was sending 0x00 instead of 0x01. Typo in my code. The Serial.print of the value being written showed the right number; the byte actually going out on the wire was wrong because of a bit-shift bug two lines earlier. Serial.print told me what I thought was true. The bus told me what was actually happening.

This tutorial is the workflow I use now whenever something is not working and Serial.print is not enough.

What a logic analyzer does

A logic analyzer records digital signals (high/low) on multiple channels at the same time. You connect probes to the pins you care about (e.g. SDA and SCL for I2C, MOSI/MISO/SCK/SS for SPI, TX/RX for UART). The analyzer samples those pins at a fixed rate and stores the transitions. You then look at the recorded waveform on your computer.

The difference between a logic analyzer and an oscilloscope:

  • Logic analyzer: many channels (8 to 32 is common), only digital (high/low, not analog voltage), long capture time (millions of samples), designed for protocols like I2C/SPI/UART.
  • Oscilloscope: fewer channels (2 to 4 typical), analog voltage, short capture time, designed for waveforms and timing.

If you want to know “is the right byte going out on the I2C bus,” the logic analyzer is the tool. If you want to know “what does the rising edge of this signal look like,” the oscilloscope is the tool. They overlap in the middle but the tool choice is usually obvious.

The cheap options (and the not-cheap one)

$10 Saleae clone (the pick for getting started):

These are USB dongles with 8 channels, 24 MHz sample rate, that work with the Saleae Logic software (the clone vendors reverse- engineered the protocol). You can find them on AliExpress by searching “Saleae Logic Analyzer” or “CY7C68013A logic analyzer.” The clone of the original Saleae Logic (8-channel, 24 MHz) is about $8-12.

$400 Saleae Logic Pro 8:

The real thing. 8 channels, 500 MHz sample rate, well-built, comes with software that just works. Worth it if you are doing this weekly. Not worth it if you are doing this once.

$30-50 Kingst LA2016 / LA1016:

Chinese brand, decent quality, official Sigrok support (no clone hassle). About $30 for the 16-channel 100 MHz version. This is what I use day-to-day.

$100-150 Rigol logic probe modules:

Add-on for some Rigol oscilloscopes. If you already have a Rigol scope, this is a no-brainer.

For the rest of this tutorial I am going to assume the $10 Saleae clone because that is what most people have and what I started with. The Sigrok workflow works the same on all of them.

Sigrok and PulseView setup

Sigrok is the open-source protocol decoding suite. PulseView is its GUI. Together they handle just about every logic analyzer on the market, including the $10 clones.

Linux:

sudo apt install sigrok pulseview

macOS:

brew install sigrok pulseview

Windows:

Download the PulseView installer from https://sigrok.org/wiki/Downloads. There is a single installer that includes the CLI tools and the GUI.

For the Saleae clone specifically:

The clone needs a kernel driver on Windows (the zadig tool handles this) and a libusb setup on Linux. Sigrok’s wiki has a page specifically for the “Saleae Logic clone” that walks through the driver install. Spend the 20 minutes, save yourself years of “why does my analyzer not show up” frustration.

When you plug the analyzer in, PulseView should auto-detect it. If it does not, check Device >> Connect to device and pick the right driver. For the $10 clones, the driver is fx2lafw.

Basic digital capture (the no-protocol workflow)

The simplest thing you can do: probe one GPIO, capture the waveform, see if the transitions match what you expect.

  1. Connect the analyzer’s GND clip to your board’s GND.
  2. Connect channel 0 to the GPIO you want to watch.
  3. In PulseView, click the green Run button.
  4. Toggle the GPIO from your firmware.
  5. PulseView shows a square wave (or whatever your firmware is actually producing).

If you see the waveform you expected: the pin is wired correctly and the firmware is driving it. If you see nothing: the pin is not toggling, or your probe is on the wrong pin, or the analyzer’s ground clip is not connected (this last one is the silent killer; without a ground reference, the analyzer floats and shows random noise).

The default sample rate is 1 MHz, which is enough for almost everything below SPI at 1 MHz. For faster signals, click the sample rate dropdown and bump to 10 MHz or 100 MHz.

The I2C / SPI / UART protocol decoders

This is where the magic happens. PulseView ships with decoders for every protocol you care about. Instead of staring at a square wave trying to mentally parse the bits, you get a human-readable timeline:

[IDX] SDA SCL  | Interpretation
[001] --- ___  | START
[002] 110 0011 | Address: 0x76 (W)
[003] 0-- 0010 | ACK
[004] 1110 0011| Data: 0xE3
[005] 0-- 0010 | ACK
...

To use a decoder:

  1. Capture your signal (channels 0 and 1 for I2C, four channels for SPI, two for UART).
  2. Click the + icon next to “Decoders” in the right sidebar.
  3. Pick the protocol (e.g. i2c).
  4. Tell PulseView which channel is which (SDA, SCL for I2C; MOSI, MISO, CLK, CS for SPI; RX, TX for UART).
  5. The decoded annotations appear above the raw waveform.

For SPI, set the clock polarity and phase (CPOL/CPHA) to match your device. For UART, set the baud rate (the decoder can auto-detect from a known string, but explicit is faster). For I2C, nothing extra; the protocol is well-defined.

A real example (the BME280 debug)

Back to the original bug. I had:

  • Channel 0: SDA
  • Channel 1: SCL
  • Sample rate: 1 MHz
  • Decoder: I2C, SDA = D0, SCL = D1

The capture showed:

START
Addr 0x76 (W) ACK
Data 0x74 ACK      <- register pointer: 0x74 (humidity ctrl)
Data 0x00 ACK      <- value: should be 0x01, was 0x00
STOP
START
Addr 0x76 (R) ACK
Data 0x00 NACK     <- readback confirms: sensor got 0x00
STOP

The two-byte write said “set register 0x74 to value 0x00.” The sensor obediently did that. The Serial.print in my firmware was the value I intended to send (0x01), but a << 1 left over from an earlier draft of the code shifted the bit out before it hit the wire. Three minutes with the logic analyzer, two minutes to fix the code.

When to use what (the picking chart)

  • Serial.print: 80% of debugging. Use it for “what value did this variable have,” “did this code path execute,” “what error code came back from this call.” Cheap, fast, no extra hardware.
  • Logic analyzer: when Serial.print is not enough. Protocol traffic, pin toggles you cannot easily print (because it is hardware-driven), timing of interrupt handlers, signal integrity on digital lines.
  • Oscilloscope: when you need to see the analog shape of a signal. Power supply noise, PWM waveform edges, analog sensor outputs, signal integrity on long wires.

The order I reach for tools: Serial.print, then logic analyzer, then oscilloscope. Each is more powerful but slower to set up than the last.

GPIO timing analysis

One thing the logic analyzer does that nothing else does well: measure timing between events across multiple pins. “How long after CS goes low does the SPI clock start?” “Is the interrupt pin asserted within 10 us of the I2C transaction ending?”

PulseView has a “Show cursors” tool. Drag the cursor onto one event (e.g. the falling edge of SCL), drag the second cursor onto another event (e.g. the next rising edge of SDA). The delta appears in the bottom toolbar in microseconds. You can also right- click on a transition and set “mark” to align multiple captures side by side.

Common gotchas

  • Sample rate too low. If your signal is 1 MHz and your sample rate is 1 MHz, you get one sample per cycle. The waveform looks blocky and the decoder might mis-read edges. Sample rate rule of thumb: at least 10x the fastest signal frequency. For a 1 MHz SPI bus, sample at 10 MHz or higher.

  • Not enough channels. Eight channels is the standard. If you are debugging a 4-wire SPI plus a UART plus two GPIO events, you need 7 channels. Plan ahead.

  • Ground clip not connected. The silent killer. The analyzer floats, shows random noise, and you chase ghosts for an hour. Always clip ground first.

  • Long wires picking up noise. The clip leads that come with cheap analyzers are unshielded and pick up everything. For high- speed signals, solder a short pigtail directly to the test point. For everything else, the clips are fine.

  • Decoder config mismatch. SPI decoders need the right CPOL/CPHA. UART decoders need the right baud rate. If the decoded output is gibberish, the config is wrong before the signal is wrong.

  • Trigger not set. By default PulseView captures from “now.” If your event is intermittent, you might capture the wrong window. Use a trigger (rising edge on CS, falling edge on INT, etc.) to start the capture at the right moment.

What to build next

  • A test sketch that deliberately mis-drives an I2C device (wrong register address, wrong byte count) so you can practice reading the decoder output.
  • An SPI flash capture with the spi decoder set to mode 0; verify the read command sequence matches the datasheet.
  • A UART capture at 115200 baud with the uart decoder, compare decoded bytes to what Serial.print shows. Useful sanity check.
  • The esp32-oscilloscope-basics tutorial if you do not have a scope yet. The scope and the logic analyzer are the two bench tools you actually need.