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

pico-de-gallo-hal

pico-de-gallo-hal lets you run real embedded-hal driver code against a Pico de Gallo board on your laptop.

That is the whole value proposition:

  • write your driver against standard traits,
  • swap in pico-de-gallo-hal during host-side testing,
  • iterate without cross-compiling, flashing, linker scripts, or probe tools.

If your driver already speaks embedded-hal, this crate turns Pico de Gallo into a host-side transport layer instead of a custom test harness.

Runtime Model

Hal::new() works in both sync and async host code.

  • Inside a Tokio runtime, the crate uses tokio::task::block_in_place() for blocking trait calls.
  • Outside Tokio, it creates and owns its own runtime.

That means one Hal value can back ordinary tests, examples, and async host applications.

Construction

MethodPurpose
Hal::new()Connect to the first matching board
Hal::new_with_serial_number(serial)Connect to one specific board

Accessors and Helpers

The current public API is:

MethodReturnsPurpose
i2c()I2cI2C bus handle implementing blocking and async traits
spi()SpiRaw SPI bus handle
spi_device(cs_pin)Result<SpiDev, SpiHalError>SPI device handle that manages chip-select for you
uart()UartUART handle implementing embedded_io and embedded_io_async
gpio(pin)GpioGPIO pin handle implementing digital traits
pwm_channel(channel)PwmChannelPWM channel handle implementing SetDutyCycle
delay()DelayDelay provider
onewire()OneWireProject-specific 1-Wire handle
adc_read(channel)Result<u16, AdcHalError>Single-shot ADC read
adc_get_config()Result<AdcConfigurationInfo, AdcHalError>ADC capabilities/configuration
i2c_set_config(frequency)Result<(), I2cHalError>Set I2C frequency
i2c_get_config()Result<I2cFrequency, I2cHalError>Read I2C frequency
spi_set_config(freq, phase, polarity)Result<(), SpiHalError>Set SPI mode and clock
spi_get_config()Result<SpiConfigurationInfo, SpiHalError>Read SPI configuration
pwm_set_config(channel, freq, phase_correct)Result<(), PwmHalError>Set PWM slice configuration
pwm_get_config(channel)Result<PwmConfigurationInfo, PwmHalError>Read PWM slice configuration
gpio_subscribe(pin, edge)Result<(), GpioHalError>Start firmware-side GPIO monitoring
gpio_unsubscribe(pin)Result<(), GpioHalError>Stop GPIO monitoring

Note

The source-of-truth API currently exposes gpio(pin) and uart(). There are not separate output_pin(), input_pin(), or uart_async() constructors; the returned handles implement the relevant blocking and async traits directly.

Implemented Traits

PeripheralBlocking traitAsync trait
GPIOOutputPin, InputPin, StatefulOutputPinWait
I2Cembedded_hal::i2c::I2cembedded_hal_async::i2c::I2c
SPISpiBus, SpiDeviceSpiBus, SpiDevice
UARTembedded_io::Read, embedded_io::Writeembedded_io_async::Read, embedded_io_async::Write
PWMSetDutyCycle
DelayDelayNsDelayNs

And two project-specific surfaces sit alongside the trait-based ones:

Type / methodWhy it exists
OneWire via hal.onewire()there is no standard embedded-hal 1-Wire trait
adc_read() / adc_get_config()there is no stable embedded-hal ADC trait in 1.0

Minimal Example

use embedded_hal::i2c::I2c;
use pico_de_gallo_hal::Hal;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let hal = Hal::new();
    let mut i2c = hal.i2c();

    let mut buf = [0u8; 2];
    i2c.write_read(0x48, &[0x00], &mut buf)?;

    println!("raw bytes: {:02x?}", buf);
    Ok(())
}

That same pattern is why this crate is so useful in driver development: the code above looks like ordinary embedded Rust because it is ordinary embedded Rust.

Transparent Transaction Batching

Two methods matter a lot for performance:

  • I2c::transaction()
  • SpiDevice::transaction()

The HAL does not turn those into several USB round-trips. Instead, it encodes the operations into Pico de Gallo batch requests and sends them in one shot.

So if your driver already uses the transaction APIs from embedded-hal, you get Pico de Gallo’s batching support automatically.

#![allow(unused)]
fn main() {
use embedded_hal::i2c::{I2c, Operation};
use pico_de_gallo_hal::Hal;

fn read_register(hal: &Hal) -> Result<[u8; 2], Box<dyn std::error::Error>> {
    let mut i2c = hal.i2c();
    let mut buf = [0u8; 2];

    i2c.transaction(
        0x48,
        &mut [
            Operation::Write(&[0x00]),
            Operation::Read(&mut buf),
        ],
    )?;

    Ok(buf)
}
}

For SPI devices, spi_device(cs_pin) wraps the same idea with automatic CS assert/deassert around the whole transaction.

When to Reach for This Crate

Use pico-de-gallo-hal when you want to:

  • validate a driver crate against real hardware behavior,
  • keep one code path for host-side tests and MCU targets,
  • avoid writing custom mocks before you know the driver is correct.

For a full walk-through, jump ahead to Testing with pico-de-gallo-hal in Part V.