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’s Field Notes: Turning a Stuck-Bus Voltage Into a Fault Resistance
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.
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)
--- 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-upif(!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 processesfloat 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 nowif(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:
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
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!