Skip to main content

consortium_ipc_transport_uart/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2// Copyright 2026 Ethan Wu
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16// SPDX-License-Identifier: Apache-2.0
17
18//! COBS-framed UART transport for Consortium IPC.
19//!
20//! UART is the *wired* transport category with a Type-B (transport-coupled)
21//! doorbell: byte arrival is itself the notification, so there is no
22//! `Doorbell` and no `Side` — direction is physical.
23//!
24//! Wire format, per frame:
25//!
26//! ```text
27//! COBS( chan: u8 | payload[0..=MTU] | crc32: u32 LE )  0x00
28//! ```
29//!
30//! # Why the framing looks like this
31//!
32//! A UART hands over bytes, not messages, and it will happily hand over the wrong
33//! ones: a peer that reboots mid-frame, a line that glitches at power-up, a baud
34//! mismatch. Each layer of the frame answers one of those.
35//!
36//! - **COBS** removes every `0x00` from the frame body, which reserves `0x00` as a
37//!   delimiter that cannot occur inside a frame. A receiver that has lost sync only
38//!   has to hunt for the next zero byte to be aligned again — no length prefix to
39//!   mistrust, no escape-state machine to get stuck in.
40//! - **CRC32** over `chan | payload` catches the corruption that framing cannot:
41//!   flipped bits, a dropped byte, a peer transmitting at the wrong rate.
42//! - **`chan`** carries the [`Chan`](consortium_ipc::Chan) contract for serial
43//!   links. One transport instance owns one logical channel and drops frames
44//!   stamped with anybody else's byte.
45//!
46//! Frame-level corruption is deliberately **not** an error: the receiver logs the
47//! bad frame, discards it, and keeps waiting for the next delimiter. [`Error`]
48//! reports only what a caller can act on — IO failure, EOF, an oversized payload, an
49//! under-sized buffer.
50//!
51//! # No doorbell, no side, no handshake
52//!
53//! Byte arrival *is* the notification, so there is no [`Doorbell`] type parameter to
54//! supply and no interrupt to forward. Direction is physical — the TX and RX wires —
55//! so there is no [`Side`] either. [`Connect`] is implemented as a documented no-op,
56//! which keeps generated bring-up code uniform across transports: it emits the same
57//! `connect(..)` call whether the link underneath is shared memory or a wire.
58//!
59//! [`Doorbell`]: consortium_ipc::Doorbell
60//! [`Side`]: consortium_ipc::Side
61//! [`Connect`]: consortium_ipc::Connect
62//!
63//! # Buffers
64//!
65//! Like every Consortium transport this one does not allocate; the caller supplies
66//! `&'static mut [u8]` scratch. UART needs *two* buffers, and their sizes are not
67//! the MTU — COBS expansion and the frame accumulator both add overhead. Derive them
68//! rather than guessing:
69//!
70//! | Half                 | Size                | Holds                                  |
71//! | -------------------- | ------------------- | -------------------------------------- |
72//! | [`UartTransportTx`]  | [`tx_buf_len(mtu)`] | frame body, then the encoded frame      |
73//! | [`UartTransportRx`]  | [`rx_buf_len(mtu)`] | a read chunk, then a frame accumulator  |
74//!
75//! Both constructors verify the buffer against the MTU and return
76//! [`Error::BufferTooSmall`] rather than trusting the caller.
77//!
78//! [`tx_buf_len(mtu)`]: tx_buf_len
79//! [`rx_buf_len(mtu)`]: rx_buf_len
80//!
81//! # Backend selection
82//!
83//! The transport is written once, against this crate's [`UartRead`]/[`UartWrite`]
84//! seam, and the `std` feature picks which IO traits that seam is
85//! blanket-implemented for:
86//!
87//! - **without `std` (firmware)** — every [`embedded_io_async::Read`] and
88//!   [`Write`](embedded_io_async::Write), the async serial traits Embassy and
89//!   modern HAL UART drivers implement.
90//! - **with `std` (Linux/host)** — every Tokio `AsyncRead`/`AsyncWrite` that is
91//!   `Unpin + Send`.
92//!
93//! The seam exists because under `std` transport futures must be `Send` (see
94//! `consortium_ipc::MaybeSend`), and `embedded_io_async`'s unnameable `async fn`
95//! futures cannot guarantee that generically. Carrying the `MaybeSend` bounds on
96//! the seam, as the FDCAN transport does with `CanTx`/`CanRx`, keeps one transport
97//! impl for both; only the two small adapters in [`io`] differ.
98//!
99//! Optional modules supply the concrete IO: `unix` (`unix` feature) opens
100//! `/dev/tty*` devices for the Tokio family, and `imx9` / `stm32mp2` (chip
101//! features) re-export a chip HAL's interrupt-buffered driver for the
102//! `embedded-io-async` family. With no chip feature, any driver implementing the
103//! `embedded-io-async` traits works directly.
104//!
105//! # Firmware: interrupt-buffered receive
106//!
107//! The chip HALs' `BufferedUart` receiver is interrupt-driven: its IRQ handler
108//! drains the receive FIFO into a ring so no bytes are lost between the wake and
109//! the next poll of the transport's `recv`. Per repository convention, this crate
110//! does **not** install `#[interrupt]` handlers — the application owns the vector
111//! table and forwards the UART interrupt into `on_buffered_interrupt::<T>`,
112//! parameterized with the same peripheral singleton the driver took. Enable the
113//! *HAL's* `rt` feature for the `#[interrupt]` macro and PAC vector table.
114//!
115//! ```ignore
116//! // `stm32mp25x` feature, plus `consortium-hal-stm32mp2/rt`.
117//! use consortium_ipc::Chan;
118//! use consortium_ipc_transport_uart::{
119//!     UartTransport, rx_buf_len, tx_buf_len,
120//!     stm32mp2::{BufferedUart, Config, on_buffered_interrupt},
121//! };
122//! use consortium_runtime_mcu::static_buf;
123//!
124//! const MTU: usize = 256;
125//!
126//! // Ring storage for the IRQ handler, plus the transport scratch buffers.
127//! static_buf!(RX_RING, 512);
128//! static_buf!(RX_SCRATCH, rx_buf_len(MTU));
129//! static_buf!(TX_SCRATCH, tx_buf_len(MTU));
130//!
131//! // The platform must leave USART2 clocked and pinned out.
132//! let p = consortium_hal_stm32mp2::init();
133//! let uart = BufferedUart::new(p.USART2, KERNEL_HZ, Config::default(), RX_RING.init([0; 512]));
134//! let (tx_wire, rx_wire) = uart.split();
135//!
136//! let transport = UartTransport::new(
137//!     Chan::new(0)?,
138//!     rx_wire,
139//!     tx_wire,
140//!     RX_SCRATCH.init([0; rx_buf_len(MTU)]),
141//!     TX_SCRATCH.init([0; tx_buf_len(MTU)]),
142//!     MTU,
143//! )?;
144//! let (tx, rx) = transport.split();
145//!
146//! #[interrupt]
147//! fn USART2() {
148//!     on_buffered_interrupt::<consortium_hal_stm32mp2::peripherals::USART2>();
149//! }
150//! ```
151//!
152//! The i.MX9 flow is identical with `imx9::BufferedUart` and an `LPUARTn` vector.
153//!
154//! # Example
155//!
156//! ```rust,ignore
157//! use consortium_ipc_transport_uart::{UartTransport, rx_buf_len, tx_buf_len};
158//!
159//! const MTU: usize = 256;
160//! static mut TX: [u8; tx_buf_len(MTU)] = [0; tx_buf_len(MTU)];
161//! static mut RX: [u8; rx_buf_len(MTU)] = [0; rx_buf_len(MTU)];
162//!
163//! let transport = UartTransport::new(chan, reader, writer, rx_buf, tx_buf, MTU)?;
164//! let (tx, rx) = transport.split();
165//! ```
166//!
167//! # Features
168//!
169//! Defaults to `std`. Firmware builds want `default-features = false`.
170//!
171//! - `std` — `alloc`, Tokio IO bounds, `Send` futures, `tracing` diagnostics.
172//! - `alloc` — link `alloc` through `consortium-ipc`.
173//! - `defmt` — route diagnostics to `defmt` and derive `defmt::Format` on [`Error`]
174//!   (and forward `defmt` to a selected chip HAL).
175//! - `unix` — implies `std`; adds the `unix` module (`open`, `pty_pair`) over
176//!   `tokio-serial`.
177//! - `mimx93` / `mimx95` / `mimx952` — re-export `consortium_hal_imx9::uart` as `imx9`.
178//! - `stm32mp21x` / `stm32mp23x` / `stm32mp25x` — re-export
179//!   `consortium_hal_stm32mp2::uart` as `stm32mp2`.
180//!
181//! # Testing
182//!
183//! ```bash
184//! cargo test -p consortium-ipc-transport-uart                        # Tokio family
185//! cargo test -p consortium-ipc-transport-uart --no-default-features  # embedded-io family
186//! cargo test -p consortium-ipc-transport-uart --features unix        # pseudo-terminals (Linux)
187//! ```
188//!
189//! The protocol is one impl, so the first two exercise the same code; run both anyway
190//! when touching the protocol or the [`io`] adapters, since each adapter is only
191//! compiled under its own feature.
192
193#[cfg(feature = "alloc")]
194extern crate alloc;
195
196pub(crate) mod error;
197pub(crate) mod frame;
198pub mod io;
199pub(crate) mod transport;
200
201#[cfg(feature = "unix")]
202pub mod unix;
203
204pub use {
205    embedded_io_async,
206    error::Error,
207    frame::{FRAME_OVERHEAD, max_frame_len, rx_buf_len, tx_buf_len},
208    io::{UartRead, UartWrite},
209    transport::{UartTransport, UartTransportRx, UartTransportTx},
210};
211
212/// i.MX9 LPUART driver, including the interrupt-buffered `BufferedUart`.
213#[cfg(any(feature = "mimx93", feature = "mimx95", feature = "mimx952"))]
214pub mod imx9 {
215    pub use consortium_hal_imx9::uart::*;
216}
217
218/// STM32MP2 USART driver, including the interrupt-buffered `BufferedUart`.
219#[cfg(any(feature = "stm32mp21x", feature = "stm32mp23x", feature = "stm32mp25x"))]
220pub mod stm32mp2 {
221    pub use consortium_hal_stm32mp2::uart::*;
222}