Skip to content

hal::bus

Bus abstraction traits and types.

Three-layer bus model:

  • Layer 1 – PhysicalBus: raw register-level driver (e.g. esp32_spi).
  • Layer 2 – Bus: protocol-level abstraction (e.g. spi_bus::SpiBus).
  • Layer 3 – Logical driver: device driver using BusHandle (e.g. bme280::Bme280).

The BusHandle struct is the bridge between layers 2 and 3: it wraps a &dyn Bus reference and provides delegation methods.

struct BusDevice {
pub name: &'static str,
pub logical_driver: &'static str,
pub bus: &'static str,
pub cs_pin: Option<u8>,
pub bus_speed: BusSpeed,
}

Logical device attached to a bus, declared in the board manifest.

References: BusSpeed

struct BusHandle<'a> { /* private fields */ }

Opaque handle returned to a logical driver.

Wraps a &dyn Bus and offers the small copy-based surface logical drivers were written against — each method builds a [Op] and calls [Bus::transfer]. Logical drivers never see the bus type, and never had to change when the trait moved to a transfer list.

The borrow is 'a, not 'static: every method here takes &self and [Bus::transfer] does too, so nothing the handle does needs the bus to live forever. A driver can therefore be handed a bus that lives only as long as the task’s stack frame — a board no longer has to leak its buses into statics just to satisfy this type. Building one is usually (&bus).into() via the [From] impl below, so a logical driver’s constructor reads Mpu6886::new(&bus) rather than Mpu6886::new(BusHandle::new(&bus)).

Implements: Clone, Copy, From

struct BusMapping {
pub name: &'static str,
pub kind: BusKind,
pub base_addr: u32,
pub irq: u8,
pub dma_capable: bool,
pub dma_pool_bytes: u32,
pub config: BusConfig,
}

Bus mapping entry in the board manifest.

References: BusConfig, BusKind

struct I2cConfig {
pub sda: u8,
pub scl: u8,
pub speed: BusSpeed,
}

Pins and clock for an I2C controller. Default is standard-mode (100 kHz) on pins 0/0.

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

References: BusSpeed

struct Op<'a> {
pub tx: Option<&'a [u8]>,
pub rx: Option<&'a mut [u8]>,
pub word_bits: u8,
pub cs: CsHold,
pub delay_us: u32,
}

One step of a bus transfer list.

[Bus::transfer] takes a slice of these and runs them in order. Both tx and rx set is a full-duplex exchange; tx alone is a write; rx alone is a read. The struct is #[non_exhaustive]: build it with [Op::write], [Op::read], or [Op::exchange] and the builder setters, so later fields (per-op speed, and so on) stay additive.

References: CsHold

struct PeripheralMapping {
pub name: &'static str,
pub base_addr: u32,
pub irq: u8,
pub dma_capable: bool,
pub dma_pool_bytes: u32,
}

Direct (non-bus-attached) peripheral mapping.

struct ServiceMapping {
pub name: &'static str,
pub always: bool,
}

Service task declaration in the board manifest.

struct SpiConfig {
pub mosi: u8,
pub miso: u8,
pub sck: u8,
pub max_speed: BusSpeed,
pub mode: SpiMode,
}

Pins, clock ceiling and mode for a SPI controller.

Default is mode 0 at 1 MHz on pins 0/0/0 — a placeholder to spread with .., not a wiring anyone should run.

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

References: BusSpeed, SpiMode

struct UartConfig {
pub tx: u8,
pub rx: u8,
pub baud: u32,
pub data_bits: UartDataBits,
pub parity: UartParity,
pub stop_bits: UartStopBits,
}

Pins, baud rate and framing for a UART. Default is 115200 8N1 on pins 0/0.

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

References: UartDataBits, UartParity, UartStopBits

enum BusConfig {
Spi(SpiConfig),
I2c(I2cConfig),
Uart(UartConfig),
}

Complete configuration for initialising a peripheral bus.

One variant per bus kind, each wrapping a named config struct so a driver or port type can hold exactly the one it needs ([SpiConfig] on a SPI port, never a UART’s). The const fn helpers cover the common shapes (spi_mode0, i2c, uart_8n1); anything else is built with struct syntax and ..Default::default().

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

References: I2cConfig, SpiConfig, UartConfig

enum BusError {
Timeout,
CrcMismatch,
DeviceNotResponding,
InvalidConfig,
DmaError,
Busy,
}

Bus error enumeration.

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

enum BusKind {
Spi,
I2c,
Uart,
Can,
OneWire,
}

Kind of bus peripheral.

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

enum BusSpeed {
KHz(u32),
MHz(u32),
Standard100k,
Fast400k,
FastPlus1M,
HighSpeed3M4,
}

Bus clock / data-rate selection.

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

enum CsHold {
Release,
Keep,
}

Chip-select behaviour after an [Op] completes.

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

enum SpiMode {
Mode0,
Mode1,
Mode2,
Mode3,
}

SPI clock polarity / phase modes.

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

enum UartDataBits {
Bits5,
Bits6,
Bits7,
Bits8,
}

Number of data bits per UART character.

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

enum UartParity {
None,
Even,
Odd,
}

UART parity setting.

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

enum UartStopBits {
Stop1,
Stop1_5,
Stop2,
}

Number of stop bits.

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

trait Bus: Send + Sync {
fn transfer(&self, ops: &mut [Op<'_>]) -> BusResult<()>
fn max_transfer(&self) -> usize
fn kind(&self) -> BusKind
fn set_speed(&self, _speed: BusSpeed) -> BusResult<()>
}

Implemented by bus abstractions (spi_bus, i2c_bus).

UART is deliberately absent: it is a byte stream, not an addressed bus — see [crate::stream].

These sit above the physical driver and provide a protocol-aware interface. They are linked directly into the calling task — no IPC.

The single transfer entry point is a list of [Op]s run as one logical transaction. The old copy-based write/read/transfer(tx, rx) methods live on [BusHandle] now, as thin wrappers that build a one- or two-element [Op] — so logical drivers keep calling them unchanged.

trait PhysicalBus: PhysicalTransfer {
fn init(&mut self, config: &BusConfig) -> BusResult<()>
}

Implemented by physical driver crates (e.g. esp32_spi).

These have direct hardware register access and run at the lowest level of the driver stack. This trait adds the one construction-time method to [PhysicalTransfer]; once init has run, callers need only the supertrait.

trait PhysicalTransfer: Send + Sync {
fn exchange(&self, tx: &[u8], rx: &mut [u8]) -> BusResult<()>
fn set_speed(&self, _speed: BusSpeed) -> BusResult<()>
}

The run-time half of a physical driver: everything that moves bytes on a peripheral that is already up. Every method takes &self, so a shared reference to a driver is itself a PhysicalTransfer (the blanket impl below) and a bus layer can hold one without owning the driver.

Construction-time work (init, &mut self) lives on [PhysicalBus], which extends this trait; the split is what makes the blanket impl sound.

fn spin_rough_us(us: u32)

Busy-wait approximately us microseconds, for [Op::delay_us].

A stopgap, exactly like soc_esp32::poll: the loop bound is iterations, not time, so its real duration drifts with the CPU clock. It exists so a per-op delay means something today; a faithful microsecond needs the portable cycle counter (doc/plan-arm32.md, Phase 2.4). Conservative by design — at a lower clock it over-waits rather than under-waits.

const REG_BURST_MAX: usize;

Longest burst [BusHandle::read_regs] reads over SPI. The handle stages the exchange in a stack buffer one byte longer than this; 64 matches the SPI controller’s data buffer, so a burst under it is one framed transfer.

type BusResult<T> = Result<T, BusError>;

Result type for bus operations.

References: BusError