KevsRobots Learning Platform
91% Percent Complete
By Kevin McAleer, 10 Minutes
Hardware communication problems are frustrating but solvable. This lesson teaches you a systematic approach to diagnosing and fixing common I2C and SPI issues. With these techniques, youβll spend less time stuck and more time building.
Before diving into specific issues, adopt this approach:
Golden Rule: When stuck, go back to basics. Unplug everything and rebuild step-by-step.
Symptom:
devices = i2c.scan()
print(devices) # Prints: []
Possible causes:
# Check if your module has pull-up resistors
# Look for small resistors marked "103" (10kΞ©) or "473" (47kΞ©)
# If missing, add 4.7kΞ© resistors:
# - SDA to 3.3V
# - SCL to 3.3V
# Code says GP8/GP9 but wired to GP4/GP5
# Fix: Match code to actual wiring
i2c = I2C(0, scl=Pin(9), sda=Pin(8)) # Check physical connections!
# Verify with multimeter:
# - VCC should read 3.3V
# - GND should read 0V
# If not, check power connections
# Some devices need initialization before appearing on bus
# Try power cycling:
# 1. Disconnect VCC for 5 seconds
# 2. Reconnect
# 3. Scan again
Diagnostic script:
from machine import I2C, Pin
import time
def diagnose_i2c():
"""Comprehensive I2C diagnostics"""
print("I2C Diagnostics Starting...\n")
# Test 1: Initialize I2C
print("Test 1: Initializing I2C...")
try:
i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=100000) # Start slow
print("β I2C initialized at 100 kHz")
except Exception as e:
print(f"β Failed to initialize I2C: {e}")
return
# Test 2: Scan for devices
print("\nTest 2: Scanning for devices...")
devices = i2c.scan()
if devices:
print(f"β Found {len(devices)} device(s):")
for addr in devices:
print(f" - 0x{addr:02X} (decimal {addr})")
else:
print("β No devices found")
print("\nTroubleshooting steps:")
print("1. Check SDA and SCL connections")
print("2. Verify VCC and GND connected")
print("3. Confirm pull-up resistors present")
print("4. Try power cycling the device")
# Test 3: Try different speeds
print("\nTest 3: Testing different speeds...")
for speed in [100000, 400000]:
i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=speed)
devices = i2c.scan()
status = "β" if devices else "β"
print(f"{status} {speed/1000:.0f} kHz: {len(devices)} device(s)")
diagnose_i2c()
Symptom:
data = i2c.readfrom(0x76, 1)
# OSError: [Errno 5] EIO
Meaning: Input/Output Error - communication failed
Causes:
# Verify address with scan
devices = i2c.scan()
print(f"Available addresses: {[hex(d) for d in devices]}")
# Use correct address
if 0x76 in devices:
data = i2c.readfrom(0x76, 1)
elif 0x77 in devices:
data = i2c.readfrom(0x77, 1)
# Try slower speed
i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=100000) # 100 kHz
# Check all wires:
# - Push firmly into breadboard
# - Wiggle gently - should not come loose
# - Try different breadboard rows
Symptom: Device responds but data is incorrect
Causes:
# Double-check datasheet for register address
# BME280 chip ID is at 0xD0, not 0x00
chip_id = i2c.readfrom_mem(0x76, 0xD0, 1) # Correct
# Not: i2c.readfrom_mem(0x76, 0x00, 1)
# Some devices need time to complete measurement
i2c.writeto_mem(addr, REG_CTRL, b'\x01') # Start measurement
time.sleep_ms(10) # Wait for completion
data = i2c.readfrom_mem(addr, REG_DATA, 2) # Now read
# Some devices send MSB first, some LSB first
data = i2c.readfrom_mem(addr, reg, 2)
# MSB first (big-endian)
value = (data[0] << 8) | data[1]
# LSB first (little-endian)
value = (data[1] << 8) | data[0]
Symptom: Works with one device but fails with multiple
Causes:
# Check for duplicate addresses
devices = i2c.scan()
if len(devices) != len(set(devices)):
print("β οΈ Duplicate addresses detected!")
# Solution: Change device address if possible
# (e.g., BME280 SDO pin, OLED solder jumper)
# Symptom: Works with short wires, fails with long
# Solution: Use shorter wires (<20cm for multiple devices)
# Or: Lower I2C speed
i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=100000)
# More devices = more load on bus
# May need stronger pull-ups (2.2kΞ© instead of 10kΞ©)
# Or: Ensure only one set of pull-ups total
Symptom: Device doesnβt respond to commands
Causes:
# Try all 4 modes systematically
for mode in range(4):
polarity = mode >> 1
phase = mode & 1
spi = SPI(0, baudrate=1000000, polarity=polarity, phase=phase,
sck=Pin(2), mosi=Pin(3), miso=Pin(4))
cs.value(0)
response = spi.read(1)
cs.value(1)
print(f"Mode {mode} (CPOL={polarity}, CPHA={phase}): {response}")
# Check CS logic level
# Most devices: LOW = selected, HIGH = deselected
cs = Pin(5, Pin.OUT)
cs.value(1) # Start HIGH (deselected)
# Some devices: HIGH = selected (rare)
# Try inverted logic if standard doesn't work
# Common mistake!
# MOSI (Master Out) β Device SDI/MOSI/DIN
# MISO (Master In) β Device SDO/MISO/DOUT
# If swapped, reading returns zeros or 0xFF
# Solution: Swap in code or physically
spi = SPI(0, sck=Pin(2), mosi=Pin(4), miso=Pin(3)) # Swapped
Symptom: Some data correct, some garbage
Causes:
# Start slow and increase gradually
spi = SPI(0, baudrate=100000, ...) # 100 kHz
# Test, then increase:
spi = SPI(0, baudrate=1000000, ...) # 1 MHz
spi = SPI(0, baudrate=10000000, ...) # 10 MHz
# SPI is very sensitive to wire length
# Keep under 10cm for reliable operation
# Longer wires = signal reflections and noise
# SPI needs solid ground reference
# Always connect:
# - Device GND to Pico GND
# - Use same GND point for all SPI devices
Symptom: Data format incorrect
Cause: Not sending dummy bytes for reading
# WRONG: Only send read command
tx_data = bytearray([0x80]) # Read register 0x00
rx_data = bytearray(1)
spi.write_readinto(tx_data, rx_data)
# Result: rx_data contains command echo, not data!
# CORRECT: Send dummy bytes
tx_data = bytearray([0x80, 0x00]) # Command + dummy byte
rx_data = bytearray(2)
spi.write_readinto(tx_data, rx_data)
data = rx_data[1] # Second byte has actual data
Symptom: Prototype works but final version doesnβt
Causes:
# PCB traces too long or incorrect routing
# Solution: Keep SPI traces short (<5cm)
# Route SCLK away from other signals (crosstalk)
# SPI needs good ground return path
# Solution: Use ground plane on PCB
# Or: Add ground traces alongside SPI signals
If you have access to a logic analyzer:
# Capture actual SPI/I2C signals
# See exactly what's being sent/received
# Identify:
# - Timing issues
# - Missing ACK (I2C)
# - Incorrect byte order
# - Wrong clock polarity/phase
# Low-cost option: Use another Pico as logic analyzer
# Software: Sigrok/PulseView (free)
# Voltage checks:
# 1. VCC to GND: Should read 3.3V Β±0.1V
# 2. SDA/SCL when idle: Should read 3.3V (pulled high)
# 3. CS when idle: Check expected level (usually HIGH)
# Continuity checks:
# 1. Verify each wire connection
# 2. Check for shorts between signals
# 3. Confirm ground connected
# What to check:
# 1. Clock signal: Clean square wave?
# 2. Data lines: Sharp transitions?
# 3. Rise/fall times: Sharp or rounded?
# 4. Noise: Any glitches or spikes?
# If signals look bad:
# - Shorten wires
# - Add decoupling capacitors (0.1Β΅F near device VCC)
# - Lower clock speed
def debug_i2c_read(i2c, addr, reg, num_bytes):
"""Debug I2C read with detailed output"""
print(f"\nDebug: Reading from 0x{addr:02X}, reg 0x{reg:02X}")
try:
# Check device present
devices = i2c.scan()
if addr not in devices:
print(f"β Device 0x{addr:02X} not found on bus")
print(f" Available: {[hex(d) for d in devices]}")
return None
print(f"β Device 0x{addr:02X} present")
# Attempt read
data = i2c.readfrom_mem(addr, reg, num_bytes)
print(f"β Read successful: {[hex(b) for b in data]}")
return data
except OSError as e:
print(f"β OSError: {e}")
print(" Check: wiring, pull-ups, speed")
return None
# Use it
data = debug_i2c_read(i2c, 0x76, 0xD0, 1)
1. Always use error handling
try:
temp = sensor.read_temperature()
print(f"Temp: {temp}Β°C")
except OSError as e:
print(f"Sensor error: {e}")
# Fallback behavior
2. Add timeouts
import time
def read_with_timeout(sensor, timeout_ms=1000):
"""Read with timeout protection"""
start = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start) < timeout_ms:
try:
return sensor.read()
except:
time.sleep_ms(10)
return None
3. Verify before use
class SafeSensor:
def __init__(self, i2c, addr):
self.i2c = i2c
self.addr = addr
self._verify_connection()
def _verify_connection(self):
"""Verify sensor is connected"""
if self.addr not in self.i2c.scan():
raise RuntimeError(f"Sensor at 0x{self.addr:02X} not found")
print(f"β Sensor at 0x{self.addr:02X} verified")
4. Add decoupling capacitors
# Hardware solution:
# Add 0.1Β΅F ceramic capacitor between VCC and GND
# Place as close to sensor as possible
# Reduces power supply noise
1. Power cycle everything
# Disconnect Pico USB
# Wait 10 seconds
# Reconnect
# Some devices have strange startup states
2. Try a different sensor/device
# If available, swap sensors
# Identifies if problem is:
# - Faulty sensor
# - Wrong wiring
# - Code issue
3. Simplify to minimum
# Start fresh:
# 1. One device only
# 2. Shortest possible wires
# 3. Slowest speed
# 4. Simplest code
i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=100000)
print(i2c.scan()) # Just scan, nothing else
4. Check for hidden connections
# Some breakout boards have:
# - Pull-up resistors you can't see
# - Address selection jumpers
# - Mode selection (I2C vs SPI)
# Read the board documentation carefully
Practice troubleshooting:
# Wrong address
data = i2c.readfrom(0x99, 1) # Doesn't exist
# Wrong pin
i2c = I2C(0, scl=Pin(99), sda=Pin(8)) # Invalid pin
# No CS control
spi.write(b'\x00') # Forgot to use cs.value()
def full_diagnostics():
"""Complete system check"""
print("System Diagnostics\n" + "="*50)
# Check I2C
print("\nI2C Check:")
try:
i2c = I2C(0, scl=Pin(9), sda=Pin(8))
devices = i2c.scan()
print(f" Found {len(devices)} I2C device(s): {[hex(d) for d in devices]}")
except Exception as e:
print(f" I2C Error: {e}")
# Check SPI
print("\nSPI Check:")
try:
spi = SPI(0, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
print(f" SPI initialized: {spi}")
except Exception as e:
print(f" SPI Error: {e}")
print("\n" + "="*50)
full_diagnostics()
In this lesson, you mastered:
You can now diagnose and fix communication problems confidently, getting your projects working faster.
Youβve learned I2C, SPI, and troubleshooting. Now letβs bring it all together! In the final lesson, weβll build a complete environmental monitoring station that combines I2C sensors, data logging, and everything youβve learned in this course.
You can use the arrows β β on your keyboard to navigate between lessons.
Comments