Skip to main content

Crate consortium_ipc_transport_uart

Crate consortium_ipc_transport_uart 

Source
Expand description

COBS-framed UART transport for Consortium IPC.

UART is the wired transport category with a Type-B (transport-coupled) doorbell: byte arrival is itself the notification, so there is no Doorbell and no Side — direction is physical.

Wire format, per frame:

COBS( chan: u8 | payload[0..=MTU] | crc32: u32 LE )  0x00

§Why the framing looks like this

A UART hands over bytes, not messages, and it will happily hand over the wrong ones: a peer that reboots mid-frame, a line that glitches at power-up, a baud mismatch. Each layer of the frame answers one of those.

  • COBS removes every 0x00 from the frame body, which reserves 0x00 as a delimiter that cannot occur inside a frame. A receiver that has lost sync only has to hunt for the next zero byte to be aligned again — no length prefix to mistrust, no escape-state machine to get stuck in.
  • CRC32 over chan | payload catches the corruption that framing cannot: flipped bits, a dropped byte, a peer transmitting at the wrong rate.
  • chan carries the Chan contract for serial links. One transport instance owns one logical channel and drops frames stamped with anybody else’s byte.

Frame-level corruption is deliberately not an error: the receiver logs the bad frame, discards it, and keeps waiting for the next delimiter. Error reports only what a caller can act on — IO failure, EOF, an oversized payload, an under-sized buffer.

§No doorbell, no side, no handshake

Byte arrival is the notification, so there is no Doorbell type parameter to supply and no interrupt to forward. Direction is physical — the TX and RX wires — so there is no Side either. Connect is implemented as a documented no-op, which keeps generated bring-up code uniform across transports: it emits the same connect(..) call whether the link underneath is shared memory or a wire.

§Buffers

Like every Consortium transport this one does not allocate; the caller supplies &'static mut [u8] scratch. UART needs two buffers, and their sizes are not the MTU — COBS expansion and the frame accumulator both add overhead. Derive them rather than guessing:

HalfSizeHolds
UartTransportTxtx_buf_len(mtu)frame body, then the encoded frame
UartTransportRxrx_buf_len(mtu)a read chunk, then a frame accumulator

Both constructors verify the buffer against the MTU and return Error::BufferTooSmall rather than trusting the caller.

§Backend selection

The transport is written once, against this crate’s UartRead/UartWrite seam, and the std feature picks which IO traits that seam is blanket-implemented for:

  • without std (firmware) — every embedded_io_async::Read and Write, the async serial traits Embassy and modern HAL UART drivers implement.
  • with std (Linux/host) — every Tokio AsyncRead/AsyncWrite that is Unpin + Send.

The seam exists because under std transport futures must be Send (see consortium_ipc::MaybeSend), and embedded_io_async’s unnameable async fn futures cannot guarantee that generically. Carrying the MaybeSend bounds on the seam, as the FDCAN transport does with CanTx/CanRx, keeps one transport impl for both; only the two small adapters in io differ.

Optional modules supply the concrete IO: unix (unix feature) opens /dev/tty* devices for the Tokio family, and imx9 / stm32mp2 (chip features) re-export a chip HAL’s interrupt-buffered driver for the embedded-io-async family. With no chip feature, any driver implementing the embedded-io-async traits works directly.

§Firmware: interrupt-buffered receive

The chip HALs’ BufferedUart receiver is interrupt-driven: its IRQ handler drains the receive FIFO into a ring so no bytes are lost between the wake and the next poll of the transport’s recv. Per repository convention, this crate does not install #[interrupt] handlers — the application owns the vector table and forwards the UART interrupt into on_buffered_interrupt::<T>, parameterized with the same peripheral singleton the driver took. Enable the HAL’s rt feature for the #[interrupt] macro and PAC vector table.

ⓘ
// `stm32mp25x` feature, plus `consortium-hal-stm32mp2/rt`.
use consortium_ipc::Chan;
use consortium_ipc_transport_uart::{
    UartTransport, rx_buf_len, tx_buf_len,
    stm32mp2::{BufferedUart, Config, on_buffered_interrupt},
};
use consortium_runtime_mcu::static_buf;

const MTU: usize = 256;

// Ring storage for the IRQ handler, plus the transport scratch buffers.
static_buf!(RX_RING, 512);
static_buf!(RX_SCRATCH, rx_buf_len(MTU));
static_buf!(TX_SCRATCH, tx_buf_len(MTU));

// The platform must leave USART2 clocked and pinned out.
let p = consortium_hal_stm32mp2::init();
let uart = BufferedUart::new(p.USART2, KERNEL_HZ, Config::default(), RX_RING.init([0; 512]));
let (tx_wire, rx_wire) = uart.split();

let transport = UartTransport::new(
    Chan::new(0)?,
    rx_wire,
    tx_wire,
    RX_SCRATCH.init([0; rx_buf_len(MTU)]),
    TX_SCRATCH.init([0; tx_buf_len(MTU)]),
    MTU,
)?;
let (tx, rx) = transport.split();

#[interrupt]
fn USART2() {
    on_buffered_interrupt::<consortium_hal_stm32mp2::peripherals::USART2>();
}

The i.MX9 flow is identical with imx9::BufferedUart and an LPUARTn vector.

§Example

ⓘ
use consortium_ipc_transport_uart::{UartTransport, rx_buf_len, tx_buf_len};

const MTU: usize = 256;
static mut TX: [u8; tx_buf_len(MTU)] = [0; tx_buf_len(MTU)];
static mut RX: [u8; rx_buf_len(MTU)] = [0; rx_buf_len(MTU)];

let transport = UartTransport::new(chan, reader, writer, rx_buf, tx_buf, MTU)?;
let (tx, rx) = transport.split();

§Features

Defaults to std. Firmware builds want default-features = false.

  • std — alloc, Tokio IO bounds, Send futures, tracing diagnostics.
  • alloc — link alloc through consortium-ipc.
  • defmt — route diagnostics to defmt and derive defmt::Format on Error (and forward defmt to a selected chip HAL).
  • unix — implies std; adds the unix module (open, pty_pair) over tokio-serial.
  • mimx93 / mimx95 / mimx952 — re-export consortium_hal_imx9::uart as imx9.
  • stm32mp21x / stm32mp23x / stm32mp25x — re-export consortium_hal_stm32mp2::uart as stm32mp2.

§Testing

cargo test -p consortium-ipc-transport-uart                        # Tokio family
cargo test -p consortium-ipc-transport-uart --no-default-features  # embedded-io family
cargo test -p consortium-ipc-transport-uart --features unix        # pseudo-terminals (Linux)

The protocol is one impl, so the first two exercise the same code; run both anyway when touching the protocol or the io adapters, since each adapter is only compiled under its own feature.

Re-exports§

pub use embedded_io_async;
pub use error::Error;
pub use frame::FRAME_OVERHEAD;
pub use frame::max_frame_len;
pub use frame::rx_buf_len;
pub use frame::tx_buf_len;
pub use io::UartRead;
pub use io::UartWrite;
pub use transport::UartTransport;
pub use transport::UartTransportRx;
pub use transport::UartTransportTx;
pub use embedded_io_async;

Modules§

io
The byte-IO seam the transport is written against.

Structs§

UartTransport
Full-duplex UART transport pairing a send and a receive half.
UartTransportRx
Receive half of a UART transport: accumulates wire bytes, deframes and validates them. Pass it to Channel::<Rx, …>.
UartTransportTx
Send half of a UART transport: frames payloads and writes them to the UART TX wire. Pass it to Channel::<Tx, …>.

Enums§

Error
Errors surfaced by UartTransportTx and UartTransportRx.

Constants§

FRAME_OVERHEAD
Bytes of framing around the payload inside the COBS region: the channel byte plus the CRC32 trailer.

Functions§

max_frame_len
Maximum encoded length of one frame on the wire, including the trailing 0x00 delimiter. COBS overhead is one byte per 254 encoded, plus one.
rx_buf_len
Required size of the buffer passed to UartTransportRx::new: a read-chunk region plus an accumulator able to hold one maximally encoded frame (without its delimiter).
tx_buf_len
Required size of the scratch buffer passed to UartTransportTx::new: staging space for the frame body plus the encoded frame.