consortium-ipc
Core inter-processor communication primitives for Consortium: the trait stack that every transport, doorbell, and codec plugs into, plus the typed endpoints that application and firmware code actually calls.
This is the abstraction crate. It contains no hardware access, no allocation, and
no runtime. Concrete links live in sibling crates
(consortium-ipc-transport-memory, consortium-ipc-transport-uart*), concrete
signaling lives in the consortium-ipc-doorbell-* crates, and concrete
serialization lives in consortium-codec.
Architecture
Six traits compose into two user-facing types. Read the stack bottom-up: bytes and signals at the bottom, typed messages at the top.
┌──────────────────────────────┐
application ───► │ Transceiver<TyTx, TyRx, ...> │ paired typed endpoint
└──────────────┬───────────────┘
┌──────────────┴───────────────┐
│ Channel<D, T, Tr, C> │ one typed direction
└───────┬──────────────┬───────┘
CodecFor<T> ──┘ └── SendTransport / RecvTransport
(encode/decode) (bytes in/out)
│
Doorbell
(wake the peer)
| Item | Responsibility |
|---|---|
Doorbell |
Data-free wake signal to a remote core; ring / wait / pending. |
SendTransport |
Byte send half. Writes bytes, rings the doorbell, reports max_send_size. |
RecvTransport |
Byte receive half. Awaits the doorbell, reads bytes, reports max_recv_size. |
Transport |
Marker for full-duplex links: SendTransport + RecvTransport. |
Connect |
One-shot readiness rendezvous with the peer, run before any traffic. |
CodecFor<T> |
Serialization family for a message type (from consortium-codec). |
Channel<D, T, Tr, C> |
Typed, single-direction endpoint over one transport half. |
Transceiver |
Typed bidirectional endpoint pairing a Tx and an Rx channel. |
Supporting types:
Chan— a validated channel identifier. Its numeric meaning is transport-specific (an HSEM gate, an MU register index, a UART frame header byte), so validation is delegated to a per-platformChanValidateimpl.Side(Primary/Secondary/Tertiary) — which register view of a multi-ported peripheral a driver drives. In this workspacePrimaryis the Linux/A-core convention andSecondarythe firmware convention.Tx/Rx— direction markers, sealed via theDirectiontrait.IpcSafe— marker for types whose bytes mean the same thing on both cores.MaybeSend—Sendunderstd, vacuous underno_std, so one trait definition serves Tokio and Embassy.
No allocation, caller-owned buffers
Channel never allocates. The caller hands it a &'static mut [u8] scratch buffer
sized for the transport MTU at construction. On firmware that is typically a
static mut array (see consortium_runtime_mcu::static_buf!); on Linux it is a
leaked Box. This is what lets the same Channel type serve both sides of a link.
Bring-up: Connect
Connect is deliberately narrow. It means exactly "wait until the far side is
ready", which is why it is async, carries no timeout (the caller owns the
deadline), and is called once — after construction, before any send/recv, and
before splitting a full-duplex transport into halves.
Side asymmetry lives in the type, not in a runtime branch: SharedMemoryTransport
implements Connect per Side (the primary publishes the descriptor and waits for
the ack; the secondary validates and acks), so generated bring-up code emits one
connect(..) call with no if side == ... The UART transport implements it as a
documented no-op, because byte arrival is its own notification.
Local bring-up is not Connect. Peripherals keep constructor + config +
typestate, with their ordering emitted into consortium-cfg's generated init();
doorbells keep bind_notifier / bind_service.
Usage
use PostcardCodec;
use ;
// 1. Build the transport (shared memory, UART, ...) and hand it its doorbell.
let mut transport = /* SharedMemoryTransport::<Secondary, _, _>::new(..) */;
// 2. Rendezvous with the peer, once, before any traffic.
transport.connect.await?;
// 3. Split into halves and wrap each in a typed channel.
let = transport.split;
let tx = new;
let rx = new;
// 4. Talk.
let mut link = new;
link.send.await?;
let reply = link.recv.await?;
recv returns a ReceivedMessage, not a bare T. Owned codecs such as
PostcardCodec set Decoded<'buf> = T and Deref straight through; zero-copy
codecs set Decoded<'buf> = &'buf T::Archived and the '_ lifetime already
prevents a second recv while the handle is live. Writing against
ReceivedMessage from the start means switching codecs does not change call sites.
Message types and IpcSafe
IpcSafe rejects address-space-local constructs — raw pointers, references,
function pointers, usize, and isize — because their values are meaningless on
the other side of the link. Use the derive, which walks the whole field type tree
(so Vec<usize> and Option<*const u8> are rejected too):
use IpcSafe;
A hand-written impl is unsafe, so out-of-derive implementors must acknowledge
the ABI contract explicitly.
Implementing a transport
- Implement
TransportErrorto name your error type. - Implement
SendTransportand/orRecvTransport. Both returnimpl Future<..> + MaybeSend(RPITIT), so a plainasync fnworks — including one that awaits an unnameable future such asembedded_io_async::Write::write. ImplementTransportas a marker if the link is full-duplex. - Implement
Connectif a peer must agree before traffic flows; useParams = ()when there is nothing to negotiate. - Report honest
max_send_size/max_recv_size— callers size their scratch buffers from these. - If the link needs a channel-id namespace, implement
ChanValidatefor a platform marker type soChan::new::<P>(id)rejects out-of-range ids.
Doorbell implementors additionally owe the ordering contract on
Doorbell::ring: writes performed before ring must happen-before the peer's
wait completion or pending observation. For software pending bits that is a
release/acquire pair; for hardware doorbells the signal must not be observable
before the shared-memory writes it publishes.
Features
| Feature | Effect |
|---|---|
| (none) | no_std, no allocation. The default firmware shape. |
alloc |
Links alloc for downstream use. |
futures |
Opts in to futures-oriented helpers. |
tracing |
Routes internal logging through consortium-log's tracing backend. |
defmt |
Routes internal logging through defmt and derives defmt::Format. |
std |
alloc + futures + tracing, and makes MaybeSend require Send. |
Enabling std is what makes channel futures Send, which Tokio's multi-threaded
executor requires. Leave it off for Embassy.
Testing
trybuild cases cover IpcSafe derive rejections.
License
Apache-2.0. See LICENSE.