Skip to main content

Crate consortium_ipc_transport_memory

Crate consortium_ipc_transport_memory 

Source
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:

Sideconnect does
Primarypublish the descriptor, flush, ring, await the ack
Secondaryawait/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_CONSUMED

FLAG_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 at Connect::connect instead 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). send never sets it, send_acked always 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. send reports 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 send in flight blocks an acknowledged one. An unacknowledged message frees its slot silently, so while one is outstanding there is no edge to wait for and send_acked returns Error::NoChannel — the same answer send would 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_CONSUMED gets Error::NoChannel rather than queueing. Backpressure is the caller’s to handle, or delegated to send_acked on a channel wired for it.
  • At most CHANNEL_NUM_MAX channels per descriptor.
  • new is unsafe. 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 a mmap of /dev/uioN; on firmware from a linker-placed symbol. consortium-cfg validates the bounds, overlap, and MPU alignment against Consortium.toml before 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.
  • connect has no timeout, per the Connect contract. Wrap it in tokio::time::timeout or embassy_time::with_timeout for bounded bring-up.

§Features

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

FeatureEffect
stdalloc, Tokio support, UIO errors, tracing logs
alloclink alloc through consortium-ipc
defmtroute 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 path

Coverage 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§

ChannelLayout
Channel descriptor written to the descriptor table.
Descriptor
DescriptorLayout
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.
EndpointState
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.
SharedMemoryTransport
A full-duplex shared-memory transport: both directions over one window.
SharedMemoryTransportRx
Receive-only half produced by SharedMemoryTransport::split.
SharedMemoryTransportTx
Send-only half produced by SharedMemoryTransport::split.
Transceiver
Constructors for typed shared-memory transceivers.

Enums§

ChannelDirection
Absolute direction of a channel’s slot, expressed in terms of the two processor sides.
ConnectError
Descriptor-handshake failure with the doorbell-specific payload erased.
DescState
DescriptorError
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.
PeripheralType
Marker trait for peripheral types.
RegionSharedMemoryTransportable
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_channels complete slots.
set_state⚠
Set the descriptor state field.
slot_offset
Byte offset of slot index in 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.
InstanceTransport
The unsplit transport stored for instance T.