16 Protocol Diagnostics and Bus Design
More
Depth: Practitioner
Progress saves as you read. Mark sections complete as you go; your progress appears in the sidebar.
Back to Sensor Communication Protocols
-
Why does the sensor report zero after a restart?
Physics Phoebe watches a room-sensor board restart and then report zero, with power, bus, timing, and code causes still open.
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 . On the chapter’s 3.3 V bus with 4.7 kΩ pull-up, a 1.65 V idle reading means 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.
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.
-
Wrong: The default clock mode is always safe. Match the data sheet and test known readings.
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.
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:
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 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.
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
16.2.6 Try It: I2C Address Conflict CheckerTry: I2C Address Conflict Checker
16.3 Knowledge CheckCheck your understanding: 16.3 Knowledge Check
This section: Knowledge Check
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:
- BME280 (temperature, humidity, pressure) - Address: 0x76
- BH1750 (light sensor) - Address: 0x23
- CCS811 (VOC-derived equivalent CO2, eCO2, and equivalent total VOC, eTVOC; not direct CO2) - Address: 0x5A
- SSD1306 (OLED display) - Address: 0x3C
- 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.
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
A red CCS811 air-quality sensor breakout board with the sensing package and labeled I2C pads visible
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):
| 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 | VOC-derived eCO₂/eTVOC air-quality estimates |
| SSD1306 OLED | 1 | $5 | Display |
| 2.2kΩ resistors | 2 | $0.01 each | I2C pull-ups |
| Total | $35.02 | Complete system |
Key Lessons:
- Bus capacitance matters - high-capacitance devices (OLED) require stronger pull-ups
- Scan before coding - verify all addresses are correct before writing application code
- Read datasheets carefully - CCS811 has specific initialization and environmental compensation requirements
- Optimize read order - start slow sensors first, read fast ones during wait times
- Pull-up calculation is critical - incorrect values cause intermittent failures that are hard to debug
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
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 pathsReviewed
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 stuckReviewed
How do interactive examples work?
Use the interactive guide to check how examples and simulations work before changing your project.
Interactive examples guideReviewed
