GitHub

Docs / API reference / syn_ipc.h

syn_ipc.hv0.3.0

<synaptic/syn_ipc.h> is the inter-core communication API for the MCXN947's dual Cortex-M33 design (CPU0 <-> CPU1). Messages are small fixed-size descriptors that live in shared SRAM, with payloads referenced by offset into the same shared region rather than copied — an inference request from CPU1 points at its staged input tensor, and CPU0 answers with a response pointing at the result. The API was frozen in v0.1.0 and is fully implemented in v0.3.0, unchanged: two lock-free SPSC rings in shared SRAM with MAILBOX interrupt notification, verified on the FRDM-MCXN947 (15 µs typical round-trip). See Dual-core design & IPC for the architecture.

Types & constants

NameKindDescription
syn_ipc_type_tenumIPC message types: SYN_IPC_INFER_REQ / SYN_IPC_INFER_RESP (inference request/response), SYN_IPC_MODEL_LOAD / SYN_IPC_MODEL_UNLOAD (model lifecycle commands), SYN_IPC_STATUS_REQ / SYN_IPC_STATUS_RESP (health/status query and reply).
syn_ipc_msg_tstructIPC message descriptor, laid out for shared SRAM: msg_id (correlates a response with its request), type (a syn_ipc_type_t value stored as uint8_t), priority, payload_len (bytes), payload_offset (payload location as an offset into the shared region), timestamp_us, status (0 or negative errno, meaningful on responses).
syn_ipc_handler_ttypedefvoid (*)(const syn_ipc_msg_t *msg, void *ctx) — callback invoked when a message of a registered type arrives.

Functions

SignatureDescription
int syn_ipc_init(void *shared_base, size_t shared_size)Initialize IPC over a shared-SRAM region. Both cores must pass the same physical region (each in its own address view). CPU0 zeroes and stamps the control block; CPU1 validates the magic, layout version, and ring geometry before attaching. Returns 0 on success, -EALREADY if already initialized, or a negative errno.
int syn_ipc_send(const syn_ipc_msg_t *msg)Post a message to the other core and raise the MAILBOX interrupt. The payload must already be in the shared region at msg->payload_offset. Returns 0 on success, -EAGAIN if the ring is full, or a negative errno.
int syn_ipc_receive(syn_ipc_msg_t *msg, uint32_t timeout_ms)Block up to timeout_ms milliseconds for the next incoming message without a registered handler and copy its descriptor into *msg. Returns 0 on success, -EAGAIN on timeout, or -ENODEV if IPC is not initialized.
int syn_ipc_register_handler(syn_ipc_type_t type, syn_ipc_handler_t handler, void *ctx)Register a callback for one message type as an alternative to polling with syn_ipc_receive(). Handlers run on the core's IPC dispatch thread (not in ISR context), with ctx passed through. Returns 0 on success or a negative errno.

Usage

The CPU1-side (application core) pattern — request an inference from the AI core and wait for the response. This is exactly what the dual_model sample's remote image does:

c
#include <synaptic/syn_ipc.h>

syn_ipc_msg_t req = {
    .msg_id         = 42,
    .type           = SYN_IPC_INFER_REQ,
    .priority       = 2,              /* scheduler priority class     */
    .payload_len    = frame_len,
    .payload_offset = SLOT_OFFSET,    /* frame staged in shared slot  */
};
syn_ipc_msg_t resp;

syn_ipc_init(shared_sram_base, shared_sram_size);
syn_ipc_send(&req);

if (syn_ipc_receive(&resp, 100) == 0 &&
    resp.type == SYN_IPC_INFER_RESP &&
    resp.msg_id == req.msg_id &&
    resp.status == 0) {
    /* result is at shared_sram_base + resp.payload_offset */
}

Notes

Status · Implemented in Phase 3 (v0.3.0), API unchanged since v0.1.0

The frozen header is implemented over two lock-free single-producer/single-consumer rings in shared SRAM (each ring index has exactly one writer core; ordering by data-memory barriers only) with MAILBOX interrupt notification. The MAILBOX ISR wakes a per-core dispatch thread — the sole ring consumer — which runs registered handlers and queues everything else for syn_ipc_receive(). Board-verified on the FRDM-MCXN947: 15 µs typical / 81 µs worst-case round-trip (CPU1-measured), zero message loss across a 1,913-serve soak, and a 10,000-message integrity sweep in the QEMU unit suite. See Dual-core design & IPC for the architecture.

  • The descriptor is a compact, fixed-layout struct with explicit widths so both cores agree on it without serialization; the 20-byte wire format and all field offsets are pinned by unit tests. Keep payloads in the shared region and pass them by payload_offset, never by raw pointer — offsets mean the same thing in both cores' address views.
  • msg_id correlation is the caller's responsibility — echo the request's ID in the response.
  • Handlers run on the IPC dispatch thread, so a long-running handler delays later messages on the same core; keep handlers short or hand off to a work queue.
  • Ring capacity is CONFIG_SYNAPTIC_IPC_RING_SIZE (default 16) messages per direction; a full ring returns -EAGAIN from syn_ipc_send() rather than blocking.
  • ipc_overhead_us in syn_prof_result_t remains reserved; round-trip statistics are instead measured by CPU1 and published through the shared control block, readable via syn ipc status on the CPU0 shell.