|
Hardware I2C Debugging Tips: Logic Analyzer Packet Capture in Practice

Hardware I2C Debugging Tips: Logic Analyzer Packet Capture in Practice

Friends doing embedded development all know that I2C protocol sounds simple - just two wires (SDA and SCL) - but when you actually use it, it can drive you crazy questioning your life choices. Device not responding, data reading incorrectly, timing issues… Today I’ll demonstrate with a logic analyzer how to catch these strange I2C problems one by one.

What Do You Need to Prepare?

ItemModel/SpecificationPrice
Logic AnalyzerSaleae Logic 8 / Domestic Clone¥80-300
I2C DeviceMPU6050 Accelerometer¥15
Development BoardCH559 Development Board¥25
Jumper WiresMale-to-Female 10cm¥5
BreadboardMini Type¥8
Total¥133-253

You don’t necessarily need to buy the original Saleae; domestic clones cost just tens of yuan and are completely sufficient for daily debugging. The one I have is 8-channel, more than enough for I2C.

Step 1: Set Up Test Environment

Let’s connect the hardware first. I2C wiring is very simple:

CH559          MPU6050
─────          ───────
3.3V    →      VCC
GND     →      GND
P1.4(SCL) →    SCL
P1.5(SDA) →    SDA

Notes: ⚠️

  • I2C needs pull-up resistors! Most modules already have built-in 4.7kΩ pull-ups, but if not, remember to connect a 4.7kΩ resistor from both SDA and SCL to VCC

  • Voltage must match! CH559 is 3.3V logic, MPU6050 is also 3.3V; if connecting 5V devices you need level shifting

  • The logic analyzer’s ground must share ground with the circuit under test, otherwise you won’t capture the signal

Next, install the logic analyzer software. Domestic clones typically use Sigrok/PulseView:

# Ubuntu/Debian
sudo apt-get install pulseview

# Fedora
sudo dnf install pulseview

# Or download Saleae official software (for original version users)
# https://www.saleae.com/downloads

Step 2: Write I2C Test Code

We’ll write a simple I2C read/write program using CH559. CH559’s hardware I2C is on pins P1.4 and P1.5:

#include <CH559.H>
#include <stdint.h>

// I2C pin definitions
sbit I2C_SDA = P1^5;
sbit I2C_SCL = P1^4;

// MPU6050 address (7-bit address, 0x68)
#define MPU6050_ADDR 0x68
#define WHO_AM_I_REG 0x75

// I2C delay (adjust according to actual clock)
void I2C_Delay() {
    _nop_();
    _nop_();
}

// I2C start signal
void I2C_Start() {
    I2C_SDA = 1;
    I2C_SCL = 1;
    I2C_Delay();
    I2C_SDA = 0;
    I2C_Delay();
    I2C_SCL = 0;
}

// I2C stop signal
void I2C_Stop() {
    I2C_SDA = 0;
    I2C_SCL = 1;
    I2C_Delay();
    I2C_SDA = 1;
    I2C_Delay();
}

// I2C send one byte
uint8_t I2C_SendByte(uint8_t dat) {
    uint8_t i, ack;
    for (i = 0; i < 8; i++) {
        I2C_SDA = (dat >> 7) & 0x01;
        dat <<= 1;
        I2C_SCL = 1;
        I2C_Delay();
        I2C_SCL = 0;
        I2C_Delay();
    }
    // Read acknowledge
    I2C_SDA = 1;
    I2C_SCL = 1;
    I2C_Delay();
    ack = I2C_SDA;
    I2C_SCL = 0;
    I2C_Delay();
    return ack;
}

// I2C receive one byte
uint8_t I2C_RecvByte(uint8_t ack) {
    uint8_t i, dat = 0;
    I2C_SDA = 1;
    for (i = 0; i < 8; i++) {
        dat <<= 1;
        I2C_SCL = 1;
        I2C_Delay();
        dat |= I2C_SDA;
        I2C_SCL = 0;
        I2C_Delay();
    }
    // Send acknowledge
    I2C_SDA = ack ? 1 : 0;
    I2C_SCL = 1;
    I2C_Delay();
    I2C_SCL = 0;
    I2C_Delay();
    I2C_SDA = 1;
    return dat;
}

// I2C write register
void I2C_WriteReg(uint8_t reg, uint8_t dat) {
    I2C_Start();
    I2C_SendByte(MPU6050_ADDR << 1);  // Write address
    I2C_SendByte(reg);
    I2C_SendByte(dat);
    I2C_Stop();
}

// I2C read register
uint8_t I2C_ReadReg(uint8_t reg) {
    uint8_t dat;
    I2C_Start();
    I2C_SendByte(MPU6050_ADDR << 1);  // Write address
    I2C_SendByte(reg);
    I2C_Start();                       // Repeated start
    I2C_SendByte((MPU6050_ADDR << 1) | 0x01);  // Read address
    dat = I2C_RecvByte(1);  // NACK
    I2C_Stop();
    return dat;
}

void main() {
    uint8_t who_am_i;

    // Configure clock (12MHz)
    CCLK_CFG = 0x00;

    // Configure GPIO (open-drain output)
    P1_MOD_OC |= 0x30;  // P1.4/P1.5 open-drain
    P1_DIR_PU |= 0x30;  // P1.4/P1.5 input

    // Initialize serial (for debug output)
    mInitSTDIO();

    printf("I2C Test Starting...\n");

    while (1) {
        // Read WHO_AM_I register (should be 0x68)
        who_am_i = I2C_ReadReg(WHO_AM_I_REG);
        printf("WHO_AM_I: 0x%02X\n", who_am_i);

        if (who_am_i == 0x68) {
            printf("MPU6050 detected!\n");
        } else {
            printf("Device not found!\n");
        }

        DelayMs(1000);
    }
}

Compile and flash:

# Install SDCC
sudo apt-get install sdcc

# Compile
sdcc --code-loc 0x0000 --xram-loc 0x0000 main.c

# Flash (using wchisp)
wchisp flash main.bin

Step 3: Logic Analyzer Packet Capture

Hardware and code are ready, now for the main event - packet capture analysis!

  1. Connect logic analyzer channels to SCL and SDA, ensure common ground

  2. Add I2C protocol decoder in PulseView, configure sampling rate to at least 10 times the SCL frequency

  3. Set trigger conditions to capture complete I2C transactions (Start → Address → Data → Stop)

  4. Compare decoding results with expected data, analyze anomaly locations (NACK, incorrect data, etc.)

SCL: ──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──
       └──┘  └──┘  └──┘  └──┘  └──┘  └──┘  └──┘

SDA: ──┐      ┌────────────────────────────┐  ┌──
   START  └──┘  Address + R/W bit + ACK + Data  └──┘ STOP

PulseView has an I2C protocol decoder; after loading it will automatically parse out:

  • Start/stop conditions

  • Device address (0x68)

  • Read/write direction

  • Register address

  • Data content

  • ACK/NACK status

Step 4: Analyze Waveforms to Find Problems

Normal Waveform Characteristics

Normal I2C communication should satisfy:

  • SCL high and low level times are basically symmetrical (50% duty cycle)

  • SDA only changes when SCL is low (except for start/stop conditions)

  • After each byte there is an ACK (SDA pulled low)

  • Start condition: SDA falling edge when SCL is high

  • Stop condition: SDA rising edge when SCL is high

Common Problem Waveforms

Problem 1: No ACK

Waveform manifestation: On the 9th clock cycle SDA stays high (not pulled low)

Possible causes:

  • Device address incorrect (MPU6050 is 0x68, not 0x69)

  • Device not powered or damaged

  • Pull-up resistors missing or value too large

  • Wiring loose

Problem 2: Data Read Back All 0xFF or 0x00

Waveform manifestation: SDA always high or always low

Possible causes:

  • Pull-up resistor too large/too small

  • Bus capacitance too large (wires too long)

  • Device in error state, needs reset

Problem 3: Timing Chaotic

Waveform manifestation: SDA changes when SCL is high

Possible causes:

  • Code delay incorrect

  • Interrupts interfered with I2C timing

  • Used wrong GPIO mode (should use open-drain)

Common Problem Troubleshooting

Problem 1: Device Address Wrong, Always NACK

  • Cause: I2C addresses have both 7-bit and 8-bit representations. MPU6050’s 7-bit address is 0x68, but some libraries use 8-bit address (shifted left one bit + R/W bit)

  • Solution: Confirm your code uses 7-bit address. When writing shift left one bit (0xD0), when reading shift left one bit +1 (0xD1)

Problem 2: Waveform Has Many Glitches

  • Cause: Poor grounding, wires too long, interference sources nearby

  • Solution: Shorten jumper wires, ensure common ground, stay away from interference sources like motors/relays

Problem 3: Logic Analyzer Can’t Capture Signal

  • Cause: Probe not connected well, trigger level set incorrectly, sampling rate too low

  • Solution: Check GND connection, adjust threshold voltage (set to around 1.5V for 3.3V logic), increase sampling rate

Problem 4: Sometimes Can Read Data, Sometimes Can’t

  • Cause: Timing margin insufficient, power supply unstable

  • Solution: Increase I2C delay, add decoupling capacitors, check power supply quality

Summary

The core of I2C debugging is “seeing” the signal. Without a logic analyzer, we can only guess: “Is the address wrong?” “Is there a timing problem?” With a logic analyzer, everything is right before your eyes, problems clear at a glance.

Several lessons learned:

  1. I2C must have pull-up resistors (4.7kΩ-10kΩ), otherwise signal edges are slow, communication unstable

  2. Pay attention to the difference between 7-bit and 8-bit address representations, avoid writing incorrectly causing NACK

  3. The shorter the jumper wires the better; long wires introduce parasitic capacitance, affecting signal quality

  4. Using a logic analyzer to capture packets is the most direct method for locating I2C problems; waveforms don’t lie

Finally a reminder, you don’t necessarily need to buy an expensive logic analyzer. Domestic clones costing just tens of yuan on Taobao are completely sufficient for daily debugging of I2C/SPI/UART. Let it heat up, just don’t let it glow - stable operation is more important than anything.

Hope this blog post is helpful to you!


Related Resources: