Skip to main content

<alp/rpc.h> — Framed RPC over OpenAMP/RPMsg

The customer-facing IPC API on heterogeneous SoMs. <alp/rpc.h> is the only header app code needs to talk between an A-cluster Yocto half and an M-class Zephyr half of a board.yaml v2 project. It sits on top of <alp/mproc.h> (mailbox + shared memory + hardware semaphore) and the build-time-generated <alp/system_ipc.h> macros emitted by scripts/alp_orchestrate.py.

Landed in v0.6. Surface is [ABI-STABLE] — adding optional fields to alp_rpc_config_t is permitted; reshaping callback signatures is not.

When to use this header

Want to…Use this
Talk between two cores running different OSes<alp/rpc.h> (this page)
Raw mailbox + shared memory + semaphore primitives<alp/mproc.h>
Send packets over Ethernet / Wi-Fi to a peer<alp/iot.h>

If you find yourself typing endpoint IDs, carve-out addresses, or mailbox channel numbers by hand, stop — they belong in board.yaml's ipc: block and the generated <alp/system_ipc.h>, not app code.

On-wire framing

Every RPMsg payload carries a tiny ASCII method header followed by an opaque application-defined byte string:

+-----------------+-----------------------------------+
| <method>\0 | application payload (verbatim) |
| 1..32 bytes | 0..N bytes |
+-----------------+-----------------------------------+
  • Method names match the regex [A-Za-z0-9_.-]+ and are ≤ 32 bytes including the NUL terminator.
  • The payload is passed through verbatim to the callback — the SDK does not parse it.
  • v0.7 will add an optional length-prefixed binary / nanopb upgrade reusing the same public API. Both ends of a channel must agree on the framing format.

Backends

SideBackend
Zephyr / M-classsubsys/ipc/ipc_service with the rpmsg backend (src/zephyr/rpc_zephyr.c).
Linux / A-classlibmetal + librpmsg user-space chardev access to /dev/rpmsg* (src/yocto/rpc_yocto.c).
Bare-metalNOSUPPORT stub (call returns NULL with ALP_ERR_NOSUPPORT).

Constants

#define ALP_RPC_METHOD_MAX_LEN   32   /* incl. trailing NUL — on-wire limit */
#define ALP_RPC_DEFAULT_MBOX_CH 0u /* used when alp_rpc_config_t.mbox_ch == 0 */

Types

alp_rpc_channel_t

Opaque RPC channel handle. Allocate via alp_rpc_open(), release via alp_rpc_close(). All API entry points except alp_rpc_open take the handle as their first argument.

alp_rpc_config_t

typedef struct {
const char *name; /* matches ALP_IPC_<NAME>_NAME from <alp/system_ipc.h> */
uint32_t src_ept; /* this side; 0 → FNV-1a hash of name */
uint32_t dst_ept; /* peer side; 0 → src_ept + 1 */
uint32_t mbox_ch; /* defaults to ALP_RPC_DEFAULT_MBOX_CH (0) */
bool cacheable; /* false on V2N (no M cache); true on AEN (M55 cached) */
} alp_rpc_config_t;

Every member except name is optional. Pass a designated-initialiser literal directly to alp_rpc_open() for terse use.

Callback signatures

typedef void (*alp_rpc_msg_cb_t)(const char *method,
const void *payload, size_t len,
void *user);

typedef void (*alp_rpc_method_cb_t)(const void *payload, size_t len,
void *user);

The typed method_cb_t is filtered by alp_rpc_subscribe() so callbacks don't need to re-check the method name.

Callbacks run on the backend's RX worker (Zephyr ipc_service callback thread; Linux per-channel poll thread inside librpmsg). Keep the body short — the worker is shared with every other subscribe on this channel.

Lifecycle

alp_rpc_open()

alp_rpc_channel_t *alp_rpc_open(const alp_rpc_config_t *cfg);

Resolves the carve-out memory region declared in <alp/system_ipc.h>, brings up the OpenAMP virtio queues, and registers the local endpoint. The peer must have called alp_rpc_open() with matching name / src_ept / dst_ept; the resolver retries the name-service handshake transparently for up to 2 s (Zephyr) or 5 s (Linux) before returning NULL.

alp_last_error() valueCause
ALP_ERR_INVALcfg / name NULL, or method-name too long.
ALP_ERR_NOMEMChannel pool exhausted.
ALP_ERR_NOT_READYRPMsg device or memory region not yet up (peer hasn't booted).
ALP_ERR_NOSUPPORTSDK built without CONFIG_ALP_SDK_RPC / no OpenAMP backend.

alp_rpc_close()

void alp_rpc_close(alp_rpc_channel_t *ch);

Drops every subscription, deregisters the local endpoint, releases the handle back to the pool. Outstanding alp_rpc_call() invocations return ALP_ERR_NOT_READY before unblocking. NULL handle is a no-op.

Subscriptions

alp_rpc_subscribe()

alp_status_t alp_rpc_subscribe(alp_rpc_channel_t *ch,
const char *method,
alp_rpc_method_cb_t cb,
void *user);

Registers cb for frames whose method matches. Replaces any prior registration for the same (ch, method) pair. Passing cb == NULL is equivalent to alp_rpc_unsubscribe().

Per-channel subscribe table cap in v0.6: 8 entries.

alp_rpc_unsubscribe()

alp_status_t alp_rpc_unsubscribe(alp_rpc_channel_t *ch, const char *method);

Send + call

alp_rpc_send() — fire-and-forget

alp_status_t alp_rpc_send(alp_rpc_channel_t *ch,
const char *method,
const void *payload, size_t len);

Frames the (method, payload) pair and hands it to the OpenAMP TX queue. Returns as soon as the queue accepts the frame; does not wait for the peer.

alp_rpc_call() — synchronous request/response

alp_status_t alp_rpc_call(alp_rpc_channel_t *ch,
const char *method,
const void *req, size_t req_len,
void *resp, size_t *resp_len,
uint32_t timeout_ms);

Sends (method, req, req_len), blocks until the peer replies with a frame whose method matches OR timeout_ms elapses. The peer replies on the same channel with the same method name; the SDK does not impose a method.reply convention — both sides agree per application.

resp / resp_lenBehaviour
Both NULLCaller doesn't care about the response body.
resp != NULLresp_len must be non-NULL; on entry holds capacity, on exit length.
timeout_ms == UINT32_MAXUnbounded wait.

Concurrent calls on the same channel from multiple threads are serialised by the SDK; the second caller blocks until the first returns or times out.

Linux side ships partial alp_rpc_call() in v0.6 — see src/yocto/rpc_yocto.c. Use alp_rpc_send + alp_rpc_subscribe for now if you need the A-cluster side.

Canonical usage

Producer (M33-SM / Zephyr)

#include <alp/rpc.h>
#include <alp/system_ipc.h> /* generated by west alp-build */
#include <zephyr/kernel.h>

int main(void) {
alp_rpc_channel_t *ch = alp_rpc_open(&(alp_rpc_config_t){
.name = ALP_IPC_ALP_DEFAULT_RPMSG_NAME,
.src_ept = ALP_IPC_ALP_DEFAULT_RPMSG_SRC_EPT,
.dst_ept = ALP_IPC_ALP_DEFAULT_RPMSG_DST_EPT,
.mbox_ch = ALP_IPC_ALP_DEFAULT_RPMSG_MBOX_CH,
});
if (!ch) return -1; /* alp_last_error() reports why */

while (1) {
float c = read_thermistor();
alp_rpc_send(ch, "temperature", &c, sizeof c);
k_msleep(1000);
}
}

Consumer (A55 / Yocto)

#include <alp/rpc.h>
#include <alp/system_ipc.h>

static void on_temperature(const void *payload, size_t len, void *u) {
(void)u;
float c;
if (len == sizeof c) {
memcpy(&c, payload, sizeof c);
printf("[a55] temperature=%.2f C\n", c);
}
}

int main(void) {
alp_rpc_channel_t *ch = alp_rpc_open(&(alp_rpc_config_t){
.name = ALP_IPC_ALP_DEFAULT_RPMSG_NAME,
.src_ept = ALP_IPC_ALP_DEFAULT_RPMSG_DST_EPT, /* swap src/dst */
.dst_ept = ALP_IPC_ALP_DEFAULT_RPMSG_SRC_EPT,
});

alp_rpc_subscribe(ch, "temperature", on_temperature, NULL);
for (;;) pause();
}

Both sides #include the same generated header. Producer's src_ept is consumer's dst_ept — that symmetry is the only piece a developer keeps straight.

Declaring the channel in board.yaml

ipc:
- kind: rpmsg
endpoints: [a55_cluster, m33_sm]
carve_out_kb: 512
name: alp_default_rpmsg

The orchestrator allocates the carve-out from the SoM preset's memory_map: and emits matching ALP_IPC_ALP_DEFAULT_RPMSG_* macros into <alp/system_ipc.h>. Per-channel cache policy follows the SoM:

  • V2N — non-cacheable (M33-SM has no data cache); cacheable: false.
  • AEN — cacheable with auto-generated cache-maintenance points; cacheable: true.

See also

Questions about this page? Discuss in Community Forum