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

GPIO

Pico de Gallo exposes 4 general-purpose I/O pins (GPIO 0–3) mapped to RP2350 GPIO 8–11.

Pin Mapping

Gallo PinRP2350 GPIO
08
19
210
311

Operations

OperationDescription
GetRead the current pin state (High or Low)
PutDrive a pin High or Low
Set ConfigConfigure pin direction (input/output) and pull resistor (none/up/down)
MonitorSubscribe to edge events on a pin (rising, falling, or any)

Pin Configuration

Before using a GPIO pin, configure its direction and pull resistor. After power-on a pin is in the firmware’s LegacyAuto mode: it has no explicit direction, and the firmware lazily selects one per operation — a read switches the pad to input, a write drives it. An explicit set-config leaves that mode and pins the direction, after which a write to an explicit input, or a read of an explicit output, is rejected.

Warning. Do not use the RP2350 internal pull-down as proof that an input is low. It can hold a node that was previously driven low, but may not discharge a node that is already high; an unpulled floating input can also drift high within seconds. For a deterministic low state, drive the node low first and verify it before releasing it to pull-down, or use an external pull-down. Configuring pull-down alone does not guarantee that gpio/get returns Low.

CLI

# Configure pin 0 as input with pull-up
gallo gpio set-config --pin 0 --direction input --pull up

# Configure pin 2 as output with no pull
gallo gpio set-config --pin 2 --direction output --pull none

Rust Library

#![allow(unused)]
fn main() {
use pico_de_gallo_lib::{PicoDeGallo, GpioDirection, GpioPull};

fn configure_pins(gallo: &PicoDeGallo) {
    // Configure pin 0 as input with pull-up
    smol::block_on(async {
        gallo
            .gpio_set_config(0, GpioDirection::Input, GpioPull::Up)
            .await
            .unwrap();

        // Configure pin 2 as output with no pull
        gallo
            .gpio_set_config(2, GpioDirection::Output, GpioPull::None)
            .await
            .unwrap();
    });
}
}

C (FFI)

#include "pico_de_gallo.h"

void configure_pins(PicoDeGallo *gallo) {
    /* Configure pin 0 as input with pull-up */
    GalloStatus rc = gallo_gpio_set_config(
        gallo, 0, GpioDirection_Input, GpioPull_Up
    );
    if (rc != GalloStatus_Ok) {
        fprintf(stderr, "set-config failed: %d\n", rc);
    }

    /* Configure pin 2 as output with no pull */
    gallo_gpio_set_config(gallo, 2, GpioDirection_Output, GpioPull_None);
}

Reading and Writing Pins

CLI

# Read the state of pin 0
gallo gpio get --pin 0
# Output: Pin 0: High

# Drive pin 2 high
gallo gpio put --pin 2 --level high

# Drive pin 2 low
gallo gpio put --pin 2 --level low

--level is required and takes high or low; there is no short option for it, because -h is --help.

Rust Library

#![allow(unused)]
fn main() {
use pico_de_gallo_lib::{PicoDeGallo, GpioState};

async fn read_write(gallo: &PicoDeGallo) {
    // Read pin 0
    let state = gallo.gpio_get(0).await.unwrap();
    println!("Pin 0 is {:?}", state);

    // Drive pin 2 high
    gallo.gpio_put(2, GpioState::High).await.unwrap();

    // Drive pin 2 low
    gallo.gpio_put(2, GpioState::Low).await.unwrap();
}
}

C (FFI)

#include "pico_de_gallo.h"
#include <stdio.h>

void read_write(PicoDeGallo *gallo) {
    /* Read pin 0 */
    bool high;
    GalloStatus rc = gallo_gpio_get(gallo, 0, &high);
    if (rc == GalloStatus_Ok) {
        printf("Pin 0: %s\n", high ? "High" : "Low");
    }

    /* Drive pin 2 high */
    gallo_gpio_put(gallo, 2, true);

    /* Drive pin 2 low */
    gallo_gpio_put(gallo, 2, false);
}

Waiting for Pin State Changes

The library provides async methods that block until a pin reaches the requested state or edge transition. These are useful for waiting on external signals without polling.

Timeout semantics

All five gpio/wait-* endpoints carry a firmware-side timeout. A timeout_ms value of 0 selects the firmware’s 30-minute ceiling; it does not wait forever. Values above 30 minutes are clamped to that same ceiling. If the requested state or edge does not arrive before the effective timeout, the endpoint returns GpioError::Timeout.

The Rust methods without a _with_timeout suffix and the C functions without a _with_timeout_ms suffix send 0, so they also expire at the 30-minute ceiling. Use the explicit timeout variants when a shorter bound is appropriate.

Rust Library

#![allow(unused)]
fn main() {
use pico_de_gallo_lib::PicoDeGallo;

async fn wait_for_button(gallo: &PicoDeGallo) {
    // Wait until pin 1 goes high
    gallo.gpio_wait_for_high(1).await.unwrap();
    println!("Pin 1 is now high");

    // Wait until pin 1 goes low
    gallo.gpio_wait_for_low(1).await.unwrap();
    println!("Pin 1 is now low");

    // Wait for a rising edge on pin 1
    gallo.gpio_wait_for_rising_edge(1).await.unwrap();
    println!("Rising edge detected on pin 1");

    // Wait for a falling edge on pin 1
    gallo.gpio_wait_for_falling_edge(1).await.unwrap();
    println!("Falling edge detected on pin 1");

    // Wait for any edge on pin 1
    gallo.gpio_wait_for_any_edge(1).await.unwrap();
    println!("Edge detected on pin 1");
}
}

C (FFI)

#include "pico_de_gallo.h"
#include <stdio.h>

void wait_for_button(PicoDeGallo *gallo) {
    /* These calls wait up to the firmware's 30-minute ceiling. */
    gallo_gpio_wait_for_high(gallo, 1);
    printf("Pin 1 is now high\n");

    gallo_gpio_wait_for_low(gallo, 1);
    printf("Pin 1 is now low\n");

    gallo_gpio_wait_for_rising_edge(gallo, 1);
    printf("Rising edge detected on pin 1\n");

    gallo_gpio_wait_for_falling_edge(gallo, 1);
    printf("Falling edge detected on pin 1\n");

    gallo_gpio_wait_for_any_edge(gallo, 1);
    printf("Edge detected on pin 1\n");
}

Edge Event Monitoring

For continuous monitoring, subscribe to GPIO edge events on a pin. The firmware streams GpioEvent structs to the host whenever the subscribed edge is detected. Each event carries:

pub struct GpioEvent {
    pub pin: u8,
    pub edge: GpioEdge,
    pub state: GpioState,
    pub timestamp_us: u64,
}
  • pin — the Gallo pin number (0–3)
  • edge — the edge that triggered the event (Rising or Falling)
  • state — the pin state after the edge
  • timestamp_us — firmware timestamp in microseconds

CLI

The monitor subcommand subscribes to edge events and prints them until you press Ctrl+C:

# Monitor rising edges on pin 0
gallo gpio monitor --pin 0 --edge rising

# Monitor any edge on pin 1
gallo gpio monitor --pin 1 --edge any

Example output:

[  12345 µs] Pin 0: Rising  → High
[  12890 µs] Pin 0: Rising  → High
[  45012 µs] Pin 0: Rising  → High
^C
Unsubscribed from pin 0.

Rust Library

#![allow(unused)]
fn main() {
use pico_de_gallo_lib::{PicoDeGallo, GpioEdge};

async fn monitor_pin(gallo: &PicoDeGallo) {
    // Subscribe to rising edges on pin 0
    gallo.gpio_subscribe(0, GpioEdge::Rising).await.unwrap();

    // Open a subscription to receive GpioEvent values (buffer depth 16)
    let mut sub = gallo.subscribe_gpio_events(16).await.unwrap();

    // Process events
    for _ in 0..100 {
        let event = sub.recv().await.unwrap();
        println!(
            "[{:>8} µs] Pin {}: {:?} → {:?}",
            event.timestamp_us, event.pin, event.edge, event.state
        );
    }

    // Unsubscribe when done
    gallo.gpio_unsubscribe(0).await.unwrap();
}
}

C (FFI)

#include "pico_de_gallo.h"
#include <stdio.h>

void monitor_pin(PicoDeGallo *gallo) {
    /* Subscribe to rising edges on pin 0 */
    gallo_gpio_subscribe(gallo, 0, GpioEdge_Rising);

    /* ... receive events via topic subscription API ... */

    /* Unsubscribe when done */
    gallo_gpio_unsubscribe(gallo, 0);
}

HAL Usage

The pico-de-gallo-hal crate implements the standard embedded-hal traits over the GPIO pins, providing a familiar interface for portable device drivers.

Blocking Traits

The HAL implements these blocking traits from embedded-hal:

  • OutputPinset_high() / set_low()
  • InputPinis_high() / is_low()
  • StatefulOutputPinis_set_high() / is_set_low()
#![allow(unused)]
fn main() {
use embedded_hal::digital::{InputPin, OutputPin, StatefulOutputPin};
use pico_de_gallo_hal::Hal;

fn blink_and_read(hal: &Hal) {
    let mut led = hal.output_pin(2);
    let button = hal.input_pin(0);

    // Drive pin 2 high
    led.set_high().unwrap();

    // Read pin 0
    if button.is_high().unwrap() {
        println!("Button pressed");
    }

    // Check what we're currently driving
    if led.is_set_high().unwrap() {
        println!("LED is on");
    }

    led.set_low().unwrap();
}
}

Async Trait

The HAL implements the embedded-hal-async Wait trait for non-blocking edge/level detection:

#![allow(unused)]
fn main() {
use embedded_hal_async::digital::Wait;
use pico_de_gallo_hal::Hal;

async fn wait_for_signal(hal: &Hal) {
    let mut pin = hal.input_pin(1);

    pin.wait_for_high().await.unwrap();
    println!("Pin went high");

    pin.wait_for_rising_edge().await.unwrap();
    println!("Rising edge detected");
}
}

Complete Example: Button-Controlled LED

This example configures pin 0 as an input (button with pull-up) and pin 2 as an output (LED). It toggles the LED on each button press.

CLI

# Configure pins
gallo gpio set-config --pin 0 --direction input --pull up
gallo gpio set-config --pin 2 --direction output --pull none

# Read button, toggle LED manually
STATE=$(gallo gpio get --pin 0)
gallo gpio put --pin 2 --level high

# Or monitor button presses
gallo gpio monitor --pin 0 --edge falling

Rust Library

#![allow(unused)]
fn main() {
use pico_de_gallo_lib::{
    PicoDeGallo, GpioDirection, GpioPull, GpioState, GpioEdge,
    PicoDeGalloError, GpioError,
};

async fn button_led() -> Result<(), PicoDeGalloError<GpioError>> {
    let gallo = PicoDeGallo::new();

    // Configure pin 0 as input with pull-up (button)
    gallo.gpio_set_config(0, GpioDirection::Input, GpioPull::Up).await?;

    // Configure pin 2 as output (LED)
    gallo.gpio_set_config(2, GpioDirection::Output, GpioPull::None).await?;

    let mut led_on = false;

    loop {
        // Wait for button press (falling edge because of pull-up)
        gallo.gpio_wait_for_falling_edge(0).await?;

        led_on = !led_on;
        let state = if led_on { GpioState::High } else { GpioState::Low };
        gallo.gpio_put(2, state).await?;
    }
}
}

C (FFI)

#include "pico_de_gallo.h"
#include <stdbool.h>

int button_led(void) {
    PicoDeGallo *gallo = gallo_new();
    if (!gallo) return -1;

    /* Configure pin 0 as input with pull-up (button) */
    gallo_gpio_set_config(gallo, 0, GpioDirection_Input, GpioPull_Up);

    /* Configure pin 2 as output (LED) */
    gallo_gpio_set_config(gallo, 2, GpioDirection_Output, GpioPull_None);

    bool led_on = false;

    for (;;) {
        /* Wait for button press (falling edge) */
        gallo_gpio_wait_for_falling_edge(gallo, 0);

        led_on = !led_on;
        gallo_gpio_put(gallo, 2, led_on);
    }

    return 0;
}

Error Handling

All GPIO operations return errors through the standard PicoDeGalloError wrapper. GPIO-specific errors are represented as PicoDeGalloError<GpioError>:

#![allow(unused)]
fn main() {
use pico_de_gallo_lib::{PicoDeGallo, PicoDeGalloError, GpioError, GpioState};

async fn safe_read(gallo: &PicoDeGallo) {
    match gallo.gpio_get(0).await {
        Ok(state) => println!("Pin 0: {:?}", state),
        Err(PicoDeGalloError::Rpc(e)) => {
            eprintln!("RPC error: {e:?}");
        }
        Err(PicoDeGalloError::Endpoint(gpio_err)) => {
            eprintln!("GPIO error: {gpio_err:?}");
        }
        Err(e) => {
            eprintln!("Other error: {e:?}");
        }
    }
}
}

API Reference

Lib Methods

All methods are async and available on PicoDeGallo:

MethodReturnsDescription
gpio_get(pin: u8)Result<GpioState, ...>Read pin state
gpio_put(pin: u8, state: GpioState)Result<(), ...>Set pin state
gpio_set_config(pin, direction, pull)Result<(), ...>Configure direction and pull
gpio_wait_for_high(pin: u8)Result<(), ...>Wait until pin is high
gpio_wait_for_low(pin: u8)Result<(), ...>Wait until pin is low
gpio_wait_for_rising_edge(pin: u8)Result<(), ...>Wait for low→high transition
gpio_wait_for_falling_edge(pin: u8)Result<(), ...>Wait for high→low transition
gpio_wait_for_any_edge(pin: u8)Result<(), ...>Wait for any transition
gpio_subscribe(pin: u8, edge: GpioEdge)Result<(), ...>Subscribe to edge events on a pin
gpio_unsubscribe(pin: u8)Result<(), ...>Unsubscribe from edge events
subscribe_gpio_events(depth)Result<Subscription<GpioEvent>, ...>Open a subscription to receive GPIO events

FFI Functions

All functions return GalloStatus:

FunctionDescription
gallo_gpio_get(gallo, pin, out_high)Read pin state into *out_high
gallo_gpio_put(gallo, pin, high)Set pin state
gallo_gpio_set_config(gallo, pin, direction, pull)Configure direction and pull
gallo_gpio_wait_for_high(gallo, pin)Block until pin is high
gallo_gpio_wait_for_low(gallo, pin)Block until pin is low
gallo_gpio_wait_for_rising_edge(gallo, pin)Block until rising edge
gallo_gpio_wait_for_falling_edge(gallo, pin)Block until falling edge
gallo_gpio_wait_for_any_edge(gallo, pin)Block until any edge
gallo_gpio_subscribe(gallo, pin, edge)Subscribe to edge events
gallo_gpio_unsubscribe(gallo, pin)Unsubscribe from edge events

CLI Commands

CommandDescription
gallo gpio get --pin NRead pin state
gallo gpio put --pin N --level highDrive pin high
gallo gpio put --pin N --level lowDrive pin low
gallo gpio set-config --pin N --direction DIR --pull PULLConfigure pin
gallo gpio monitor --pin N --edge EDGEStream edge events until Ctrl+C

Zephyr

The pico-de-gallo/zephyr module ships an odp,pico-de-gallo-gpio controller so a Zephyr application running on native_sim can drive these pins through the standard Zephyr GPIO API. Full build instructions live in zephyr/README.md.

Topology. The controller is a direct child of the odp,pico-de-gallo MFD parent, which owns the USB connection. Enable both:

&pdg0 {
	status = "okay";
	serial-number = "E6614C311B8C9E37";
};

&pdg_gpio0 {
	status = "okay";
};

serial-number on the parent is mandatory for an enabled GPIO child and is enforced at build time. GPIO actuates physical pins, and a connection opened without a selector cannot report which attached board it chose. Presence is not uniqueness: two parents naming the same serial still alias to one board.

Pin indices are the firmware GPIO indices 0–3 above, not RP2350 numbers. ngpios must equal the firmware-reported GPIO count or initialization fails with -EINVAL.

Supported flags: GPIO_INPUT, GPIO_OUTPUT, GPIO_PULL_UP, GPIO_PULL_DOWN, GPIO_OUTPUT_INIT_LOW, GPIO_OUTPUT_INIT_HIGH and GPIO_ACTIVE_LOW.

Rejected: GPIO_DISCONNECTED and GPIO_INPUT | GPIO_OUTPUT together (-ENOTSUP); single-ended, open-source and open-drain (-ENOTSUP); interrupt-mode flags including GPIO_INT_WAKEUP (-ENOTSUP); any flag bit outside the supported set (-ENOTSUP); both pulls, both init levels, or an init level without GPIO_OUTPUT (-EINVAL).

Blocking. Every operation that reaches hardware is a USB round trip. Calls from interrupt context return -EWOULDBLOCK; transport failure is -EIO. Configuration and writes may return -EBUSY when a live firmware event subscription owns the pin. Write paths may return -EACCES when firmware records the pin as an explicit input. During a read, gpio_pin_get() normalizes subscription -EBUSY to -EIO and treats -EACCES from an explicit output as a zero bit.

Reads are destructive on unconfigured pins. gpio_port_get_raw() scans every pin. Firmware gpio/get switches a LegacyAuto pad to input, so gpio_pin_get() can reconfigure unrelated unconfigured pins as a side effect. Configure all pins explicitly and do not treat reads as state-neutral queries.

Non-atomic. Multi-pin writes are ascending per-pin round trips with no rollback: on failure the acknowledged prefix definitely changed, the failed pin is indeterminate, and later pins were never issued. Output initialization is two round trips, so the previous level can briefly appear before the requested one.

No toggle, no interrupts. gpio_pin_toggle() returns -ENOTSUP, because an explicit output cannot be read back and the driver deliberately caches no pin state. Interrupt configuration, callback management and the pending-interrupt query return -ENOSYS. Generic toggle consumers — blinky, the GPIO shell, the TPS382x watchdog and the LS0xx display — therefore do not work with this controller.

Serving as SPI chip select

This controller is also how the odp,pico-de-gallo-spi controller drives chip select. Each cs-gpios entry on an enabled SPI controller must name an enabled odp,pico-de-gallo-gpio controller under the same odp,pico-de-gallo parent; a foreign controller, a disabled sibling, or a Pico de Gallo GPIO controller belonging to a different parent is rejected at build time with an assertion naming the cs-gpios array index. Enabling pdg_gpio0 is therefore now a prerequisite for using SPI, not an optional extra. See SPI.

At initialization the SPI controller configures every declared chip-select pin as an explicit output, inactive, in ascending array order. That is two USB round trips per pin (set-config, then put), and there is no rollback: if a pin fails, earlier entries are acknowledged inactive, that entry’s state is indeterminate, and later entries were never issued. A pin under a live firmware GPIO event subscription fails with -EBUSY; reset it explicitly with gallo_system_reset_subscriptions() after a strict open, then reinitialize, or power-cycle the board.

A declared chip-select pin must be owned exclusively by SPI. The GPIO child being the sole driver path for the pin’s mode is not an ownership reservation: a direct GPIO consumer in the same application can reconfigure or drive that pin between SPI operations, and nothing detects it.

Limitations

  • 4 pins only — GPIO 0–3 (RP2350 GPIO 8–11).
  • Shared with Logic Capture — pins used by an active capture session cannot be used for GPIO operations. They are returned automatically when capture stops.
  • No analog — all pins are digital only.
  • Edge event timestamps come from the firmware’s microsecond timer, not the host clock. Events are timestamped when the edge is detected on the RP2350.