Skip to content

hal::pinmux

Pin routing: connecting a peripheral signal to a physical pin.

This is the one hardware operation that has no portable implementation, only a portable contract. Every SoC family solves it differently:

  • ESP32 has a GPIO matrix — nearly any signal can reach nearly any pin, via a routing table, plus a small set of IO_MUX “native” pins that bypass the matrix for lower latency.
  • STM32 has alternate functions — each pad has a fixed, short list of functions and you select one by index. A signal that is not on that pad’s list cannot reach it at all.
  • NXP / i.MX has IOMUXC, a third model again.

A driver that wants SDA on GPIO 21 should not have to know which of those it is talking to, and a board manifest cannot express any of them. So the SoC layer implements [PinMux], the driver calls [PinMux::route], and a request the silicon cannot satisfy comes back as BusError::InvalidConfig rather than as a peripheral quietly wired to nothing.

struct PinConfig {
pub drive: PinDrive,
pub pull: PinPull,
pub input: bool,
}

Everything about a pad that is not “which signal”.

Implements: Clone, Copy, Debug, Default, Eq, PartialEq, StructuralPartialEq

References: PinDrive, PinPull

enum PinDrive {
PushPull,
OpenDrain,
}

How the pad drives its output.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

enum PinPull {
None,
Up,
Down,
}

Internal pull resistor selection.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

enum Signal {
UartTx(u8),
UartRx(u8),
UartCts(u8),
UartRts(u8),
I2cSda(u8),
I2cScl(u8),
SpiMosi(u8),
SpiMiso(u8),
SpiSck(u8),
SpiCs(u8),
PulseOut(u8),
PwmOut(u8),
CanTx,
CanRx,
I2sTxData,
I2sRxData,
}

A peripheral signal that can be routed to a pin.

The u8 is the controller instance, not a pin: Signal::I2cSda(0) is I2C0’s SDA line. Which controllers exist is a property of the SoC, so routing a signal for an instance the chip does not have is an error, not a compile failure — a board manifest is data, and data can be wrong.

Names are the function, not a vendor’s peripheral: a pulse-train output is PulseOut, whatever the SoC calls the block that generates it (the ESP32’s is “RMT”). #[non_exhaustive] because the set of functions a chip family can route is open — a second SoC will add signals this one has no block for, and that must not be a breaking change to the contract crate.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

enum SignalDirection {
Input,
Output,
Bidirectional,
}

Which way a signal flows through the pad.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

trait PinMux {
fn can_route(&self, signal: Signal, pin: u8) -> BusResult<()>
fn route(&self, signal: Signal, pin: u8, config: PinConfig) -> BusResult<()>
fn is_native(&self, signal: Signal, pin: u8) -> bool
}

Connects peripheral signals to pins, in whatever idiom the SoC uses.

Implemented once per SoC family (soc-esp32, soc-stm32f4, …), never per board and never per driver.