Expand description
Shared-memory transport for Consortium IPC: the CIPC descriptor, its channel
table, the slot protocol, and SharedMemoryTransport.
This is the primary Consortium link — the one an A-core and an M-core on the same
SoC use. It implements consortium-ipc’s SendTransport, RecvTransport, and
Connect over a window of memory both cores can address, using a
Doorbell for wakeups because shared memory on its own cannot notify anybody.
Both primitives come from consortium-core: memory (Region and its cache
semantics, the descriptor header) and signaling (the Doorbell trait, the
Type-A bells in consortium_core::bell (IPCC, MU, HSEM), and the GPIO doorbell).
What this crate adds is the protocol.
§Layout
One window holds everything. A fixed-size descriptor at offset 0 describes a channel table, and each table entry points at a slot elsewhere in the same window:
0x00 ┌────────────────────────────────┐
│ header: "CIPC", version │ consortium-core
│ num_channels, updated ids │
│ tx/rx_transfer_size │
│ state (DescState) │
0x20 ├────────────────────────────────┤
│ channel table: N × 12 bytes │ offset, size, direction
├────────────────────────────────┤
│ cache-line padding │
├────────────────────────────────┤
│ slot 0 [len|flags|crc32|data] │ ← one direction, aligned
│ padding to cache-line boundary │
│ slot 1 [len|flags|crc32|data] │ ← the other, aligned
└────────────────────────────────┘Descriptor and ChannelLayout give the exact field offsets, slot the
slot header. Channel directions in the table are absolute
(ChannelDirection::PrimaryToSecondary / SecondaryToPrimary), and each
side resolves them to TX or RX through its own Side type parameter. One
descriptor therefore describes the link identically from both ends, and neither
end needs a runtime branch on “am I the primary”.
§Protocol
Bring-up is Connect, implemented once per Side:
| Side | connect does |
|---|---|
Primary | publish the descriptor, flush, ring, await the ack |
Secondary | await/observe the descriptor, validate it, flush, ring the ack |
The header magic is the readiness gate: the primary writes it last, so a
secondary that sees CIPC knows the whole descriptor landed. That makes the
doorbell an optimization rather than a requirement — a secondary that boots late
finds the descriptor already published and skips the wait.
DescriptorLayout is the negotiated part, and only that: num_channels plus a
proposed mtu. Everything identifying the link — channel ids, doorbell, region,
base — is fixed at construction. mtu is a proposal; after connect returns,
both sides report the resulting slot capacity through max_send_size /
max_recv_size, which is what send enforces.
Steady state is the slot protocol, and its ordering is the whole design:
send: pick a consumed TX slot → write payload → len → crc32 → FLAG_VALID
→ region.flush() → doorbell.ring()
recv: doorbell.wait() → region.invalidate()
→ check FLAG_VALID → read len → copy payload → verify crc32
→ FLAG_CONSUMEDFLAG_VALID is written last on the sending side and read first on the receiving
side, so a peer either sees a complete frame or no frame — never a half-written
one. flush precedes every ring and invalidate follows every wake, which is
what makes a CachedRegion link work; on an UncachedRegion both compile away.
A CRC32 over the payload catches what ordering cannot: a truncated DMA, a
misconfigured MPU window, a peer writing with the wrong cache attributes. A
mismatch sets FLAG_INTEGRITY_ERROR in the slot and reports no message rather
than handing up corrupt bytes.
§Example
new is unsafe: the caller guarantees region covers the descriptor, channel table,
and every slot, and that nothing else aliases the window.
use consortium_ipc::{Connect, Secondary};
use consortium_ipc_transport_memory::{DescriptorLayout, SharedMemoryTransport};
let mut transport = unsafe {
SharedMemoryTransport::<Secondary, _, _>::new(ch_tx, ch_rx, doorbell, region)
};
// Same call on both sides; Side selects the protocol role.
transport.connect(DescriptorLayout::new(2, 256)).await?;
let (tx, rx) = transport.split();split yields
SharedMemoryTransportTx and SharedMemoryTransportRx, each carrying one
channel id and a clone of the doorbell and region handles, so a Tx and an Rx
Channel can be driven concurrently over one window.
§Acknowledged sends
send is fire-and-forget: it writes a slot, rings, and returns. That stays the
default on every channel. A channel built with new_acked also offers
send_acked, which resolves only once the peer has drained that slot — and which
reports Error::PeerIntegrityError when the peer took the message and rejected its
CRC, something a plain send can never tell you.
Two separate flags carry this, at two levels, because they answer different questions:
- Is acknowledgement wired on this link? A capability, agreed once in the
descriptor’s channel entry (
FLAG_CHAN_ACK_CAPABLE) so a half-configured pair fails atConnect::connectinstead of hanging on the first acknowledged send. - Does this message want acknowledging? The sender’s per-call decision, recorded
in the message’s own slot header (
slot::FLAG_ACK_REQUESTED).sendnever sets it,send_ackedalways does, so a capable channel taxes only the messages that ask.
Acknowledgement gets its own pair of doorbell lines, one per core, rather than reusing the data lines. Each line carries exactly one waiter — the receive half is already parked on the data line, and the hardware doorbells hold one waker per line, arm and disarm it unrefcounted, and consume the status on the first poll — so an acknowledgement arriving there would have nowhere to land.
Three things to know before using it:
- Two peers both awaiting an acknowledgement, with neither draining the other
direction, deadlock. The acknowledgement is produced by the peer’s receive path,
so that path has to be running.
sendreports instead of waiting, which is why it remains the default for lockstep single-task designs. - Delivery is at-least-once; acknowledgement is not. Dropping the future after the ring loses the confirmation, not the message.
- A plain
sendin flight blocks an acknowledged one. An unacknowledged message frees its slot silently, so while one is outstanding there is no edge to wait for andsend_ackedreturnsError::NoChannel— the same answersendwould give, with the retry left to the caller. Hanging would be the alternative.
§Considerations and limitations
- Depth is one frame per direction. A slot holds a single message, so a
sender whose peer has not set
FLAG_CONSUMEDgetsError::NoChannelrather than queueing. Backpressure is the caller’s to handle, or delegated tosend_ackedon a channel wired for it. - At most
CHANNEL_NUM_MAXchannels per descriptor. newisunsafe. The caller guarantees the window covers the descriptor, channel table, and every slot, and that nothing else aliases it. On Linux the pointer comes from ammapof/dev/uioN; on firmware from a linker-placed symbol.consortium-cfgvalidates the bounds, overlap, and MPU alignment againstConsortium.tomlbefore either is generated.- The descriptor is not a security boundary. CRC32 detects accident, not tampering; a malicious peer sharing the window is outside the threat model.
connecthas no timeout, per theConnectcontract. Wrap it intokio::time::timeoutorembassy_time::with_timeoutfor bounded bring-up.
§Features
Defaults to std. Firmware builds want default-features = false.
| Feature | Effect |
|---|---|
std | alloc, Tokio support, UIO errors, tracing logs |
alloc | link alloc through consortium-ipc |
defmt | route protocol tracing to defmt; derive Format |
The transport is generic over any Doorbell and has no chip features. The
Type-A bells and their chip HAL are selected on consortium-core instead
(mimx93/mimx95, stm32mp21x/23x/25x).
§Testing
Tests under tests/ run the real protocol against heap-backed regions and fake
doorbells, so they need no hardware:
cargo test -p consortium-ipc-transport-memory
cargo test -p consortium-ipc-transport-memory --no-default-features \
--test async_atomic_waker_test # no_std-shaped future pathCoverage spans sync descriptor layout, Tokio futures, split halves, codec-backed channels, Embassy-style polling, and an end-to-end primary/secondary exchange. Prefer extending these before requiring hardware.
Re-exports§
pub use endpoint::ConnectError;pub use endpoint::Connected;pub use endpoint::Endpoint;pub use endpoint::EndpointState;pub use endpoint::Instance;pub use endpoint::InstanceTransport;pub use endpoint::Transceiver;
Modules§
- slot
- Low-level slot layout operations over the DMA-coherent shared region.
Macros§
- channels
- Declare the channel singleton markers and their once-only bundle.
- impl_
channel - Bind one generated channel marker to its endpoint types and storage.
Structs§
- Channel
Layout - Channel descriptor written to the descriptor table.
- Descriptor
- Descriptor
Layout - The descriptor geometry the two ends of a shared-memory link agree on during the handshake.
- Endpoint
- A constructed but not yet connected shared-memory endpoint.
- Endpoint
State - Take-once storage for one generated endpoint instance.
- Peri
- An exclusive reference to a peripheral.
- Primary
- The first processor side.
- Secondary
- The second processor side.
- Shared
Memory Transport - A full-duplex shared-memory transport: both directions over one window.
- Shared
Memory Transport Rx - Receive-only half produced by
SharedMemoryTransport::split. - Shared
Memory Transport Tx - Send-only half produced by
SharedMemoryTransport::split. - Transceiver
- Constructors for typed shared-memory transceivers.
Enums§
- Channel
Direction - Absolute direction of a channel’s slot, expressed in terms of the two processor sides.
- Connect
Error - Descriptor-handshake failure with the doorbell-specific payload erased.
- Desc
State - Descriptor
Error - Error
Constants§
- CHANNEL_
NUM_ MAX - DESCRIPTOR_
LENGTH - FLAG_
CHAN_ ACK_ CAPABLE - Channel-entry flag: acknowledgement doorbell lines are wired for this channel.
- MAGIC
- RX_
UPDATED_ ID_ OFFSET - SLOT_
ALIGNMENT - Cache-line boundary used to isolate slots with different writers.
- VERSION
Traits§
- Instance
- A generated shared-memory channel instance.
- Peripheral
Type - Marker trait for peripheral types.
- Region
Shared Memory Transportable - Side
- A processor-side marker selecting which register view a doorbell drives.
Functions§
- channel_
table_ entry_ offset - descriptor_
complete ⚠ - Read the A-core updated max transfer size from the descriptor.
- get_
state ⚠ - Read the descriptor state field.
- required_
region_ len - Minimum shared-window length containing
num_channelscomplete slots. - set_
state ⚠ - Set the descriptor state field.
- slot_
offset - Byte offset of slot
indexin the shared-memory window. - slot_
stride - Number of bytes reserved for one slot, including cache-line padding.
Type Aliases§
- Connected
- The connected typed transceiver produced for instance
T. - Instance
Transport - The unsplit transport stored for instance
T.