Skip to main content

consortium_ipc_transport_fdcan/
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//! Segmenting FDCAN IPC transport.
19//!
20//! This is the second *wired*, Type-B transport in the workspace, after
21//! [`consortium-ipc-transport-uart`]. Type-B means the link itself is the
22//! notification: a frame arriving is the wakeup, so there is no
23//! [`Doorbell`](consortium_ipc::Doorbell) and no `Side` — unlike shared memory,
24//! where both are needed to coordinate two cores over one window.
25//!
26//! # Wire format
27//!
28//! A CAN controller already delimits every frame and protects it with a CRC, so
29//! there is nothing here to frame or checksum — the UART transport's COBS and
30//! CRC32 layers have no counterpart. What CAN lacks is a way to carry a message
31//! longer than one frame, so that is all this crate adds:
32//!
33//! ```text
34//! byte 0: [ kind:2 | low:6 ]
35//!   00 Single   : low = payload length; the whole message follows
36//!   01 First    : low = 0; bytes 1..3 = total length u16 LE, then the head
37//!   10 Continue : low = sequence number, 1,2,3,… mod 64; then the body
38//!   11 reserved
39//! ```
40//!
41//! # Sizing
42//!
43//! One payload byte per frame goes to the header, and a `First` frame spends
44//! two more on the total length:
45//!
46//! | Frame kind | Classic (8 B) | FD (64 B) |
47//! | ---------- | ------------- | --------- |
48//! | `Single`   | 7 B           | 63 B      |
49//! | `First`    | 5 B           | 61 B      |
50//! | `Continue` | 7 B           | 63 B      |
51//!
52//! [`frames_for`] reports what a full-MTU message costs in frames, which is the
53//! number to reach for when budgeting bus time. A 128-byte message is 19
54//! classic frames — about 2.5 ms at 500 kbit/s — or 3 FD frames.
55//!
56//! Segments are padded up to a length the medium can describe ([`padded_len`]).
57//! On classic CAN that is a no-op, since every length `0..=8` is exact. On FD
58//! the DLC jumps 8, 12, 16, 20, 24, 32, 48, 64, so a 10-byte segment goes out
59//! as 12. The receiver ignores the difference — a `Single` carries its own
60//! length and a `Continue` is bounded by the declared total.
61//!
62//! Only the receive half takes a buffer, sized by [`rx_buf_len`]. CAN frames are
63//! fixed-size, so the transmit path stages each one in a stack array and slices
64//! the caller's payload directly; there is no `tx_buf_len` to match the UART
65//! crate's.
66//!
67//! # Identifiers
68//!
69//! [`Chan`](consortium_ipc::Chan) does not go on the wire. CAN frames already
70//! carry an identifier, so each half is bound to one: the transmit half stamps
71//! it into every frame, and the receive half drops anything else. The two
72//! directions **must** use different identifiers, since both halves see every
73//! frame on the bus and an endpoint must not reassemble its own traffic.
74//! [`FdcanTransport::new`] rejects equality with [`Error::SameIdentifier`]
75//! rather than leaving it to be discovered at runtime — a controller running
76//! with loopback or self-reception, as bring-up self-tests use, echoes every
77//! frame straight back.
78//!
79//! Filtering is currently done in software, because neither chip HAL programs
80//! hardware acceptance filters yet. Frames for other identifiers therefore cost
81//! a wakeup; a hardware filter is the natural optimization and needs no change
82//! here.
83//!
84//! # Example
85//!
86//! ```ignore
87//! use consortium_ipc_transport_fdcan::{FdcanTransport, rx_buf_len};
88//! use embedded_can::{Id, StandardId};
89//!
90//! const MTU: usize = 128;
91//! static RX: StaticCell<[u8; rx_buf_len(MTU)]> = StaticCell::new([0; rx_buf_len(MTU)]);
92//!
93//! let (tx, rx) = can.split();
94//! let mut transport = FdcanTransport::new(
95//!     chan,
96//!     Id::Standard(StandardId::new(0x101).unwrap()), // we send here
97//!     Id::Standard(StandardId::new(0x102).unwrap()), // the peer sends here
98//!     tx,
99//!     rx,
100//!     RX.init([0; rx_buf_len(MTU)]),
101//!     MTU,
102//! )?;
103//! transport.connect(()).await?;
104//! let (tx, rx) = transport.split();
105//! ```
106//!
107//! # Backends
108//!
109//! | Crate | Controller |
110//! | ----- | ---------- |
111//! | `consortium-ipc-transport-fdcan-embedded` | Chip HAL drivers for i.MX9 FlexCAN and STM32MP2 FDCAN |
112//! | this crate, no backend | Any type implementing [`CanTx`]/[`CanRx`] |
113//!
114//! # Limitations
115//!
116//! - Classic 8-byte frames. The seam is FD-ready — a frame type reporting
117//!   `MAX_PAYLOAD = 64` needs no change here — but neither chip HAL drives FD
118//!   yet.
119//! - No flow control. A receiver that is not already parked in `recv` can miss
120//!   frames from a burst, and the segmentation layer will report the resulting
121//!   sequence gap and drop that message. The i.MX9 driver's single receive
122//!   mailbox makes this tighter than the STM32MP2's eight-element FIFO.
123//! - One [`Chan`](consortium_ipc::Chan) per transport; no multiplexing.
124//! - Losing exactly a multiple of 64 consecutive frames is invisible to the
125//!   sequence counter. The codec's decode is the backstop.
126//!
127//! # Features
128//!
129//! | Feature | Effect |
130//! | ------- | ------ |
131//! | `std`   | Default. Widens `MaybeSend` to `Send` and routes logging to `tracing`. |
132//! | `alloc` | Implied by `std`; enables `consortium-ipc`'s allocating paths. |
133//! | `defmt` | Routes logging to `defmt` and derives `Format` on [`Error`]. |
134//!
135//! `std` does not select a second set of impls: the
136//! [`CanTx`]/[`CanRx`] seam belongs to this crate, so its futures are declared
137//! `MaybeSend` once and one impl family serves both firmware and host.
138//!
139//! # Testing
140//!
141//! ```bash
142//! cargo test -p consortium-ipc-transport-fdcan
143//! cargo test -p consortium-ipc-transport-fdcan --no-default-features
144//! ```
145//!
146//! Both run the same sources, including the same `loopback` integration test —
147//! which is the evidence for the single-impl-family claim above.
148//!
149//! [`consortium-ipc-transport-uart`]: https://docs.rs/consortium-ipc-transport-uart
150
151pub(crate) mod can;
152pub(crate) mod error;
153pub(crate) mod frame;
154pub(crate) mod transport;
155
156/// Re-exported so callers can name [`Id`](embedded_can::Id) — which every
157/// constructor here takes — without adding their own dependency and risking a
158/// version mismatch on the trait this crate's blanket [`CanFrame`] impl keys on.
159pub use embedded_can;
160
161pub use {
162    can::{CanErrorType, CanFrame, CanRx, CanTx},
163    error::Error,
164    frame::{
165        CLASSIC_PAYLOAD, FD_PAYLOAD, FIRST_EXTRA, HEADER_LEN, MAX_MTU, MIN_FRAME_PAYLOAD,
166        continue_max, first_max, frames_for, padded_len, rx_buf_len, single_max,
167    },
168    transport::{FdcanTransport, FdcanTransportRx, FdcanTransportTx},
169};