ww_gpio¶
Part of the IO trait family. ww_gpio is a global WireWeaver trait for controlling GPIO pins remotely: reading and setting output levels,
reading input levels, configuring pin mode / pull / drive strength, and
observing edge events as a stream. It is
no_std by default and depends only on wire_weaver and ww_si (for the Volt IO level).
Add it to a device API crate with:
Traits¶
GPIO pins are grouped into banks (ports), mirroring how most MCUs expose GPIO in hardware. Both levels are traits,
so they compose with the rest of the API using ww_impl! and can be nested as
arrays.
Bank¶
One bank (port) of pins sharing the same IO level:
#[ww_trait]
trait Bank {
ww_impl!(pin[]: Pin);
fn capabilities() -> BankCapabilities<'i>;
fn configure(config: BankConfig) -> Result<BankConfig, Error>;
fn name() -> &'i str;
}
pin[]- array of individual pins in the bank, indexed 0-based; seePinbelow. Indexing can either follow the natural range of a bank (e.g. an IO expander) or a list of specific indices (e.g. exposing onlyPB1andPB5of an MCU port).capabilities()- static description of what the bank supports: which IO levels, modes and custom mode/pull/speed strings, seeBankCapabilitiesunder Types below.configure- what the pins of the bank share, in aBankConfig: the IO level, adjustable if the hardware has level translators (io_level: Noneleaves it as it is). Answers with what was actually set, soconfigurewithNonereads the level in effect. Same shape as theconfigureofww_uart,ww_i2c,ww_spi.name()- user-friendly bank name (e.g."PA").
Pin¶
One pin of a bank. Commonly used operations (set_output_level, toggle, output_level, input_level) are
defined first, to get more compact resource paths since low path indices take fewer bits on the wire (see
traits):
#[ww_trait]
pub trait Pin {
fn set_output_level(level: Level);
fn toggle();
fn output_level() -> Level;
fn input_level() -> Level;
#[stream(IoPinEvent, counted)]
fn event(enabled: IoPinEnabledEvents<'i>) -> Result<(), Error>;
fn set_mode(mode: Mode, initial: Option<Level>) -> Result<(), Error>;
fn mode() -> Mode;
property!(rw pull: Pull, Error);
property!(rw speed: Speed, Error);
fn capabilities() -> PinCapabilities<'i>;
}
set_output_level/toggle/output_level- control and read back the pin's output register. If the pin is currently an input, these still act on the output register without changing pin mode.input_level- reads the input register. If the pin is configured as output and its input buffer is disabled, the current output level should be returned instead.event- a counted stream function ofIoPinEvents (rising/falling edge, custom sources): the host opens it with the sources to report (Common(Unsupported)for one the pin can't generate) and closes it to stop. Its unit is an event: the event head carries the device time of each edge and exactly how many edges the device dropped (stream delivery).set_mode/mode- switch between push-pull, open-drain, input, high-z, analog or a custom mode, optionally setting the initial output level before the switch. Can fail withCommon(Unsupported), orDifferentModeson a bank whose pins share one mode.pull- pull resistor configuration, can fail withCommon(Unsupported).speed- drive strength/slew rate, can fail withCommon(Unsupported).capabilities()- what this pin supports: modes, pulls, speeds, whethereventworks, and its name, seePinCapabilitiesunder Types below.
Several pins¶
ww_gpio stays simple and low speed: every call acts on one pin. A host that reads or sets many pins in one
round trip sends the per-pin calls as one multi request (MultiCall, MultiRead, MultiWrite).
The device still runs them one after another, so pins are not sampled or changed at one instant. A bit-banged
parallel bus, a strobe with its data lines, or anything else that needs several pins at the same instant or with
precise timing is a signal generator (driving) or a logic analyzer
(sampling).
Observing a pin level (planned)¶
A property!(ro+observe level: Level) on Pin, at one of its reserved indices 4..=6, would let a host watch slow
inputs without opening the event stream of every pin. It waits for array index bounds (API-29): Pin sits in the
Bank::pin[] array, and observed properties under a resource array aren't
supported yet (STD-31).
Types¶
Level-Low/High,sized, 1 bit on the wire.Mode-PushPullOutput,OpenDrainOutput,Input,HighZ(likeInputbut may disable the input buffer to save power),Analog,Custom(u8);sized, 1 nibble.Pull-None,Up,Down,Custom(u8);sized, 2 bits.Speed-Slow,Medium,Fast,VeryFast,Custom(u8);sized, 1 nibble.IoPinEvent-RisingEdge,FallingEdge,Custom(u8);sized, 2 bits.GpioError<'i>-Common(CommonError)(common errors),DifferentModes; unsized (unib32discriminant), evolvable,Displayfrom the doc lines. Each method lists the variants it returns (#[errors]).IoPinEnabledEvents<'i>- which sources theeventstream reports, its argument:rising: bool,falling: bool, and acustom: RefVec<'i, u8>list of custom source indices.PinCapabilities<'i>- what one pin supports:modes: MaxLen<RefVec<'i, Mode>, 8>,pulls: MaxLen<RefVec<'i, Pull>, 4>,speeds: MaxLen<RefVec<'i, Speed>, 5>,events: bool(theeventstream works on this pin),name: MaxLen<&'i str, 16>(e.g."IO4","PB5").BankConfig-io_level: Option<Volt>; the argument and the answer ofBank::configure.BankCapabilities<'i>- what a bank supports:io_levels: MaxLen<RefVec<'i, Volt>, MAX_IO_LEVELS>,io_level_min/io_level_max: Option<Volt>(an adjustable supply),push_pull/open_drain/input/individually_configurable_pins: bool, andcustom_mode/custom_pull/custom_speed: RefVec<'i, &'i str>describing anyCustom(u8)variants.
Level and Mode also carry small helpers: Level::is_high() / is_low(), Mode::is_driving() (true for
PushPullOutput and OpenDrainOutput).
All enums and structs are behind #[derive_shrink_wrap] with owned(feature = "std") and
cfg_attr_borrowed(feature = "defmt", derive(defmt::Format)), so owned (*Owned) variants and defmt::Format are
available by enabling the crate's std / defmt features.
Composing into a device API¶
A device exposes one or more banks by implementing Bank as an array on its API root, see the examples/all_gpio_api
crate:
Because Bank itself contains pin[]: Pin, resources end up two levels deep and addressed by a [bank_index,
pin_index] pair, e.g. port(0).pin(5).
Client usage (examples/all_gpio)¶
The generated client wraps wire_weaver_client::Commander; see examples/all_gpio/src/lib.rs for the small
AllGpio wrapper type. A few usage patterns from examples/all_gpio/examples:
Enumerate banks and pins, and read a bank's capabilities (info.rs):
let device = AllGpio::new().connect().await?;
let available_ports = device.port_valid_indices().read().await?;
for port_idx in available_ports.iter() {
let name = device.port(port_idx).name().call().await?;
let available_pins = device.port(port_idx).pin_valid_indices().read().await?;
println!("Port {port_idx}: name: '{name}', pins: {available_pins:?}");
}
let capabilities = device.port(0).capabilities().call().await?;
Read every pin's mode sequentially (mode.rs) - simple, but one round trip per call:
for port in ports.iter() {
for pin in 0..=15 {
let mode = device.port(port).pin(pin).mode().call().await?;
}
}
The same, but firing all calls concurrently with futures::future::join_all (mode_parallel.rs) - independent
requests get assembled into as few USB packets as possible by the client, giving a large speedup (940ms -> 55ms on
USB Full Speed in this example, 361ms -> 38ms on High Speed) without changing anything server-side:
let modes = (0..=15)
.map(|pin| device.port(port).pin(pin).mode().call())
.collect::<Vec<_>>();
let modes = join_all(modes).await;
mode_mega_parallel.rs takes this further, flattening the bank/pin loop into a single join_all over every pin of
every bank at once, for the best-case latency (17ms on USB High Speed in this example).
embedded-hal (ww_gpio_hl)¶
ww_gpio_hl wraps the generated client in pin types modeled after embassy's: Flex (any mode), PushPullOutput,
Input, OpenDrainOutput and Bank, in a blocking (blocking::FlexBlocking, ...), an async
(asynchronous::Flex, ...) and a promise flavor (immediate-mode GUIs). They carry the embedded-hal 1.0 traits, so
a stock driver gets its reset, chip-select or interrupt line from a remote device:
- blocking pins:
embedded_hal::digital::{OutputPin, StatefulOutputPin, InputPin}. Each call is a round trip,toggleone call (Pin::toggle), not a read and a write. They block, so not inside an async task. - async pins (
Flex,Input,OpenDrainOutput):embedded_hal_async::digital::Wait.
let irq = Flex::new_ignore_mode(device.port(0).pin(4).attachment())?
.urgent()
.into_input(Pull::Up)
.await?;
let mut fusb = Fusb302::new(i2c, irq);
A wait doesn't miss an edge that comes after the call, nor count one from before it: it opens the pin's event
stream with the edges it needs first, then (wait_for_high / wait_for_low) reads the input level, then awaits the
edge. The stream is closed when the wait returns or is dropped. Setting up is one round trip, two for a level; a pin
already at the level returns after these. last_edge_time() gives the device time of the edge that ended the last
wait (when the device sends one), to measure from the moment the line moved rather than from when the host heard of
it. A gap in the stream (the device dropped events) ends an edge wait, since the edge may be among the lost ones,
and makes a level wait read the level again. urgent() / set_priority() set the priority of the pin's requests,
the stream open included.
tests/io_gpio runs them through generic functions, the way a driver uses them: eh_async_wait (a level that is
already there, a falling edge with its device time, an earlier edge that doesn't count, a dropped wait closing the
stream) and eh_blocking_digital.
MCU server implementation (examples_mcu/nucleo_h743zi2/src/bin/all_gpio.rs)¶
On the device side, ww_codegen! generates the dispatch code and leaves the actual register access to hand-written
methods on the server state:
struct ServerState {
bank: [Gpio; 11],
}
mod server_impl {
wire_weaver::ww_codegen!(
all_gpio_api :: AllGpioApi for super::ServerState,
server = true, no_alloc = true, use_async = true,
method_model = "_=immediate",
property_model = "_=get_set",
introspect = "no_docs",
);
}
Each resource path becomes an async method named after the flattened path, e.g. port_pin_set_output_level,
get_port_pin_pull / set_port_pin_pull (property get/set), or port_pin_event / close_port_pin_event (the stream opened with its sources, closed).
The [bank_index, pin_index] addressing is passed in as index: [UNib32; 2]:
async fn port_pin_set_output_level(
&mut self,
_cx: &mut Context<'_, impl EventOut>,
index: [UNib32; 2],
level: Level,
) -> RpcResult<()> {
let bank_idx = index[0].0 as usize;
let pin_idx = index[1].0 as usize;
let odr = self.bank[bank_idx].odr();
let level = if level == Level::High { Odr::HIGH } else { Odr::LOW };
odr.modify(|o| o.set_odr(pin_idx, level));
Ready(())
}
Which indices are valid is reported through wire_weaver::ValidIndices, driving what port_valid_indices() /
pin_valid_indices() return on the client:
fn valid_indices_root_port(&mut self) -> ValidIndices<'_> {
ValidIndices::range_u32(0..self.bank.len() as u32)
}
fn valid_indices_root_port_pin(&mut self, index: [UNib32; 1]) -> ValidIndices<'_> {
ValidIndices::range_u32(0..16)
}
In this example, capabilities() and configure are hardcoded (3.3V without level control, push-pull/open-drain/input all
supported, no custom modes), set_mode/mode and pull/speed are translated to/from the STM32 MODER /
OTYPER / PUPDR / OSPEEDR registers, and the event stream never opens (port_pin_event answers
Common(Unsupported), one Counted state serves every pin) since the example does not wire up EXTI
interrupts.