Skip to content
Home » Embedded Systems » Basic Electronics » Using a Logic Analyzer for Embedded Debugging: Complete Guide

Using a Logic Analyzer for Embedded Debugging: Complete Guide

Embedded Systems Learning Path
Part 104 of 129View Full Path →

KEY TAKEAWAYS

  • A logic analyzer captures digital signals and decodes them into protocols (I2C, SPI, UART), making invisible communication visible.
  • Budget USB analyzers ($10-$25 like Saleae clones) are sufficient for most embedded debugging up to 24MHz.
  • Protocol decoders automatically parse raw bit streams into addresses, data bytes, ACK/NACK — saving hours of manual analysis.
  • Trigger modes (edge, pattern, protocol) capture only the events you care about, even in long-running systems.
  • Adding GPIO “toggle pins” in firmware lets you measure ISR latency, function timing, and task scheduling on the analyzer.

What Is a Logic Analyzer?

A logic analyzer is a digital measurement instrument that captures and displays digital signals over time. Unlike an oscilloscope that shows analog voltage waveforms, a logic analyzer sees only HIGH and LOW states — exactly what your microcontroller sees. This makes it the perfect tool for debugging digital communication protocols like I2C, SPI, and UART.

When your I2C sensor returns garbage data, or your SPI flash writes seem to go nowhere, or your UART communication drops characters, a logic analyzer shows you exactly what’s happening on the wire — bit by bit, byte by byte.

Types of Logic Analyzers

USB Logic Analyzers ($10–$500): Connect to your PC via USB. The PC software handles display and protocol decoding. Best choice for most embedded engineers. Examples: Saleae Logic 8 ($479), DSLogic Plus ($149), and the ubiquitous Saleae clones ($10–$25).

Standalone/Benchtop Analyzers ($500–$50,000+): Have their own screens and controls. Faster sampling, deeper memory, more channels. Used in professional labs for complex designs.

FPGA-based Analyzers: Built into development boards (Xilinx ILA, Intel SignalTap). Capture signals inside the FPGA fabric.

For learning and most embedded development, a $10–$25 USB analyzer with 8 channels and 24MHz sampling is more than adequate for I2C (400kHz), SPI (up to ~4MHz), and UART (up to 1Mbps).

Connecting the Logic Analyzer

Proper physical connection is the first step to getting useful captures. A logic analyzer reads digital signals, so you connect its probe channels to the signal lines you want to monitor. Ground connection is critical — without a solid ground reference between the analyzer and your target board, you will see noise and false triggers. Here is a systematic approach to connecting your logic analyzer for common embedded protocols.

/* Logic Analyzer Connection Guide
 *
 * CRITICAL: Connect the analyzer's GND to the target's GND!
 * Without a common ground reference, all signals are meaningless.
 *
 * ┌──────────────┐          ┌──────────────────┐
 * │ Logic        │          │  MCU Board        │
 * │ Analyzer     │          │                   │
 * │              │          │                   │
 * │ CH0 ─────────┼──────────┤ SDA (I2C data)    │
 * │ CH1 ─────────┼──────────┤ SCL (I2C clock)   │
 * │ CH2 ─────────┼──────────┤ MOSI (SPI)        │
 * │ CH3 ─────────┼──────────┤ MISO (SPI)        │
 * │ CH4 ─────────┼──────────┤ SCK  (SPI clock)  │
 * │ CH5 ─────────┼──────────┤ CS   (SPI select) │
 * │ CH6 ─────────┼──────────┤ TX   (UART)       │
 * │ CH7 ─────────┼──────────┤ Debug Toggle Pin  │
 * │              │          │                   │
 * │ GND ─────────┼──────────┤ GND               │
 * └──────────────┘          └──────────────────┘
 *
 * Tips:
 * - Use short probe wires (10MHz), use proper probe clips
 */

Capturing and Decoding I2C

Let’s walk through a real debugging scenario: your BMP280 temperature sensor on I2C returns 0xFF for every register. Is it a wiring issue? Wrong address? Missing pull-ups? The logic analyzer tells you.

/* Step 1: Add instrumentation to your firmware */

#include 

/* Debug toggle pin — visible on the logic analyzer */
#define DEBUG_PIN_PORT  GPIOA
#define DEBUG_PIN       (1 <BSRR = DEBUG_PIN;    /* Set HIGH */
    __asm volatile ("nopnnopnnop");     /* Brief pulse */
    DEBUG_PIN_PORT->BRR  = DEBUG_PIN;    /* Set LOW */
}

void read_bmp280_with_debug(void) {
    uint8_t chip_id;

    debug_pulse();  /* Marker: "I2C transaction starts here" */

    i2c_status_t status = i2c_mem_read(&i2c1, 0x76, 0xD0, &chip_id, 1, 100);

    if (status == I2C_OK) {
        debug_pulse();
        debug_pulse();  /* Two pulses = success */
    } else {
        /* Leave pin HIGH = error indicator */
        DEBUG_PIN_PORT->BSRR = DEBUG_PIN;
    }
}

/* Step 2: Analyzer capture settings
 *
 * Software: PulseView (free, open-source) or Saleae Logic
 *
 * Channels:
 *   CH0 = SDA  → Label "SDA"
 *   CH1 = SCL  → Label "SCL"
 *   CH7 = PA8  → Label "DEBUG"
 *
 * Sample rate: 2MHz (20x oversampling for 100kHz I2C)
 * Capture length: 100ms (captures several transactions)
 * Trigger: Falling edge on CH1 (SCL) — catches START condition
 *
 * Step 3: Add I2C protocol decoder
 *   In PulseView: Decoders → I2C
 *   Set SDA = CH0, SCL = CH1
 *
 * What you'll see in a WORKING capture:
 *   [S] [0xEC] [ACK] [0xD0] [ACK] [Sr] [0xED] [ACK] [0x58] [NACK] [P]
 *    ↑    ↑      ↑     ↑      ↑    ↑     ↑      ↑     ↑      ↑     ↑
 *   START addr+W  ok  reg_addr ok  rSTART addr+R  ok  chip_id last  STOP
 *
 * Common failure patterns:
 *   [S] [0xEC] [NACK] [P]
 *     → Device not responding. Check: wiring, power, pull-ups, address
 *
 *   SCL is flat LOW:
 *     → Clock stretching stuck. Slave device hung. Power cycle.
 *
 *   SDA stuck LOW, SCL toggling:
 *     → Bus locked. Need bus recovery (9 SCL clocks).
 *
 *   No transitions at all:
 *     → Check I2C peripheral clock enable (RCC), GPIO config
 */

Capturing and Decoding SPI

SPI captures require four channels at minimum: SCLK, MOSI, MISO, and CS. The analyzer’s protocol decoder needs to know the clock polarity and phase (CPOL/CPHA) to correctly interpret the data. If you are unsure of the SPI mode, capture first and try all four mode combinations until the decoded data makes sense. For SPI protocol fundamentals, see SPI Protocol Deep Dive.

/* SPI Protocol Decode Setup
 *
 * Channels needed: MOSI, MISO, SCK, CS (4 channels minimum)
 *
 * Analyzer settings:
 *   Sample rate = 10× SPI clock speed
 *   Example: 1MHz SPI → 10MHz sample rate
 *
 * Protocol decoder settings:
 *   Clock polarity (CPOL): 0 or 1 (check your SPI mode)
 *   Clock phase (CPHA): 0 or 1
 *   Bit order: MSB first (most common)
 *   CS active: LOW (most common)
 *
 * Example: Reading W25Q flash chip ID
 *
 * Capture shows:
 *   CS: ‾‾‾___________________________________/‾‾‾
 *   SCK: ___/‾_/‾_/‾_/‾_/‾_/‾_/‾_/‾_ ...
 *   MOSI: [0x9F][0x00][0x00][0x00]
 *   MISO: [0xFF][0xEF][0x40][0x18]
 *             ↑     ↑     ↑     ↑
 *          dummy  Mfr=EF  DevID  Capacity
 *                 (Winbond) (W25Q) (128Mbit)
 *
 * Common SPI issues visible on analyzer:
 *
 * 1. Wrong SPI mode — data sampled on wrong clock edge
 *    Fix: Toggle CPOL or CPHA in your SPI config
 *
 * 2. CS stays LOW between bytes — some devices need CS toggle
 *    between commands (page program sequence)
 *
 * 3. MISO is always 0xFF — slave not responding
 *    Check: CS wiring, SPI clock speed (too fast?), power
 *
 * 4. Data shifted by 1 bit — clock polarity mismatch
 *    The decoder shows garbage; raw bits look "almost right"
 */

Capturing and Decoding UART

UART is the simplest protocol to capture — you only need one channel per direction (TX and RX). However, the analyzer must know the baud rate to decode the data correctly. If you don’t know the baud rate, you can often determine it by measuring the width of the narrowest pulse in the capture, which represents one bit period. The baud rate is the inverse of that period. For UART fundamentals, see UART Protocol Deep Dive.

/* UART Protocol Decode Setup
 *
 * Only 1 channel needed per direction (TX or RX)
 * For bidirectional: 2 channels (MCU TX + MCU RX)
 *
 * Decoder settings:
 *   Baud rate: must match EXACTLY (e.g., 115200)
 *   Data bits: 8 (most common)
 *   Parity: None (most common)
 *   Stop bits: 1 (most common)
 *   Bit order: LSB first (standard for UART)
 *
 * Sample rate: 4× baud rate minimum (8× recommended)
 *   115200 baud → at least 460kHz, ideally 1MHz
 *
 * Common UART issues visible on analyzer:
 *
 * 1. Baud rate mismatch — decoder shows garbage characters
 *    Measure actual bit width: 1/baud_rate
 *    115200 baud → each bit = 8.68µs
 *    If you measure 9.6µs → actual baud ≈ 104167 (≈9600×10?)
 *    Means firmware is at 9600 baud, not 115200!
 *
 * 2. Inverted logic — some USB-UART adapters invert TX/RX
 *    Idle state should be HIGH (mark). If LOW → inverted.
 *
 * 3. Framing errors — stop bit not HIGH
 *    Usually means baud rate is slightly off (clock calibration)
 *
 * 4. Missing characters — TX buffer overrun
 *    Capture shows the MCU stopping mid-stream
 */

Timing Measurements with Toggle Pins

Beyond protocol decoding, logic analyzers are excellent for measuring timing. By toggling GPIO pins at strategic points in your firmware, you can measure ISR latency, function execution time, task scheduling intervals, and more.

/* Using toggle pins for timing measurements
 *
 * Assign dedicated GPIO pins for debug instrumentation.
 * Toggle them HIGH/LOW around code sections of interest.
 * Measure the pulse width on the logic analyzer.
 */

#include 

/* Debug pins — use unused GPIOs */
#define DBG_ISR_PIN     (1 << 8)   /* PA8: ISR timing */
#define DBG_TASK_PIN    (1 << 9)   /* PA9: Task timing */
#define DBG_FUNC_PIN    (1 << 10)  /* PA10: Function timing */

#define DBG_PORT_BSRR   (*(volatile uint32_t *)0x40010810)
#define DBG_PORT_BRR    (*(volatile uint32_t *)0x40010814)

/* Measure ISR execution time */
void TIM2_IRQHandler(void) {
    DBG_PORT_BSRR = DBG_ISR_PIN;   /* HIGH = ISR entered */

    /* ... ISR code ... */
    handle_timer_tick();

    DBG_PORT_BRR = DBG_ISR_PIN;    /* LOW = ISR exited */
    /* Pulse width on analyzer = ISR execution time */
}

/* Measure function execution time */
void process_sensor_data(uint16_t *raw, float *result, int count) {
    DBG_PORT_BSRR = DBG_FUNC_PIN;  /* HIGH */

    for (int i = 0; i < count; i++) {
        result[i] = (float)raw[i] * 3.3f / 4096.0f;
        /* Apply calibration, filtering, etc. */
    }

    DBG_PORT_BRR = DBG_FUNC_PIN;   /* LOW */
}

/* Measure task scheduling (RTOS) */
void sensor_task(void *params) {
    while (1) {
        DBG_PORT_BSRR = DBG_TASK_PIN;  /* HIGH = task running */

        read_sensors();
        process_data();

        DBG_PORT_BRR = DBG_TASK_PIN;   /* LOW = task sleeping */

        vTaskDelay(pdMS_TO_TICKS(100));
        /* Gap between pulses = sleep time
         * Pulse width = task CPU time
         * Period = task schedule interval */
    }
}

/* Measure interrupt latency */
/* Set up an external interrupt on a pin.
 * Apply a test signal (e.g., from a function generator).
 * In the ISR, toggle a debug pin immediately.
 * Measure the delay between the input edge and the debug toggle
 * on the logic analyzer — that's your interrupt latency. */

void EXTI0_IRQHandler(void) {
    DBG_PORT_BSRR = DBG_ISR_PIN;  /* Immediate toggle */
    /* Latency = time from EXTI0 edge to this toggle */

    /* ... handle interrupt ... */

    DBG_PORT_BRR = DBG_ISR_PIN;
}

/* Conditional debug output — only on interesting events */
static uint32_t error_count = 0;

void check_sensor(void) {
    int16_t value = read_adc(0);

    if (value  4095) {
        error_count++;
        DBG_PORT_BSRR = DBG_FUNC_PIN;  /* Pulse = error detected */
        __asm volatile ("nopnnopnnopnnop");
        DBG_PORT_BRR = DBG_FUNC_PIN;
    }
}

/* Compile-time removal of debug instrumentation */
#ifdef DEBUG_TIMING
    #define DBG_HIGH(pin)  (DBG_PORT_BSRR = (pin))
    #define DBG_LOW(pin)   (DBG_PORT_BRR  = (pin))
#else
    #define DBG_HIGH(pin)  ((void)0)
    #define DBG_LOW(pin)   ((void)0)
#endif

Software Setup: PulseView (Free)

PulseView is a free, open-source logic analyzer frontend from the sigrok project. It supports dozens of USB logic analyzers and includes 100+ protocol decoders.

/* PulseView Quick Start Guide
 *
 * 1. Download: https://sigrok.org/wiki/PulseView
 *    Available for Windows, macOS, Linux
 *
 * 2. Connect your logic analyzer (most USB analyzers auto-detected)
 *
 * 3. Configure channels:
 *    - Click channel labels to rename (e.g., "D0" → "SDA")
 *    - Disable unused channels (reduces file size)
 *    - Set sample rate (2MHz for I2C, 10MHz for SPI)
 *
 * 4. Add protocol decoders:
 *    - Menu → Add Decoder (or yellow/green decoder icon)
 *    - Select "I2C", "SPI", "UART", etc.
 *    - Map channels: SDA→CH0, SCL→CH1
 *    - Set protocol parameters (speed, mode, etc.)
 *
 * 5. Capture:
 *    - Set sample count or time (e.g., 1M samples or 500ms)
 *    - Click "Run" (or press Space)
 *    - Trigger your firmware (press button, send command)
 *
 * 6. Analyze:
 *    - Zoom in/out with scroll wheel
 *    - Click on decoded data to see details
 *    - Use cursors (right-click → "Place cursor") to measure timing
 *    - Two cursors = time difference displayed in toolbar
 *
 * Decoder stacking:
 *    Decoders can be stacked! For example:
 *    I2C decoder → feeds into → BMP280 decoder (if available)
 *    SPI decoder → feeds into → SPI Flash decoder
 *    This shows high-level operations, not just raw bytes.
 */

Advanced Trigger Modes

Basic edge triggering captures everything and lets you scroll through the data, but advanced triggers let you capture only the events you care about. This is essential when debugging intermittent issues — you might need to capture thousands of transactions before the bug occurs, and a targeted trigger catches exactly the problematic event without filling your buffer with normal traffic.

/* Trigger modes for capturing specific events
 *
 * Without triggers, you capture everything and search later.
 * With triggers, the analyzer starts recording only when
 * specific conditions are met — essential for catching
 * rare or intermittent bugs.
 *
 * ─── Edge Trigger ───
 * Capture when a signal changes state.
 * Example: Trigger on falling edge of CS to capture SPI transactions.
 * Use: Start of any communication, button press, interrupt.
 *
 * ─── Pattern Trigger ───
 * Capture when multiple channels match a pattern simultaneously.
 * Example: Trigger when CS=LOW AND MOSI_bit7=HIGH
 *          (catches specific SPI commands)
 * Use: Debugging specific protocol states.
 *
 * ─── Protocol Trigger (Advanced analyzers only) ───
 * Trigger on decoded protocol content.
 * Example: Trigger on I2C address 0x76 with NACK
 *          (catches failed BMP280 communication)
 * Use: Finding specific protocol errors in long captures.
 *
 * ─── Firmware-Assisted Trigger ───
 * The simplest approach: use a debug GPIO pin as trigger.
 * Toggle the pin right before the event of interest.
 * Set analyzer to trigger on that pin's edge.
 */

/* Firmware-assisted trigger example */
void debug_triggered_capture(void) {
    /* Normal operation... */
    for (int i = 0; i < 1000; i++) {
        int result = i2c_read_sensor();

        if (result < 0) {
            /* Error detected! Trigger the analyzer */
            DBG_PORT_BSRR = DBG_ISR_PIN;  /* Rising edge = trigger */

            /* Now perform the retry — analyzer captures this */
            result = i2c_read_sensor();

            DBG_PORT_BRR = DBG_ISR_PIN;
        }
    }
}

/* Set analyzer trigger: Rising edge on debug pin channel
 * Pre-trigger buffer: 10% (captures a bit before the trigger too)
 * Now you'll see exactly what the I2C bus looks like during the error */

Logic Analyzer vs Oscilloscope

Both tools are essential for embedded debugging, but they excel at different tasks:

  • Logic Analyzer: Many channels (8-32+), digital only, protocol decoding, long captures, timing measurements. Use for: protocol debugging, timing analysis, digital signal integrity.
  • Oscilloscope: Fewer channels (2-4), analog waveforms, voltage measurements, bandwidth/frequency analysis. Use for: signal quality, noise analysis, power supply issues, analog debugging.

If your I2C communication fails and the logic analyzer shows correct protocol but the slave still NACKs, switch to an oscilloscope. You might find that the signal levels are marginal, rise times are too slow (wrong pull-up values), or noise is causing false transitions. See our oscilloscope guide for details.

Related Articles

📖 Related: LM35 Temperature Sensor: Working, Circuit, and Arduino Code

Related on this site

Leave a Reply

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