I²C
Pico de Gallo provides a single I²C bus on the RP2350’s hardware I²C1 controller. SDA is on GPIO 2 and SCL on GPIO 3. The v1.1 PCB includes on-board 4.7 kΩ pull-ups; on v1.0 you must supply your own.
Operations
| Operation | Description |
|---|---|
| Read | Read N bytes from a device at the given address |
| Write | Write bytes to a device at the given address |
| Write-Read | Write then read on the same target (repeated start, no STOP between) |
| Scan | Probe every address on the bus |
| Batch | Send a sequence of read/write ops as a single USB transaction |
| Set Config | Change the bus clock frequency at runtime |
| Get Config | Query the current bus configuration |
Bus Frequencies
| Variant | Value | Standard name |
|---|---|---|
Standard | 100 kHz | I²C Standard mode |
Fast | 400 kHz | I²C Fast mode |
FastPlus | 1 MHz | I²C Fast-mode Plus |
The firmware defaults to Standard mode.
CLI
$ gallo i2c help
I2C access methods
Commands:
scan Scan I2C bus for existing devices
read Read bytes through the I2C bus from device at given address
write Write bytes through I2C bus to device at given address
write-read Write bytes followed by read bytes
set-config Set I2C bus configuration (frequency)
get-config Get current I2C bus configuration
batch Execute multiple I2C operations in a single transfer
Scanning
Warning
The RP235x I²C controller doesn’t expose a pure address-probe primitive, so
gallo i2c scandoes a 1-byte read at each address. Devices that ACK a read are reported as present. A handful of peripherals may end up in an unexpected state after being probed this way — usually a power cycle clears it.
$ gallo i2c scan
╭────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────╮
│ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ a │ b │ c │ d │ e │ f │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ 0 │ RR │ RR │ RR │ RR │ RR │ RR │ RR │ RR │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │
│ 1 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │
│ 2 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │
│ 3 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │
│ 4 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ 48 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │
│ 5 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │
│ 6 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ 68 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │
│ 7 │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ -- │ RR │ RR │ RR │ RR │ RR │ RR │ RR │ RR │
╰────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────╯
RR marks reserved I²C addresses. Pass -r (--include-reserved)
to probe them anyway.
Read / Write / Write-Read
$ gallo i2c read --address 0x48 --count 2
6b 15
$ gallo i2c write --address 0x48 --bytes 0x01 0xe0 0xa0
$ gallo i2c write-read --address 0x48 --bytes 0x00 --count 2
6b 15
Read output supports -f hex (default), -f binary, and
-f ascii.
Config
$ gallo i2c set-config --frequency fast
$ gallo i2c get-config
Frequency: Fast (400 kHz)
Batch
A single USB round-trip for a multi-op transaction:
$ gallo i2c batch -a 0x48 --op write:0x00 --op read:2
Read data (2 bytes):
0000: 19 80 ..
See Transaction Batching for the full mechanism.
Rust Library
All PicoDeGallo methods are async. PicoDeGallo::new() is
not async.
use pico_de_gallo_lib::{I2cBatchOp, I2cFrequency, PicoDeGallo};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pg = PicoDeGallo::new();
pg.i2c_set_config(I2cFrequency::Fast).await?;
// Plain write-read
let data = pg.i2c_write_read(0x48, &[0x00], 2).await?;
let raw = u16::from_be_bytes([data[0], data[1]]);
println!("raw = 0x{raw:04x}");
// Same transaction, batched
let ops = [
I2cBatchOp::Write { data: &[0x00] },
I2cBatchOp::Read { len: 2 },
];
let _ = pg.i2c_batch(0x48, &ops).await?;
Ok(())
}
HAL
The HAL exposes the bus as an
[embedded_hal::i2c::I2c] / [embedded_hal_async::i2c::I2c]
implementor — so any driver written against those traits Just
Works:
#![allow(unused)]
fn main() {
use embedded_hal::i2c::I2c;
use pico_de_gallo_hal::Hal;
fn read_tmp102(hal: &Hal) {
let mut i2c = hal.i2c();
let mut buf = [0u8; 2];
i2c.write_read(0x48, &[0x00], &mut buf).unwrap();
let raw = u16::from_be_bytes(buf);
let celsius = (raw >> 4) as f32 * 0.0625;
println!("Temperature: {celsius:.2} °C");
}
}
I2c::transaction() is automatically batched into a single USB
round-trip — see Transaction Batching.
C (FFI)
#include "pico_de_gallo.h"
#include <stdio.h>
void read_tmp102(PicoDeGallo *gallo) {
uint8_t tx[] = {0x00};
uint8_t rx[2];
Status s = gallo_i2c_write_read(gallo, 0x48, tx, 1, rx, 2);
if (s != Ok) { fprintf(stderr, "write-read failed: %d\n", s); return; }
uint16_t raw = ((uint16_t)rx[0] << 8) | rx[1];
printf("raw = 0x%04x\n", raw);
}
I²C frequency is passed as uint8_t: 0 = Standard, 1 = Fast,
2 = FastPlus. See crates/ffi.md.
Python
from pyco_de_gallo import PycoDeGallo, I2cFrequency
pg = PycoDeGallo()
pg.i2c_set_config(I2cFrequency.Fast)
data = pg.i2c_write_read(0x48, [0x00], 2)
raw = (data[0] << 8) | data[1]
print(f"raw = 0x{raw:04x}")
Error Handling
I²C operations return PicoDeGalloError<I2cError> on the Rust
side; FFI returns negative Status values:
| Variant | Meaning |
|---|---|
Nack | Target did not acknowledge |
BusError | I²C bus protocol error |
ArbitrationLoss | Lost arbitration to another master |
Overrun | Data overrun on read |
BufferTooLong | Request exceeds firmware buffer limit |
AddressOutOfRange | Address outside the 7-bit range |
Unsupported | Returned by firmware builds without I²C |
Other | Catch-all |
The full status-code mapping for FFI lives in
appendix/status-codes.md.