Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

OperationDescription
ReadRead N bytes from a device at the given address
WriteWrite bytes to a device at the given address
Write-ReadWrite then read on the same target (repeated start, no STOP between)
ScanProbe every address on the bus
BatchSend a sequence of read/write ops as a single USB transaction
Set ConfigChange the bus clock frequency at runtime
Get ConfigQuery the current bus configuration

Bus Frequencies

VariantValueStandard name
Standard100 kHzI²C Standard mode
Fast400 kHzI²C Fast mode
FastPlus1 MHzI²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 scan does 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:

VariantMeaning
NackTarget did not acknowledge
BusErrorI²C bus protocol error
ArbitrationLossLost arbitration to another master
OverrunData overrun on read
BufferTooLongRequest exceeds firmware buffer limit
AddressOutOfRangeAddress outside the 7-bit range
UnsupportedReturned by firmware builds without I²C
OtherCatch-all

The full status-code mapping for FFI lives in appendix/status-codes.md.