RPMsg Between Linux and an MCU: One-Way Messaging, Telemetry, and Request-Response

A common embedded design pairs a Linux-capable application processor with an MCU. Linux handles networking, storage, and the user interface; the MCU handles precise sampling, motor control, or other time-sensitive work. RPMsg (Remote Processor Messaging) provides a message channel between the two processors on supported systems.

This guide shows three useful communication patterns:

  1. Linux sends commands to the MCU without expecting a reply.
  2. The MCU periodically publishes status for Linux to consume.
  3. The MCU publishes status independently while Linux occasionally requests or updates a setting and waits for a matching response.

The examples use a Linux RPMsg client driver and OpenAMP on the MCU. They focus on the message handling; remote-processor startup, endpoint discovery, and board-specific initialization are platform-dependent and omitted.

What RPMsg Provides

RPMsg is a message transport commonly built on shared memory, VirtIO, and one or more notification mechanisms such as mailbox interrupts. The Linux remoteproc framework can load and manage remote firmware; the RPMsg framework creates named channels and routes messages to endpoints.

The transport delivers bounded messages between endpoint addresses. It does not define your application’s command format, guarantee that a command was applied, or provide a request/reply protocol by itself. Those rules belong to your firmware and Linux driver.

LayerResponsibility
remoteprocLoads, starts, stops, and monitors the remote processor when supported by the platform
RPMsgRoutes messages between endpoints over the shared transport
Application protocolDefines message types, field encoding, sequence IDs, errors, timeouts, and compatibility

The MCU commonly uses OpenAMP to implement the remote side. Other firmware stacks may provide their own RPMsg implementation. Exact setup APIs vary by kernel version, SoC vendor, and firmware platform.

Endpoints and Channel Names

Each endpoint has an address. A remote firmware can announce a named service, for example control-service; Linux matches that name to an RPMsg driver. The channel is then associated with the endpoint addresses needed to exchange messages.

For the examples below, assume the MCU announces the control-service channel and the Linux driver binds to it. Name-service discovery and endpoint creation must be supported and configured by both sides. A service name is not a payload format: both processors still need to agree on every message they exchange.

Design a Small Message Protocol First

Use explicit message types and fixed-width fields. One possible protocol is:

MessageDirectionPurpose
SET_OUTPUTLinux to MCUSet a target output; may be fire-and-forget
STATUSMCU to LinuxPublish current state and a monotonically increasing sample counter
GET_CONFIGLinux to MCURequest the current configuration with a sequence ID
SET_CONFIGLinux to MCURequest a configuration update with a sequence ID
CONFIG_REPLYMCU to LinuxReturn the matching sequence ID, result, and optional value

Specify byte order, field widths, maximum payload size, and protocol version. Do not assume C structure layout is a portable wire format: compilers can insert padding, and processors may use different byte order. The snippets use structures to keep the example readable; production code should encode and decode fields explicitly or use a documented serialization format.

RPMsg payload capacity depends on the platform’s buffer configuration. Keep messages small, check the received length before reading fields, and split larger transfers into a separately designed chunked protocol. Do not treat a successful send call as proof that the other processor applied the command.

Pattern 1: Linux Sends a One-Way Command

This is fire-and-forget at the application level: Linux sends a command and does not wait for a reply. It is suitable when the command is noncritical or the MCU’s subsequent status stream can confirm the resulting state. If Linux must know whether the MCU accepted or applied the command, add an acknowledgement or use the request-response pattern below.

Linux side

An RPMsg client driver can send through the endpoint created for its channel. This fragment assumes rpdev is the driver’s bound struct rpmsg_device * and the call runs in a sleepable context, such as a workqueue. In-kernel RPMsg send functions can wait for a transmit buffer; do not call a potentially blocking send from an interrupt or receive callback.

#include <linux/rpmsg.h>
#include <linux/types.h>

enum control_message_type {
    CONTROL_SET_OUTPUT = 1,
};

struct set_output_message {
    __le16 type;
    __le16 version;
    __le32 target_millivolts;
} __packed;

static int send_output_target(struct rpmsg_device *rpdev, u32 millivolts)
{
    const struct set_output_message message = {
        .type = cpu_to_le16(CONTROL_SET_OUTPUT),
        .version = cpu_to_le16(1),
        .target_millivolts = cpu_to_le32(millivolts),
    };

    return rpmsg_send(rpdev->ept, &message, sizeof(message));
}

The Linux kernel headers provide the little-endian conversion helpers. The MCU must decode this exact byte order and field layout rather than assuming its native structure matches.

MCU side

The MCU’s OpenAMP endpoint callback validates the incoming message, decodes it, and queues the work for the control task. Keeping the callback short avoids doing slow control operations while the RPMsg receive path is processing a message.

static int control_rx_callback(struct rpmsg_endpoint *ept,
                               void *data, size_t len,
                               uint32_t src, void *priv)
{
    const uint8_t *bytes = data;

    if (len != 8)
        return RPMSG_SUCCESS;

    if (read_le16(bytes) != CONTROL_SET_OUTPUT)
        return RPMSG_SUCCESS;

    if (read_le16(bytes + 2) != 1)
        return RPMSG_SUCCESS;

    uint32_t target_mv = read_le32(bytes + 4);
    control_queue_output_target(target_mv);
    return RPMSG_SUCCESS;
}

read_le16, read_le32, and control_queue_output_target represent application helpers. Decode by byte offset or use a carefully defined serialization routine; do not cast an untrusted byte buffer directly to a potentially padded C structure.

Pattern 2: MCU Sends Status to Linux

Periodic telemetry is naturally one-way. For a 5 Hz update, the MCU publishes one status message every 200 ms. At 4 Hz, the interval is 250 ms. Use an MCU timer or scheduler task to prepare and send the latest state; do not busy-wait for the interval.

MCU side

Assume control_ept has already been created and the remote endpoint is ready. The status message includes a counter so Linux can detect gaps or stale data.

enum control_message_type {
    CONTROL_SET_OUTPUT = 1,
    CONTROL_STATUS = 2,
};

static uint32_t status_sequence;

static void publish_status(void)
{
    uint8_t message[12];
    uint32_t sequence = status_sequence++;
    uint32_t measured_mv = adc_get_output_millivolts();
    uint32_t flags = read_status_flags();

    write_le16(message, CONTROL_STATUS);
    write_le16(message + 2, 1);
    write_le32(message + 4, sequence);
    write_le32(message + 8, measured_mv);
    send_status_with_flags(&control_ept, message, sizeof(message), flags);
}

static void telemetry_task(void *argument)
{
    for (;;) {
        publish_status();
        delay_ms(200); /* 5 Hz */
    }
}

The snippet reserves flags handling for the application-specific wire format; in a real implementation, include it in the encoded payload and update the message length. Check the return value from OpenAMP’s rpmsg_send() and define what to do when no transmit buffer is available. For replaceable telemetry, it is often better to keep only the latest sample than to build an unbounded queue of old status messages.

Linux side

The Linux driver’s receive callback validates the payload and copies decoded data into driver-owned state. RPMsg callbacks should remain short and nonblocking. If processing requires sleeping, filesystem access, or a reply that might block, queue work and return from the callback first.

static int control_rx_callback(struct rpmsg_device *rpdev,
                               void *data, int len,
                               void *priv, u32 src)
{
    const u8 *bytes = data;
    u32 sequence;
    u32 measured_mv;

    if (len != 12 || get_unaligned_le16(bytes) != CONTROL_STATUS)
        return 0;

    if (get_unaligned_le16(bytes + 2) != 1)
        return 0;

    sequence = get_unaligned_le32(bytes + 4);
    measured_mv = get_unaligned_le32(bytes + 8);
    update_cached_status(sequence, measured_mv);
    return 0;
}

update_cached_status() should be a short, concurrency-safe update. A userspace interface can expose the cached snapshot through an appropriate driver API; avoid creating a separate RPMsg device for every field. Count sequence gaps and track the time of the most recent valid update so the application can distinguish a current reading from a stale one.

Pattern 3: Telemetry Plus Independent Requests and Replies

In a typical product, the MCU keeps sending status every 200 ms regardless of Linux requests. Occasionally Linux asks for a setting or requests an update. This is still bidirectional messaging, not a half-duplex conversation. The status stream and request-response exchange are separate message flows that share the RPMsg transport.

For example:

MCU  -> Linux: STATUS(seq=104, output=11820 mV)
Linux -> MCU:  GET_CONFIG(request_id=27)
MCU  -> Linux: CONFIG_REPLY(request_id=27, limit=12000 mV)
MCU  -> Linux: STATUS(seq=105, output=11840 mV)
Linux -> MCU:  SET_CONFIG(request_id=28, limit=12500 mV)
MCU  -> Linux: CONFIG_REPLY(request_id=28, result=OK)

Status messages do not need to pause while Linux waits for a response. The Linux driver matches each reply using its request ID; the MCU returns that ID in the reply. If multiple requests can be outstanding, keep a pending-request table keyed by ID. If only one request may be active, serialize requests with a mutex and still validate the reply ID.

Define the response semantics

For a settings write, decide what the acknowledgement means:

  • Received: The MCU parsed the message.
  • Accepted: The value passed validation and was queued.
  • Applied: The value took effect in the hardware or control loop.
  • Persisted: The value was saved across a reboot.

Use distinct result codes or message types if callers need to tell these stages apart. An OK reply should not ambiguously mean all four.

Linux request flow

The pattern on Linux is to register a pending request, send from a sleepable context, and wait with a timeout. The receive callback matches the request_id, stores the response, and wakes the waiter; it should not block waiting for itself to finish.

static int set_limit(struct control_device *control, u32 limit_mv)
{
    struct set_config_message request;
    long waited;
    int ret;

    mutex_lock(&control->request_lock);
    reinit_completion(&control->reply_ready);
    control->pending_id++;
    control->reply_id = 0;

    request.type = cpu_to_le16(CONTROL_SET_CONFIG);
    request.version = cpu_to_le16(1);
    request.request_id = cpu_to_le32(control->pending_id);
    request.limit_millivolts = cpu_to_le32(limit_mv);

    ret = rpmsg_send(control->rpdev->ept, &request, sizeof(request));
    if (ret)
        goto out_unlock;

    waited = wait_for_completion_timeout(&control->reply_ready,
                                         msecs_to_jiffies(500));
    if (!waited) {
        ret = -ETIMEDOUT;
        goto out_unlock;
    }

    ret = control->reply_id == control->pending_id ?
          control->reply_result : -EPROTO;

out_unlock:
    mutex_unlock(&control->request_lock);
    return ret;
}

This is a pattern, not a drop-in driver: the driver needs a reply callback that validates the message and request ID before completing reply_ready, and it must handle removal or remote-processor restart while a request is pending. If the caller is userspace, perform this wait in an ioctl or another process-context operation, not in the RPMsg callback. For concurrent requests, replace the single completion with a request table and per-request completions.

MCU reply flow

The MCU callback validates the request, applies or queues the setting according to the product’s control design, and sends a response containing the same ID. A response send can fail if resources are temporarily unavailable, so define a bounded retry or error policy rather than silently dropping important acknowledgements.

static void handle_set_config(const uint8_t *bytes, size_t len)
{
    uint32_t request_id;
    uint32_t limit_mv;
    uint16_t result;

    if (len != 12)
        return;

    request_id = read_le32(bytes + 4);
    limit_mv = read_le32(bytes + 8);
    result = control_validate_and_queue_limit(limit_mv);

    send_config_reply(&control_ept, request_id, result);
}

In a real service, dispatch on the message type before calling the handler, validate the protocol version and range, and include a defined result code in the response. Do not send the reply until the stage promised by that result has completed.

Practical Rules for Reliable Messaging

  • Keep receive callbacks short. Validate, copy or update small state, queue slower work, then return.
  • Bound every wait and queue. RPMsg has finite buffers. Handle send failures, timeouts, and remote restarts explicitly.
  • Choose a telemetry loss policy. For sampled status, newest-value-wins is often appropriate. For alarms or events, add buffering, event IDs, and an explicit overflow strategy.
  • Use sequence IDs. They help correlate replies, detect missed telemetry, reject stale responses, and diagnose restarts.
  • Version the protocol. Reject unsupported versions safely and plan how firmware and Linux are upgraded together.
  • Validate every payload. Check length, type, version, ranges, and state transitions before using received fields.
  • Treat the remote core as a trust boundary. A remote processor can send malformed data; constrain what the Linux driver accepts and expose only the required user-space operations.
  • Do not use RPMsg for hard real-time guarantees. Shared-memory queues and interrupts are useful IPC mechanisms, but scheduling, interrupt latency, and contention affect delivery time. Keep tight control loops on the MCU.

Common Setup Problems

If the Linux RPMsg driver never probes, check that the remote processor starts, its firmware resource table describes the required VirtIO/RPMsg resources, and the firmware announces the channel name expected by the Linux driver’s ID table. Also verify the device tree, reserved memory, mailbox, and vendor-specific remoteproc configuration.

If the channel exists but messages do not arrive, check endpoint addresses, payload lengths, receive callback registration, and whether both processors agree on message encoding. Confirm cache-coherency and shared-memory configuration for the SoC and OpenAMP port. These details are platform-specific; a correct application callback cannot compensate for a broken vring or notification setup.

If messages are lost under load, inspect buffer exhaustion and callback latency. Avoid blocking in receive callbacks, reduce unnecessary telemetry, coalesce replaceable updates, and add counters for send errors, timeouts, sequence gaps, and remote restarts.

Choosing a Pattern

RequirementRecommended design
Linux issues a noncritical action and does not need confirmationOne-way command; use later status to observe its effect where possible
MCU periodically reports current measurementsOne-way MCU-to-Linux telemetry with sequence number and stale-data detection
Linux reads a setting occasionallyRequest plus matching response ID and bounded timeout
Linux changes a setting and needs to know it took effectRequest-response with an explicit acknowledgement stage
MCU reports frequently while Linux sends occasional commandsIndependent telemetry and command flows on the same bidirectional channel

For safety-critical or irreversible actions, fire-and-forget is usually the wrong contract. Use a response that clearly communicates acceptance or completion, and define what Linux should do when the MCU does not respond.

Conclusion

RPMsg is a useful transport between Linux and MCU firmware, but the application protocol determines whether the system is understandable and robust. Start with a small set of message types, explicit encoding, validation, sequence IDs, and a clear failure policy.

Use one-way messages when the sender does not need confirmation. Use periodic MCU-to-Linux telemetry for current state, and keep it independent from occasional Linux requests. For settings that need confirmation, add request IDs, a matching response, and a bounded timeout. Keep real-time control on the MCU and treat RPMsg as the coordination path between processors.

For the broader processor-selection context, see What Makes a Processor Capable of Running Linux?.

References