<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 toalp_rpc_config_tis 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
| Side | Backend |
|---|---|
| Zephyr / M-class | subsys/ipc/ipc_service with the rpmsg backend (src/zephyr/rpc_zephyr.c). |
| Linux / A-class | libmetal + librpmsg user-space chardev access to /dev/rpmsg* (src/yocto/rpc_yocto.c). |
| Bare-metal | NOSUPPORT 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_servicecallback 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() value | Cause |
|---|---|
ALP_ERR_INVAL | cfg / name NULL, or method-name too long. |
ALP_ERR_NOMEM | Channel pool exhausted. |
ALP_ERR_NOT_READY | RPMsg device or memory region not yet up (peer hasn't booted). |
ALP_ERR_NOSUPPORT | SDK 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_len | Behaviour |
|---|---|
Both NULL | Caller doesn't care about the response body. |
resp != NULL | resp_len must be non-NULL; on entry holds capacity, on exit length. |
timeout_ms == UINT32_MAX | Unbounded 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 — seesrc/yocto/rpc_yocto.c. Usealp_rpc_send+alp_rpc_subscribefor 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
- Heterogeneous Builds — full project walkthrough
board.yamlreference —ipc:block schema<alp/mproc.h>— lower-level mailbox + shared memory primitivesrpmsg-v2n— V2N flagship example