The Raspberry Pi Pico 1 (RP2040) features a dual-core 133MHz Cortex-M0+, 264KB SRAM, and 26 usable GPIOs, while the newer Pico 2 (RP2350) upgrades to 150MHz Cortex-M33 cores, 520KB SRAM, and a significantly cleaner ADC. If you are building a precision battery logger or sensor node today, buy the original Pico 1 (RP2040) with pre-soldered headers (approx. $4.00). The Pico 2's 5V-tolerant GPIOs and improved ADC are excellent, but the RP2040's mature MicroPython ecosystem and lower price make it the definitive default for I2C sensor projects. Below, we break down the exact specifications, build a LiFePO4 capacity logger, and debug the most common I2C bus failures.
The Decision Matrix: Which Pico Variant Should You Buy?
Do not default to the newest board. Use this decision path to select the exact hardware for your bench:
| If your project requires... | Then choose this board variant | Why (Spec Constraint) |
|---|---|---|
| Strict budget (<$5) + mature MicroPython libs | Pico 1 (RP2040) | Lowest cost, 100% library compatibility, 264KB RAM is enough for most I2C polling. |
| WiFi/BLE + MQTT telemetry | Pico W / Pico 2 W | Integrated Infineon CYW43439 radio. Note: Pico W uses 3 GPIOs internally for the radio. |
| Direct 5V sensor interfacing + precision ADC | Pico 2 (RP2350) | RP2350 fixes the RP2040's noisy ADC and adds a secondary VREG for 5V-tolerant GPIO banks. |
| High-speed PIO state machines (e.g., RGB LEDs) | Pico 1 or Pico 2 | Both have 8 PIO state machines. Pico 2 adds HSTX for DVI output. |
Spec-Sheet Deep Dive: RP2040 vs. RP2350 Hardware Limits
Understanding the Raspberry Pi Pico specifications at the silicon level prevents hardware damage and software bugs. The most critical difference lies in the Analog-to-Digital Converter (ADC) and GPIO voltage tolerance.
| Specification | Pico 1 (RP2040) | Pico 2 (RP2350) |
|---|---|---|
| Processor | Dual-core ARM Cortex-M0+ @ 133MHz | Dual-core ARM Cortex-M33 @ 150MHz (or RISC-V) |
| Memory (SRAM) | 264 KB | 520 KB |
| Flash | 2 MB (QSPI) | 4 MB (QSPI) |
| ADC Performance | 12-bit, but ~50mV noise floor, non-linear bottom 4 bits | 12-bit, vastly improved linearity and lower noise floor |
| GPIO Voltage | 3.3V strictly (5V fries the chip) | 3.3V default, select banks are 5V tolerant |
| UART/I2C/SPI | 2x UART, 2x I2C, 2x SPI | 2x UART, 2x I2C, 2x SPI |
The ADC Trap: The RP2040's internal ADC is notoriously noisy. If you try to measure a LiFePO4 cell's voltage (3.0V - 3.6V) directly on GPIO26 (ADC0), the bottom 4 bits will fluctuate wildly, giving you a ±50mV error margin. For battery capacity testing, this is unacceptable. This specification flaw is exactly why we use an external INA219 I2C sensor in the build below.
Project Build: LiFePO4 Capacity Logger
This build monitors a LiFePO4 cell's voltage and current draw over time, calculating remaining capacity. It targets the Pico 1 (RP2040) running MicroPython v1.22+.
Parts List
- MCU: Raspberry Pi Pico 1 (RP2040) with headers
- Sensor: INA219 I2C High-Side Current/Power Sensor (Adafruit 904 or generic equivalent)
- Display: 128x64 SSD1306 I2C OLED (0.96 inch)
- Power: 1S LiFePO4 battery (3.2V nominal) + JST connector
- Wiring: 22 AWG silicone wire, breadboard or perfboard
Pin Mapping Table
| Component | Pin Label | Pico GPIO / Pin | Notes |
|---|---|---|---|
| SSD1306 OLED | SDA | GP0 (Pin 1) | I2C0 SDA |
| SSD1306 OLED | SCL | GP1 (Pin 2) | I2C0 SCL |
| INA219 Sensor | SDA | GP4 (Pin 6) | I2C0 SDA (Shared bus) |
| INA219 Sensor | SCL | GP5 (Pin 7) | I2C0 SCL (Shared bus) |
| Both Modules | VCC | 3V3 OUT (Pin 36) | Do NOT use VBUS (5V) |
| Both Modules | GND | GND (Pin 38) | Common ground required |
Wiring Steps
- De-energize: Ensure the Pico is unplugged from USB and the LiFePO4 cell is disconnected.
- Bus the I2C lines: Connect the SDA pins of both the OLED and INA219 together, then route to Pico GP0. Connect the SCL pins together and route to Pico GP1.
- Power the modules: Route Pico Pin 36 (3V3 OUT) to the VCC rails of both sensors. Route Pico Pin 38 (GND) to the GND rails.
- Connect the load: Wire the LiFePO4 positive terminal to the INA219
VIN+screw terminal. Wire the INA219VIN-terminal to your load (e.g., a resistor or motor). Wire the load's ground back to the battery ground. - Verify: Use a multimeter to check continuity between the Pico GND pin and the INA219 GND pin. Read < 1 ohm.
Complete MicroPython Code with Error Handling
This code includes a lightweight, inline INA219 driver so you do not need to hunt for third-party .mpy libraries. It assumes you have installed the standard micropython-ssd1306 package via Thonny's package manager (Tools -> Manage Packages).
import machine
import time
import ssd1306
from framebuf import FrameBuffer, MONO_HLSB
# --- PIN DEFINITIONS ---
I2C_SDA = machine.Pin(0)
I2C_SCL = machine.Pin(1)
I2C_FREQ = 400000
# --- HARDWARE INITIALIZATION ---
try:
i2c = machine.I2C(0, sda=I2C_SDA, scl=I2C_SCL, freq=I2C_FREQ)
oled = ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
except OSError as e:
print(f'FATAL: I2C Init failed. {e}')
machine.reset()
# --- INLINE INA219 DRIVER ---
class INA219:
def __init__(self, i2c_bus, addr=0x40):
self.i2c = i2c_bus
self.addr = addr
# Config: 16V Bus, 40mV Shunt, 12-bit resolution, continuous mode
self._write_reg(0x00, 0x199F)
self.calibrate()
def calibrate(self):
# Calibrate for 0.1 ohm shunt, max 3.2A
self._write_reg(0x05, 4096)
def _write_reg(self, reg, val):
self.i2c.writeto(self.addr, bytes([reg, (val >> 8) & 0xFF, val & 0xFF]))
def _read_reg(self, reg):
self.i2c.writeto(self.addr, bytes([reg]))
data = self.i2c.readfrom(self.addr, 2)
return (data[0] << 8) | data[1]
def get_bus_voltage(self):
raw = self._read_reg(0x02)
return (raw >> 3) * 0.004 # 4mV per bit
def get_current_ma(self):
raw = self._read_reg(0x04)
if raw > 32767:
raw -= 65536
return raw * 1.0 # 1mA per bit with 4096 cal
sensor = INA219(i2c)
total_mah = 0.0
last_time = time.ticks_ms()
# --- MAIN LOOP ---
while True:
try:
v = sensor.get_bus_voltage()
ma = sensor.get_current_ma()
# Integrate current over time to get mAh
now = time.ticks_ms()
dt_hours = time.ticks_diff(now, last_time) / 3600000.0
total_mah += (ma * dt_hours)
last_time = now
# Update OLED
oled.fill(0)
oled.text(f'V: {v:.2f}V', 0, 0)
oled.text(f'I: {ma:.0f}mA', 0, 15)
oled.text(f'Cap: {total_mah:.1f}mAh', 0, 30)
oled.show()
time.sleep(1.0)
except OSError as e:
oled.fill(0)
oled.text('I2C BUS ERROR', 0, 0)
oled.text(str(e), 0, 15)
oled.show()
time.sleep(5)
Debugging: Fixing OSError: [Errno 110] ETIMEDOUT
When working with Pico I2C specifications, the most common failure mode during sensor polling is the bus hanging. If your Thonny REPL throws this exact string:
This means the Pico sent a clock pulse on the SCL line, but the SDA line remained stuck high or low, and the slave device failed to acknowledge (ACK) within the hardware timeout window.
The First Three Things to Check When It Fails
- Verify Pull-Up Resistors: The RP2040 I2C peripheral requires external pull-up resistors to 3.3V. While the Pico has internal pull-ups, they are ~50kΩ, which is too weak for 400kHz I2C. Check your OLED and INA219 breakout boards; 95% of them include 4.7kΩ surface-mount pull-ups. If you are using bare modules, add 4.7kΩ resistors from SDA to 3.3V and SCL to 3.3V.
- Check Wire Length and Capacitance: The I2C spec limits bus capacitance to 400pF. If your jumper wires exceed 30cm (12 inches), the capacitance will round off the square wave edges, causing the
ETIMEDOUTerror. Keep I2C traces under 15cm. - Drop the Clock Frequency: If your wiring is messy or long, 400kHz (Fast Mode) will fail. Change the initialization in the code from
freq=400000tofreq=100000(Standard Mode). This gives the signal more time to rise and fall.
Ranked Causes for Intermittent Timeouts
| Rank | Cause | Fix |
|---|---|---|
| 1 | Loose breadboard contact on SDA/SCL | Solder headers or use a screw-terminal breakout. |
| 2 | Slave device brownout (INA219 resetting) | Add a 100µF decoupling capacitor across the sensor's VCC/GND. |
| 3 | Address collision | Run i2c.scan() in REPL. Ensure OLED is 0x3C and INA219 is 0x40. |
Extending and Simplifying the Build
Depending on your deployment environment, you may need to alter the hardware footprint.
How to Simplify (Headless Data Logging)
If you are deploying this inside a battery pack where a screen is useless, drop the SSD1306 OLED entirely. This removes the heaviest I2C dependency and frees up 1KB of RAM. Instead, log the data to the Pico's internal flash using the uos module:
with open('log.csv', 'a') as f:
f.write(f'{time.time()},{v},{ma}\n')
Note: Flash memory has a limited write cycle life (~100k cycles). Only write to the file once every 60 seconds, not every second.
How to Extend (Wireless Telemetry)
If you need to monitor the battery from your phone, swap the Pico 1 for a Pico W. Add the network and umqtt.simple libraries to push the voltage and capacity data to an MQTT broker (like Mosquitto or Home Assistant) every 10 seconds. The Pico W's CYW43439 radio draws ~120mA during WiFi transmission, so factor that parasitic draw into your capacity calculations by measuring the Pico's own current consumption on the INA219's load side.






