14  Protocol Diagnostics and Bus Design

I2C Pull-Ups, SPI Timing, Address Conflicts, and Multi-Sensor Bus Proof

sensing
protocols
diagnostics
i2c
spi
Author

IoT Textbook

Published

July 21, 2026

Keywords

I2C pull-ups, SPI mode, address conflicts, sensor bus design, protocol diagnostics

Back to Sensor Communication Protocols

14.1 Start With the Measurement Story

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.

Phoebe the physics guide

Phoebe’s Why

This chapter’s own return-code table lists “SDA/SCL short, stuck line, or bus conflict” as one failure class – but a real fault is rarely a clean dead short. Moisture on a header, a cracked trace still making partial contact, or a marginal ESD structure often behaves as an unwanted resistor from the bus line to ground, sitting in parallel with whatever the line is already doing. Once that resistor exists, the pull-up and the fault resistance form an ordinary DC voltage divider, exactly like the one this chapter already reasons about for sink current. Reading the resulting bus voltage on a multimeter is not just “high” or “low” – it is a number you can run backward through Ohm’s Law to estimate how resistive the fault actually is, before you start desoldering anything.

The Derivation

Pull-up \(R_{pu}\) to \(V_{DD}\), unwanted fault path \(R_f\) to ground, both feeding the same node – an ordinary loaded divider:

\[V_{bus} = V_{DD}\,\frac{R_f}{R_{pu}+R_f}\]

Solved the diagnostic way, backward from a measured \(V_{bus}\):

\[R_f = R_{pu}\,\frac{V_{bus}}{V_{DD}-V_{bus}}\]

The I2C specification’s own logic thresholds turn that voltage into a pass/fail line. A bus can only be read as HIGH above \(V_{IH}=0.7\,V_{DD}\), so the fault resistance that just barely prevents the bus from ever reaching HIGH is the same formula evaluated at \(V_{bus}=V_{IH}\).

Worked Numbers: This Chapter’s Own 4.7 kΩ Bus

  • Threshold fault resistance, using this chapter’s own “4.7 kohm for standard 100 kHz I2C” pull-up at \(V_{DD}=3.3\) V: \(V_{IH}=0.7\times3.3=2.31\) V, so \(R_f = 4700\times2.31/(3.3-2.31) = 10{,}967\ \Omega \approx 11.0\ \text{k}\Omega\). Any leakage path below about 11.0 kΩ to ground – far higher than a dead short – is enough to keep the bus from ever reaching a valid HIGH, which is exactly the “address NACK on every address” or “other bus error” symptom this chapter’s own return-code table names, with no short required.
  • A clean diagnostic reading: if a suspect bus measures 1.65 V at rest (half of the 3.3 V rail), \(R_f = 4700\times1.65/(3.3-1.65) = 4700\ \Omega\) – the fault resistance happens to equal the pull-up itself. As a memorable field rule with this chapter’s own 4.7 kΩ pull-up: a bus sitting at roughly half the rail voltage points to a fault resistance in the same ballpark as the pull-up value, not a hard short (which would read near 0 V) and not a healthy idle bus (which reads near \(V_{DD}\)).
  • Contrast with the healthy case: with no fault, \(R_f\to\infty\) and \(V_{bus}\to V_{DD}=3.3\) V – the open-circuit idle-high state this chapter assumes everywhere else. The voltage-divider view only matters once an unintended resistive path appears; it says nothing new about a bus that is simply missing its pull-up, which this chapter already covers with the rise-time calculation instead.

14.2 Common Protocol Pitfalls

14.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:

14.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 Code Meaning First Thing to Check
0 Success Device responded at that address
2 Address NACK Wrong address, missing pull-ups, or device not powered
3 Data NACK Register or command byte rejected
4 Other bus error SDA/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.

14.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.

14.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.

14.2.5 Try It: 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.

14.2.6 Try It: I2C Address Conflict Checker

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

14.3 Knowledge Check

14.4 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 (CO2 and VOC) - Address: 0x5A
  4. SSD1306 (OLED display) - Address: 0x3C
  5. Second BME280 (outdoor sensor) - Address: 0x77 (SDO pin to VCC)

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: Use 2.2 kΩ pull-ups for reliable 100 kHz operation:

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;
    }

    // Wait for sensor to be ready (mandatory 20 minutes burn-in on first use)
    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() {
    // Start CCS811 measurement (takes ~50ms)
    ccs811.readData();

    // 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();

    // CCS811 should be ready now
    if (ccs811.available()) {
        co2 = ccs811.geteCO2();
        tvoc = ccs811.getTVOC();

        // Set environmental data for compensation
        ccs811.setEnvironmentalData(hum_in, temp_in);
    }

    // Update display (non-blocking update)
    updateDisplay(temp_in, hum_in, co2, tvoc, lux);
}

Bill of Materials (BOM):

Component Quantity Cost (unit) Purpose
ESP32 DevKit 1 $8 Microcontroller
BME280 2 $4 each Temp/humidity/pressure
BH1750 1 $2 Light sensor
CCS811 1 $12 Air quality (CO2/VOC)
SSD1306 OLED 1 $5 Display
2.2kΩ resistors 2 $0.01 each I2C pull-ups
Total $35.02 Complete 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

14.5 Order the Steps

14.6 Match the Concepts

14.7 Label the Diagram

14.8 Code Challenge

14.9 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

14.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.

14.9.2 For Kids: Meet the Sensor Squad!

Sammy the Sensor wanted to tell Max the Microcontroller about the temperature, but they did not speak the same language! Luckily, their friend Lila 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 Bella 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!

14.10 What’s Next

Chapter Focus
Sensor Data Processing Filtering techniques, calibration procedures, and data validation pipelines
Sensor Fundamentals and Types Core sensor types, characteristics, and selection criteria
Sensor Circuits and Signals Analog circuit design, signal conditioning, and ADC interfacing
Multi-Sensor Data Fusion Combining readings from multiple sensors for improved accuracy
IoT Networking Core Network protocols that carry sensor data to the cloud