Chapters

16 Protocol Diagnostics and Bus Design

sensing
protocols
diagnostics
i2c
spi

Back to Sensor Communication Protocols

Shows: CP-0150 pre-concept hook: Why does the sensor report zero after a restart?
  1. Physics Phoebe watches a room-sensor board restart and then report zero, with power, bus, timing, and code causes still open.

    Why does the sensor report zero after a restart?

CP-0150 pre-concept hook: Why does the sensor report zero after a restart?

16.1 Start With the Measurement Story

This section: Start With the Measurement Story — Picture a room sensor that sometimes reports zero after a board restart.

Turn Bus Silence Into a Named Failure

Picture a room sensor that sometimes reports zero after a board restart. Zero may be a real reading, a missing reply, the wrong address, or a timing error. The receiving code must keep those states separate.

GPIO means a general-purpose input/output pin used for a direct line. I2C is a shared two-wire link for addressed nearby parts. SPI is a clocked link with separate data paths and a select line. UART is a simple serial link that sends timed bits between two endpoints.

Record the sensor identity, address or select line, timing, raw bytes, unit, and error state. Disconnect a wire, change the rate, repeat a read, and restart the controller. Reject missing or malformed data instead of turning it into a plausible zero.

This runway does not diagnose every board fault. The deeper sections cover pull-ups, addresses, clock modes, framing, logic traces, shared-line design, and the tests that separate wiring from software.

A bus failure often looks like a bad sensor until the evidence is checked. The useful story follows pull-ups, addresses, clock speed, cable length, signal integrity, and logs until the fault is visible instead of guessed.

The mathematical gist. A pull-up and a resistive fault form a divider, so Rf=RpuVbus/(VDD−Vbus)R_f=R_{pu}V_{bus}/(V_{DD}-V_{bus}). On the chapter’s 3.3 V bus with 4.7 kΩ pull-up, a 1.65 V idle reading means Rf=4.7R_f=4.7 kΩ. The HIGH threshold is 2.31 V, which corresponds to about 11.0 kΩ: leakage below that can hold the bus invalid without being a dead short.

Math Bridge · guided foundationsWhat resistance is pulling this I2C bus down?Let Phoebe turn one multimeter voltage into a fault estimate and a logic-threshold test.

16.2 Common Protocol Pitfalls

This section: Common Protocol Pitfalls — First inspect the SPI mode mistake in this single panel.

First inspect the SPI mode mistake in this single panel. Compare the crossed-out switch and zero readings with the checked switch and known reading; a bus can appear active while its sampled bits are wrong.

  1. Physics Phoebe crosses out a default clock-mode switch beside a stream of false zero readings; in the same panel she checks the sensor timing sheet, changes the clock edges, and verifies a known reading.

    Wrong: The default clock mode is always safe. Match the data sheet and test known readings.

CP-0102 misconception buster: The Mistake: Using the default SPI mode (Mode 0) when the sensor datasheet specifies a different mode, resulting in consistently wrong readings, all-zeros, or all-ones from the sensor even though communication appears to work.

The left switch gives a stream of zeros. The middle timing sheet points to the clock-edge setting, and the right display shows a checked reading after that setting changes. The practical test is to compare the sensor data sheet with the SPI mode in code and confirm one known value. The next pitfall concerns I2C pull-ups, which are a separate electrical check.

16.2.1 Missing or Wrong I2C Pull-Up Resistors

The Mistake: Connecting I2C sensors directly to GPIO pins without external pull-up resistors, assuming the microcontroller’s internal pull-ups are sufficient, or using incorrect resistor values that cause unreliable communication.

Why It Happens: Many tutorials skip pull-up resistors because they work in short-range prototyping. Internal pull-ups on ESP32 are weak (45-65 kohm), only suitable for very short wires (<10 cm) at low speeds. Beginners also confuse I2C (open-drain, requires pull-ups) with SPI (push-pull, no pull-ups needed).

The Fix: Always use external pull-up resistors. Common starting values are 4.7 kohm for standard 100 kHz I2C and 2.2 kohm for fast mode 400 kHz. The optimal value depends on bus capacitance: R = t_rise / (0.8473 x C_bus), where t_rise is the maximum rise time from the I2C specification (1000 ns standard mode, 300 ns fast mode). Place resistors near the master (ESP32/Arduino), not at each sensor. See the Worked Example below for a detailed step-by-step pull-up calculation with a real multi-sensor design. Use the interactive calculator to quickly check values for your setup:

Check your understanding: Use the questions below to check the key idea

16.2.2 I2C Pull-Up Calculator

Calculate the optimal pull-up resistor value for your I2C bus based on capacitance and speed.

Diagnosing Pull-Up Issues: Use the endTransmission() return code to identify whether communication failures stem from missing pull-ups, wrong addresses, or bus faults:

Return CodeMeaningFirst Thing to Check
0SuccessDevice responded at that address
2Address NACKWrong address, missing pull-ups, or device not powered
3Data NACKRegister or command byte rejected
4Other bus errorSDA/SCL short, stuck line, or bus conflict

Symptom of missing pull-ups: I2C scanner finds no devices, or communication works intermittently. If you see error code 2 on all addresses, check your pull-up resistors first.

16.2.3 SPI Mode Mismatch Causing Corrupted Data

The Mistake: Using the default SPI mode (Mode 0) when the sensor datasheet specifies a different mode, resulting in consistently wrong readings, all-zeros, or all-ones from the sensor even though communication appears to work.

Why It Happens: SPI has four modes defined by CPOL (clock polarity) and CPHA (clock phase), and unlike I2C addresses which cause obvious failures, wrong SPI mode still clocks data - it just samples at the wrong edge. The MAX31855 thermocouple ADC requires Mode 0, while the ADXL345 accelerometer is typically used in Mode 3 (it also supports Mode 0). Many developers copy-paste SPI initialization code without checking the specific sensor’s timing requirements.

The Fix: Check the sensor datasheet for CPOL and CPHA, then configure SPI explicitly. Mode 0 means clock idle low and sample on the rising edge; Mode 3 means clock idle high and sample on the falling edge. Do not assume the library default is correct.

16.2.4 Optional SPI Configuration Pattern


// WRONG: Default mode may not match sensor
SPI.begin();
SPI.transfer(0x00);

// CORRECT: Explicit mode configuration
SPI.begin();
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));  // 1 MHz, MSB first, Mode 0

// For ADXL345 accelerometer (Mode 3):
SPI.beginTransaction(SPISettings(5000000, MSBFIRST, SPI_MODE3));  // 5 MHz, Mode 3

// Always end transaction when done
SPI.endTransaction();

Debug tip: If sensor returns 0x00 or 0xFF consistently, try all four SPI modes - one should produce valid register values.

16.2.5 Try It: SPI Mode Timing VisualizerTry: SPI Mode Timing Visualizer

Select an SPI mode and see how CPOL and CPHA affect the clock signal and data sampling edges. Understanding which clock edge is used for sampling helps you debug corrupted sensor readings.

16.2.6 Try It: I2C Address Conflict CheckerTry: I2C Address Conflict Checker

Enter the sensors in your design to check for I2C address conflicts. The tool identifies collisions and suggests resolution strategies.

16.3 Knowledge CheckCheck your understanding: 16.3 Knowledge Check

This section: Knowledge Check

Check your understanding: Use the questions below to check the key idea Check your understanding: Use the questions below to check the key idea

16.4 Multi-Sensor I2C Bus Design

This section: Multi-Sensor I2C Bus Design

Scenario: You’re building an indoor air quality monitor with 5 sensors on a single I2C bus connected to an ESP32:

  1. BME280 (temperature, humidity, pressure) - Address: 0x76
  2. BH1750 (light sensor) - Address: 0x23
  3. CCS811 (VOC-derived equivalent CO2, eCO2, and equivalent total VOC, eTVOC; not direct CO2) - Address: 0x5A
  4. SSD1306 (OLED display) - Address: 0x3C
  5. Second BME280 (outdoor sensor) - Address: 0x77 (SDO pin to VCC)

Before solving the five-device bus, inspect Figure 16.1 to see what one listed address hides. The breakout shares SDA and SCL but also exposes wake, reset, and interrupt signals that affect integration and recovery.

A red CCS811 air-quality sensor breakout board with the sensing package and labeled I2C pads visible
Figure 16.1: The CCS811 at address 0x5A is a physical gas-sensor breakout with shared SDA and SCL pads plus reset, interrupt, and wake connections. Seeing those extra signals is a useful reminder that assigning unique I2C addresses is necessary, but not sufficient, for a dependable five-device bus. Photo: SparkFun, CC BY 2.0

Read Figure 16.1, locate the sensing package first, then read the labelled I2C pads and the extra control pins around it. Unique addresses prevent one class of conflict, while pull-up strength, capacitance, power sequencing, and control-line handling complete the dependable bus design developed below.

Design Challenges to Solve:

1. Pull-Up Resistor Calculation

The I2C bus needs pull-up resistors on both SDA and SCL lines. Too weak (high resistance) causes slow rise times and communication errors. Too strong (low resistance) wastes power and may damage devices.

Formula: R_pullup = t_rise / (0.8473 × C_bus)

Where:

  • t_rise = maximum rise time (1000 ns for standard mode 100 kHz, 300 ns for fast mode 400 kHz)
  • C_bus = total bus capacitance (trace capacitance + device capacitance)

Step-by-step Calculation:

a) Estimate bus capacitance:

  • PCB trace: ~30 pF per 30cm wire (assume 60cm total) = 60 pF
  • BME280: 10 pF per device × 2 = 20 pF
  • BH1750: 10 pF
  • CCS811: 10 pF
  • SSD1306: 400 pF (worst-case with long flex cable; typical breakout boards add 100-150 pF)
  • Total C_bus: 60 + 20 + 10 + 10 + 400 = 500 pF

b) Choose I2C speed:

  • Standard mode (100 kHz): lower power, simpler
  • Fast mode (400 kHz): faster data, needed if polling sensors frequently
  • Choice: Fast mode 400 kHz (t_rise = 300 ns max)

c) Calculate maximum pull-up resistance for fast mode:

R_max = t_rise / (0.8473 × C_bus)
R_max = 300 ns / (0.8473 × 500 pF)
R_max = 300 × 10^-9 / (0.8473 × 500 × 10^-12)
R_max = 708 Ω (approximately 0.708 kΩ)

d) Calculate minimum pull-up resistance: Minimum limited by maximum sink current (3 mA per I2C spec) and V_OL_max (0.4V):

R_min = (V_CC - V_OL) / I_OL = (3.3V - 0.4V) / 3mA = 0.97 kΩ

e) Evaluate resistor selection for 400 kHz:

  • Maximum resistance from rise time: 708 Ω
  • Minimum resistance from current limit: 0.97 kΩ
  • Problem: No standard resistor value exists between 708 Ω and 0.97 kΩ that satisfies both constraints!
  • Actual rise time with 1.0 kΩ: 1kΩ × 500pF × 0.8473 = 424 ns (exceeds 300 ns limit for 400 kHz)

Conclusion: 400 kHz is not achievable with 500 pF bus capacitance!

Let’s recalculate for 100 kHz standard mode instead:

Revised Calculation for 100 kHz:

t_rise_max = 1000 ns (standard mode specification)
R_max = 1000 ns / (0.8473 × 500 pF) = 2.36 kΩ (maximum resistance allowed)
R_min = (3.3V - 0.4V) / 3mA = 0.97 kΩ (minimum resistance from current limit)

Acceptable range: 0.97 kΩ to 2.36 kΩ

Test with 4.7 kΩ resistors (common value):

t_rise = 4.7kΩ × 500pF × 0.8473 = 1991 ns
Result: FAILS (exceeds 1000 ns maximum for 100 kHz)

Final Decision: A 2.2 kΩ pull-up meets the calculated rise-time and LOW-current bounds at 100 kHz, but the assumed 500 pF exceeds the 400 pF Standard-mode bus-load limit (NXP UM10204, Table 11). Reduce or segment the capacitance, or qualify an extended-load design under the I²C specification, before calling this bus reliable:

t_rise = 2.2kΩ × 500pF × 0.8473 = 932 ns ✓ (safely under 1000 ns limit)
Current draw = 3.3V / 2.2kΩ = 1.5 mA ✓ (within ESP32 limits)

2. Address Conflict Resolution

One BME280 at default address 0x76, second at alternate 0x77:

#include <Adafruit_BME280.h>

Adafruit_BME280 bme_indoor;  // 0x76
Adafruit_BME280 bme_outdoor; // 0x77

void setup() {
    Wire.begin(21, 22); // SDA=21, SCL=22 on ESP32

    // Initialize with explicit addresses
    if (!bme_indoor.begin(0x76)) {
        Serial.println("Could not find indoor BME280");
    }
    if (!bme_outdoor.begin(0x77)) {
        Serial.println("Could not find outdoor BME280");
    }
}

3. Bus Scanning for Debugging

Before writing sensor-specific code, scan the bus to verify all devices are present:

void scanI2CBus() {
    Serial.println("\n--- I2C Bus Scan ---");
    byte count = 0;

    for (byte addr = 1; addr < 127; addr++) {
        Wire.beginTransmission(addr);
        byte error = Wire.endTransmission();

        if (error == 0) {
            Serial.print("Device found at 0x");
            if (addr < 16) Serial.print("0");
            Serial.print(addr, HEX);

            // Identify common sensors
            switch(addr) {
                case 0x23: Serial.print(" (BH1750 Light)"); break;
                case 0x3C: Serial.print(" (SSD1306 OLED)"); break;
                case 0x5A: Serial.print(" (CCS811 Air Quality)"); break;
                case 0x76: Serial.print(" (BME280 #1)"); break;
                case 0x77: Serial.print(" (BME280 #2)"); break;
                default: Serial.print(" (Unknown)"); break;
            }
            Serial.println();
            count++;
        }
    }

    Serial.print("Total devices found: ");
    Serial.println(count);

    if (count != 5) {
        Serial.println("WARNING: Expected 5 devices!");
    }
}

Expected Output:

--- I2C Bus Scan ---
Device found at 0x23 (BH1750 Light)
Device found at 0x3C (SSD1306 OLED)
Device found at 0x5A (CCS811 Air Quality)
Device found at 0x76 (BME280 #1)
Device found at 0x77 (BME280 #2)
Total devices found: 5

4. Power Sequencing for CCS811

The CCS811 air quality sensor has specific power-up requirements:

void initCCS811() {
    pinMode(CCS811_WAKE_PIN, OUTPUT);
    digitalWrite(CCS811_WAKE_PIN, LOW); // Wake sensor

    delay(50); // Wait for sensor wake-up

    if (!ccs811.begin(0x5A)) {
        Serial.println("CCS811 not found!");
        return;
    }

    // available() waits for one data-ready sample, not burn-in. On first use allow 60 min in modes 1–3; after selecting a mode allow 20 min conditioning before treating eCO2/eTVOC as accurate.
    while (!ccs811.available()) {
        delay(500);
    }

    ccs811.setDriveMode(CCS811_DRIVE_MODE_1SEC); // Read every 1 second
}

5. Complete Reading Sequence

Optimized order to minimize blocking:

void readAllSensors() {
    // Read fast sensors while CCS811 processes
    float lux = lightMeter.readLightLevel();

    // Read BME280s (I2C read ~5ms each)
    bme_indoor.takeForcedMeasurement();
    float temp_in = bme_indoor.readTemperature();
    float hum_in = bme_indoor.readHumidity();
    float press_in = bme_indoor.readPressure() / 100.0F;

    bme_outdoor.takeForcedMeasurement();
    float temp_out = bme_outdoor.readTemperature();
    float hum_out = bme_outdoor.readHumidity();

    // Set current environmental data before checking for a fresh CCS811 sample
    ccs811.setEnvironmentalData(hum_in, temp_in);

    // Persist these start times and update modeSelectedMs whenever the drive mode changes.
    extern uint32_t firstPowerOnMs;
    extern uint32_t modeSelectedMs;
    const bool ccs811ConditioningComplete =
        static_cast<uint32_t>(millis() - firstPowerOnMs) >= 60UL * 60UL * 1000UL &&
        static_cast<uint32_t>(millis() - modeSelectedMs) >= 20UL * 60UL * 1000UL;
    bool ccs811ReadingsValid = false;
    if (ccs811.available() && ccs811.readData() == 0) {
        eCO2 = ccs811.geteCO2();
        eTVOC = ccs811.getTVOC();
        ccs811ReadingsValid = ccs811ConditioningComplete;
    }

    // Withhold estimates during conditioning; the display should show a conditioning label when invalid.
    updateDisplay(temp_in, hum_in, ccs811ReadingsValid ? eCO2 : NAN, ccs811ReadingsValid ? eTVOC : NAN, lux);
}

Bill of Materials (BOM):

ComponentQuantityCost (unit)Purpose
ESP32 DevKit1$8Microcontroller
BME2802$4 eachTemp/humidity/pressure
BH17501$2Light sensor
CCS8111$12VOC-derived eCO₂/eTVOC air-quality estimates
SSD1306 OLED1$5Display
2.2kΩ resistors2$0.01 eachI2C pull-ups
Total$35.02Complete system

Key Lessons:

  1. Bus capacitance matters - high-capacitance devices (OLED) require stronger pull-ups
  2. Scan before coding - verify all addresses are correct before writing application code
  3. Read datasheets carefully - CCS811 has specific initialization and environmental compensation requirements
  4. Optimize read order - start slow sensors first, read fast ones during wait times
  5. Pull-up calculation is critical - incorrect values cause intermittent failures that are hard to debug

Practise

Hands-on lab · 30 min · Wokwi ESP32

Read an MPU6050 over I2C

I have explained bus transactions; now let's read three acceleration registers and check their units.

Open the lab
Practise

Hands-on lab · 20 min · IoT Ideator

Light-sensor interface repair

I have explained interface diagnostics; now let's repair the sensor cable and check the relay's voltage boundary.

Open the lab
Practise

Hands-on lab · 70 min · Wokwi ESP32

Confirm sustained PIR motion

I have explained bus diagnostics; now let's connect sensors and test their addresses, touch response, and control path.

Open the lab

16.5 Order the Steps

This section: Order the Steps

16.6 Match the Concepts

This section: Match the Concepts

16.7 Label the Diagram

This section: Label the Diagram

16.8 Code Challenge

This section: Code Challenge

16.9 Summary

This section: Summary

This chapter covered the three essential communication protocols for sensor interfacing:

  • I2C Protocol: Two-wire synchronous bus with 7-bit addressing, ideal for connecting multiple slow sensors with minimal GPIO usage.
  • SPI Protocol: Four-wire synchronous full-duplex interface for high-speed data transfer (SD cards, displays, high-resolution ADCs).
  • UART Protocol: Two-wire asynchronous point-to-point link for simple serial devices (GPS, Bluetooth modules, debug output).
  • Protocol Selection: Choose I2C for multi-sensor setups with limited pins; SPI when speed is critical; UART for simple serial peripherals.
  • Common Pitfalls: Pull-up resistors for I2C, SPI mode configuration, address conflict resolution.

16.9.1 Key Takeaway

I2C, SPI, and UART are the three core sensor communication protocols in IoT. Use I2C (two wires, 7-bit addressing) when you need to connect many low-speed sensors with minimal wiring. Use SPI (four wires, full-duplex) when high-speed data transfer is essential. Use UART (two wires, asynchronous) for simple point-to-point serial devices. Always add proper pull-up resistors for I2C and verify SPI mode settings from the sensor datasheet to avoid corrupted data.

16.9.2 For Kids: Meet the Sensor Squad!

Temperature Terry wanted to tell the microcontroller about the temperature, but they did not speak the same language! Luckily, their friend the LED knew a translator called “I2C” — it only needed two wires, like a tin-can telephone with two strings. “I will talk on the data string, and you keep time on the clock string!” Sammy said.

But when the battery needed to send a LOT of data really fast (like a whole photo album), she used a different translator called “SPI” — it was like having four walkie-talkies at once! One to send, one to receive, one to keep time, and one to pick who is talking.

“Why not always use SPI?” Max asked. “Because SPI needs more wires,” Lila explained. “If you have 10 sensor friends, I2C lets them ALL share just two wires — each friend just has a different nickname (address). SPI would need a separate wire for each friend!” Now the whole Sensor Squad could chat, whether they needed speed or simplicity!

16.10 What’s Next

This section: What’s Next

ChapterFocus
Sensor Data ProcessingFiltering techniques, calibration procedures, and data validation pipelines
Sensor Fundamentals and TypesCore sensor types, characteristics, and selection criteria
Sensor Circuits and SignalsAnalog circuit design, signal conditioning, and ADC interfacing
Multi-Sensor Data FusionCombining readings from multiple sensors for improved accuracy
IoT Networking CoreNetwork protocols that carry sensor data to the cloud

Find help

3 reviewed answers.

Where should I start in a module?

Open the module page from the sidebar to see its chapters in order. Use the module map to return to a topic you need.

Modules and paths

Reviewed

What can I try when a project does not work?

Check the symptom, then work through the diagnostic steps. Keep the error message and the smallest example that reproduces it.

When you are stuck

Reviewed

How do interactive examples work?

Use the interactive guide to check how examples and simulations work before changing your project.

Interactive examples guide

Reviewed