Timeline
Timeline
2026-06-27
init
This article introduces the bus abstraction mechanism of Rpmsg Core in Linux 5.10.23 and its backend access methods, and discusses in detail the device-driver matching scoring rules and device probe process. This article summarizes the core logic in the probe process, such as power domain association, automatic Endpoint creation, and dynamic address allocation, revealing the implementation details of the rpmsg framework in hardware power management and endpoint communication.
linux 5.10.23

rpmsg is a bus abstraction that is not bound to any specific transport. virtio is currently the most common implementation in Linux, but theoretically there can be:
- Direct mapping implementation based on shared memory
- Implementation based on mailbox interrupts
- Implementation based on PCIe doorbell
Each backend only needs to provide its ownrpmsg_endpoint_opsandrpmsg_device_ops, to access the rpmsg core. And not all backends need to support all send variants. For example:
- The simplest backend only needs to implement send and trysend.
- If the backend does not support explicit specification of src/dst (offchannel), it may not support
sendto/send_offchannel - If the backend does not support poll, userspace write operations are still available (blocking/non-blocking mode is guaranteed by the
rpmsg_send/rpmsg_trysendsemantics).
module_init/module_exit
1 | static int __init rpmsg_init(void) |
rpmsg_initfunction usespostcore_initcall, callingbus_registerregister a bus
1 | // include/linux/init.h |
struct bust_type rpmsg_bus
struct bus_type rpmsg_busdefined as follows:
1 | static struct bus_type rpmsg_bus = { |
rpmsg_dev_match
1 | /* match rpmsg channel and rpmsg driver */ |
If
rpdev->driver_overridethen only needs to comparerpdev->driver_overrideanddrv->name, i.e., specifying that therpdevforce matching a driver with a corresponding nameIf
rpdev->id_talbeexistsfrom
struct rpmsg_driver *rpdrvtake out fromconst struct rpmsg_device_id *idsThen, iterate through the ids table, throughids[i].nameMatchrpmsg_device, return 1 if the match is successful,otherwise call
of_driver_match_deviceMatchstruct device *devandstruct device_driver *drv
of_driver_match_device's matching is done throughstruct device_driver *drvin thedrv->of_match_tableandstruct device *devin thedev->of_nodeas a parameter, calling__of_match_nodeMatch:
1 | static |
Through the__of_device_is_compatibleCalculate the score, find the one with the highest scoreconst struct of_device_id *best_match, while__of_device_is_compatibledefined as follows:
1 | /** |
| Priority | Driving constraint combination | Matching detail conditions | Score calculation formula | Final score example |
|---|---|---|---|---|
| 1 | specificcompat&&type&&name | compatMatch andindex=0;typeMatch;nameMatch | (Highest score) | |
| 2 | specificcompat&&type | compatMatch andindex=0;typeMatch; nonenameConstraint | ||
| 3 | specificcompat&&name | compatMatch andindex=0; nonetypeConstraint;nameMatch | ||
| 4 | specificcompat | compatMatch andindex=0; nonetypeandnameConstraint | ||
| 5 | generalcompat&&type&&name | compatMatch andindex=1;typeMatch;nameMatch | ||
| 6 | generalcompat&&type | compatMatch andindex=1;typeMatch; nonenameConstraint | ||
| 7 | generalcompat&&name | compatMatch andindex=1; nonetypeConstraint;nameMatch | ||
| 8 | generalcompat | compatMatch andindex=1; nonetypeandnameConstraint | ||
| — | More generalized compat… | compatMatch andindex=2(later compatible string) | Continue to decrease as index increases | |
| 9 | type&&name | nonecompatConstraint;typeMatch;nameMatch | ||
| 10 | type | nonecompatConstraint;typeMatch; nonenameConstraint | ||
| 11 | name | nonecompatConstraint; nonetypeConstraint;nameMatch | (Lowest Significant Score) | |
| — | No match / Eliminated | Any constraint specified by the driver was not found in the node | Return directly0 |
rpmsg_dev_probe
1 | /* |
- Power domain association
1 | err = dev_pm_domain_attach(dev, true); |
Function: Associate the device with a Power Management Domain (genpd).
- In modern SoCs, different peripherals may belong to different power domains and can be independently powered on/off
dev_pm_domain_attach(dev, true)A true value indicates: if the device tree specifies the device’spower-domainsproperty, the kernel will try to automatically attach- If attach fails (e.g., the power domain does not exist), subsequent initialization is meaningless, exit directly
Placed first because subsequent endpoint creation and driver initialization may all depend on the hardware power being on. If the power domain is not ready, these operations may fail or even cause hardware exceptions.
- Auto-create Endpoint
1 | if (rpdrv->callback) { |
First, determineif (rpdrv->callback)
- Simple driver: only needs one receive callback, provides callback during registration, the framework automatically creates an endpoint for it
- Complex driver: may require multiple endpoints, dynamic address management, the callback for such drivers may be NULL, they will manually call it in their own probe
rpmsg_create_ept(),
Then constructstruct rpmsg_channel_info chipinfo
1 | strncpy(chinfo.name, rpdev->id.name, RPMSG_NAME_SIZE); // Service name |
Note
dst = RPMSG_ADDR_ANYMeaning: when the endpoint is created, it does not bind to a fixed peer address, and can accept messages from any remote address.
Callrpmsg_create_ept()The parameters passed in arestruct rpmsg_device *rpdev,rpdrv->callback,priv = NULL,struct rpmsg_channel_info chinfo, after creating the ept, the key assignment:rpdev->src = ept->addr, this is a very critical operation!
- If before creation
rpdev->src = RPMSG_ADDR_ANY, the backend will dynamically assign an available address ept->addris the actual local address assigned by the backend- store
ept->addrWrite backrpdev->src, ensuring the device structure records the real address
This means: the driver’s receive callback will be bound to this newly allocated address, and messages sent from the remote to this address will trigger the callback.
- Call
rpdrvofprobefunction
1 | err = rpdrv->probe(rpdev); |
Callstruct rpmsg_driver *rpdrv's probe function
- announce_create
1 | if (ept && rpdev->ops->announce_create) { |
Ifstruct rpmsg_device *rpdevofconst struct rpmsg_device_ops *ops;ofannounce_createis set, then call thisannounce_create, that is, call it after the ept is createdannounce_createthis callback
rpmsg_dev_remove
1 | static int rpmsg_dev_remove(struct device *dev) |
The remove function and probe function are in reverse order, firstannounce_destroy, and then callstruct rpmsg_driver *rpdrvthe remove function in, then calldev_pm_domain_detachseparate the power domain, and finally callrpmsg_destroy_eptDestroyrpdev->ept
rpmsg_uevent
1 | static int rpmsg_uevent(struct device *dev, struct kobj_uevent_env *env) |
This function is the callback when an RPMSG bus device generates a uevent (userspace event/hotplug event). It is the bridge connecting the kernel device model and userspace udev. Function location and call chain:
1 | 设备注册到 rpmsg_bus |
The uevent carries a set of environment variables to userspace, and udev decides based on these variables to:
- Create device node
- Automatically load driver modules
- Execute rule scripts
Line-by-line code analysis
- First priority: Device tree format modalias
1 | ret = of_device_uevent_modalias(dev, env); |
If the device is associated with a device tree node (dev->of_node),of_device_uevent_modaliasit will:
- Read the compatible property of the device tree
- Generate the standard OF modalias format:
of:N<name>T<type>C<compatible> - Add to uevent environment variables
Meaning of the return value:
0: Successfully added OF modalias, return directly-ENODEV: Device has no device tree node, continue with RPMSG’s own logic
2. **Second priority: RPMSG custom modalias**
1 | return add_uevent_var(env, "MODALIAS=" RPMSG_DEVICE_MODALIAS_FMT, |
If the device has no device tree node, RPMSG generates its own modalias:
RPMSG_DEVICE_MODALIAS_FMTDefinition (ininclude/linux/rpmsg.hormod_devicetable.h):
1 |
Example of the final generated uevent environment variable:MODALIAS=rpmsg:rpmsg-tty
- Complete uevent output example
When a new RPMSG device is registered, the uevent might look like this:
1 | ACTION=add |
How does udev use modalias?
- Automatically loading driver modules: udev rules usually contain:
1 | # /lib/udev/rules.d/80-drivers.rules |
When the uevent carriesMODALIAS=rpmsg:rpmsg-tty, udev will execute:modprobe rpmsg:rpmsg-tty
But modprobe doesn’t recognize the colon format; the module itself needs to match via an alias.
- Alias declaration in the driver module: In the driver source code:
1 | static struct rpmsg_device_id rpmsg_tty_id_table[] = { |
After compilation, the module file will contain alias information:
1 | $ modinfo rpmsg_tty |
Note:
MODULE_DEVICE_TABLEThe macro generates at compile time mod_rpmsg… symbols, depmod will write them into/lib/modules/$(uname -r)/modules.alias。
udev→modprobethe complete chain of
Kernel:rpmsg_uevent()
│
▼ Generate
MODALIAS=rpmsg:rpmsg-tty
│
▼ vianetlink/socketsent to userspaceudevdReceiveuevent
│
▼ Parse environment variablesMODALIAS=rpmsg:rpmsg-tty
│
▼ Execute rulesmodprobe rpmsg:rpmsg-tty
│
▼ Match/lib/modules/.../modules.alias
Foundrpmsg_tty.ko
│
▼insmod rpmsg_tty.ko
This way, as soon as the RPMSG channel is created, the corresponding driver module can be loaded automatically without the user needing to manually modprobe.
If userspace wants to test manually:
View device uevent
1
2
3
4 $ cat /sys/bus/rpmsg/devices/virtio0.rpmsg-tty.-1.0/uevent
BUS=rpmsg
DRIVER=rpmsg_tty
MODALIAS=rpmsg:rpmsg-ttyManually trigger uevent
1 $ echo change > /sys/bus/rpmsg/devices/virtio0.rpmsg-tty.-1.0/ueventThis will re-invoke rpmsg_uevent(), and udev will process it again. View module aliases
1
2
3 $ grep rpmsg /lib/modules/$(uname -r)/modules.alias
alias rpmsg:* rpmsg_core
alias rpmsg:rpmsg-tty rpmsg_tty
rpmsg_dev_groups
dev_groups is a field of struct bus in the Linux device model_, its role is: to automatically create a set of sysfs attribute files for each device registered on this bus.
Inrpmsg_core.cin:
1 | static struct attribute *rpmsg_dev_attrs[] = { |
ATTRIBUTE_GROUPS(rpmsg_dev) is a kernel macro, which expands to:
1 | static struct attribute_group rpmsg_dev_group = { |
Then attach it to the bus:
1 | static struct bus_type rpmsg_bus = { |
Key data structures
struct rpmsg_channel_info
1 | /** |
Common scenarios:
- Scenario 1: Passing channel information when creating an endpoint
1 | // rpmsg_dev_In probe() |
Here, chinfo tells the backend:
What service I am:
name = "rpmsg-tty"What local address I want: src (might be
RPMSG_ADDR_ANY, let the backend assign)Who I accept to access me:
dst = RPMSG_ADDR_ANY(any remote)Scenario 2: Identifying channels
1 | // rpmsg_find_In device() |
Here, chinfo is a “query condition”:
- Can be retrieved via
name+src+dstPrecisely find an existing channel - Can also use
RPMSG_ADDR_ANYas a wildcard, ignoring src or dst for matching
struct rpmsg_device
1 | /** |
struct device dev
This is the embedded base class of the Linux device model.rpmsg_deviceAccess the kernel device model through composition rather than inheritance.
Key macro (inrpmsg_internal.h):
1 |
Kernel bus callback only getsstruct device *, converted to through this macrostruct rpmsg_device *。
struct rpmsg_device_id id
include/linux/mod_devicetable.h
1 | /* rpmsg */ |
This is the service name compared during bus matching. For example, “rpmsg-tty”, “rpmsg-client-sample”.
Why wrap a separate layer of structure?
- Follow the Linux device model’s
mod_devicetable.hstandardMODULE_DEVICE_TABLE(rpmsg, ...)Requires unifiedxxx_device_idFormat- Can extend fields in the future without breaking ABI
const char *driver_override
Force specify the driver name. When set, the bus matching logic bypasses id_table and OF matching, directly comparing the driver name.
Important: The comment says do not set directly because the kernel will kfree() this pointer when the device is destroyed. Correct usage:
1 driver_set_override(dev, &rpdev->driver_override, "my_drv", strlen("my_drv"));
u32 src/u32 dst
| Field | Meaning | Change timing |
|---|---|---|
| src | Local address. When the device is created, it might beRPMSG_ADDR_ANY, and updated to the actual value after backend allocation | rpmsg_dev_probe()inrpdev->src = ept->addr |
| dst | Peer address. Usually the known remote service address, orRPMSG_ADDR_ANY | set by the backend at creation time |
src is the address I listen/receive on, dst is who I send to. rpmsg_device represents a logical channel, so it contains both addresses.
struct rpmsg_endpoint *ept
Points to the default endpoint of this channel. When the driver provides a callback,rpmsg_dev_probe() an endpoint is automatically created and assigned here.
Note: A rpmsg_device can only have one default ept, but the driver can manually create additional endpoints in probe() (e.g., when multiple listening addresses are needed).
bool announce
Controls whether to send to the remote end when the channel is created/destroyed**Name Service (NS)**announcement messages.announce = true: Sends “I’m online” at creation, and “I’m offline” at destruction. After receiving it, the remote processor can update its own service table, or trigger the corresponding client connection
const struct rpmsg_device_ops *ops
Backend operation table, defined inrpmsg_internal.h:
1 | /** |
This is the dividing line between the RPMSG core layer and the specific backend:
rpmsg_core.conly calls these interfacesvirtio_rpmsg_bus.c(or other backends) implements these interfaces- allows the RPMSG framework to support multiple underlying transports (although currently it’s mainly virtio)
struct rpmsg_endpoint
1 | typedef int (*rpmsg_rx_cb_t)(struct rpmsg_device *, void *, int, void *, u32); |
endpoint is the first-class object for sending/receiving
rpmsg core attaches all communication operations torpmsg_endpoint, rather thanrpmsg_device. This means:
- A
rpmsg_device(channel) can have multiple endpoints. - Different endpoints can have different callbacks and addresses.
- The send operation is performed via the endpoint, naturally carrying the source address.
This is similar to the design of TCP sockets: the device is like a socket fd, and the endpoint is like a specific connection endpoint.
rpmsg_rx_cb_tCallback type
1 | typedef int (*rpmsg_rx_cb_t)(struct rpmsg_device *, void *, int, void *, u32); |
Parameter details
| Type | Meaning |
|---|---|
struct rpmsg_device * | RPMSG device (channel) that received the message |
void * | Message data pointer (payload) |
int | Message length (payload len) |
void * | Private data (rpmsg_create_eptpassed in whenpriv) |
u32 | Message source address (sender address) |
Usually returns 0 to indicate successful processing. The specific meaning is defined by the backend, generally:
- 0: Message processed, buffer can be released
- Negative value: Processing error
Usage scenario
- Scenario A: Simple driver, providing callback during registration
1 | static int my_rpmsg_cb(struct rpmsg_device *rpdev, void *data, int len, |
The framework atrpmsg_dev_probe()automatically creates the endpoint, calling the backend-implementedrpdev->ops->create_ept()binds this callback in (virtio_rpmsg_bus.c)。
- Scenario B: Complex driver, manually creating multiple endpoints
1 | static int my_probe(struct rpmsg_device *rpdev) |
rpmsg_deviceandrpmsg_endpointrelationship between
This is the key to understanding the RPMSG architecture:
1 | ┌─────────────────────┐ ┌──────────────────────┐ |
Relationship summary:
- 1
rpmsg_deviceRepresents a logical channel (associated with a remote processor) - 1
rpmsg_deviceAt least 1 defaultrpmsg_endpoint(rpdev->ept) - 1
rpmsg_deviceCan have N additional endpoints (manually created by the driver) - each
rpmsg_endpointBinds a uniqueaddr, triggers its own cb when a message destined for that address is received
Address lifecycle mapping
| Stage | rpmsg_device->src | rpmsg_endpoint->addr | Description |
|---|---|---|---|
| Device just created | RPMSG_ADDR_ANY | None (not yet created) | Waiting for backend allocation |
**rpmsg_dev_probe()In ** | Updated, set toept->addr | ept->addr | Actual address allocated by the backend |
| Runtime | Remains unchanged | Remains unchanged | Used for message routing |
Key assignment chain:
1 | // rpmsg_dev_probe() |
struct rpmsg_driver
1 | /** |
struct rpmsg_driverIs the core structure that driver developers need to populate in the RPMSG framework. It follows the standard Linux device driver model while encapsulating RPMSG-specific messaging semantics.
struct device_driver drv
Linux device model base class, used to attach torpmsg_bus
const struct rpmsg_device_id *id_table
Device ID table supported by the driver (matched by service name)
int (*probe)(struct rpmsg_device *)
Called upon successful match, executes driver initialization, refer torpmsg_dev_probefunction
void (*remove)(struct rpmsg_device *dev);
Called upon device removal/driver unload, executes cleanup
int (*callback)(struct rpmsg_device *, void *, int, void *, u32);
Message receive callback (triggers the framework to automatically create a default endpoint),callbackandprobeDivision of labor
callback triggers automatic endpoint creation
| callback | Framework behavior | Applicable scenarios |
|---|---|---|
| Non-NULL | rpmsg_dev_probe()Automatically creates default endpoint, binds callback torpdev->src | Simple service, single address listening |
| NULL | Framework does not create default endpoint, driver needs toprobe()manually call inrpmsg_create_ept() | Complex service, multi-address, dynamic endpoint management |
andrpmsg_device/rpmsg_endpointtriangular relationship
| Object | Created by | Managed by | Lifecycle |
|---|---|---|---|
rpmsg_device | Backend (virtio) receives NS message | Kernel device model | During channel existence |
rpmsg_endpoint | Framework automatically creates (rpdev->ept) or driver manually creates | krefReference count | Bound to device or driver requirements |
rpmsg_driver | Statically defined by driver author | module_init/module_exit | During module loading |
Call chain
1 | rpmsg_bus.match() 比较 |
struct rpmsg_device_ops
1 | /** |
Invocation timing
| Member | Mandatory? | Call function | Invocation timing | Precondition |
|---|---|---|---|---|
| create_ept | Must | rpmsg_create_ept() | ① The framework automatically creates the default endpoint; ② The driver manually creates the endpoint | rpdev->opsNon-empty |
| announce_create | Optional | rpmsg_dev_probe() | driverprobe()After success | ept created successfully andops->announce_createNon-empty |
| announce_destroy | Optional | rpmsg_dev_remove() | Device removal/Driver unload At the beginning | ops->announce_destroyNon-empty |
rpdev->ops->create_ept()
- Framework automatically creates
rpmsg_core.c:rpmsg_dev_probe()
1 | // rpmsg_core.c: rpmsg_dev_probe() |
- Driver manually creates
1 | // Driver code example |
rpdev->ops->announce_create()
1 | // rpmsg_core.c: rpmsg_dev_probe() |
Backend:
- Send a “channel creation” message to the remote processor via the RPMSG Name Service protocol
- Message content usually includes: service name
rpdev->id.name, local addressrpdev->src
rpdev->ops->announce_destroy()
1 | // rpmsg_core.c: rpmsg_dev_remove() |
Backend:
- Send a “channel destroy” message to the remote end via the name service protocol
- After receiving it, the remote end will delete the channel from its own service table, and subsequent messages sent to that address will be discarded or return an error
Summary
| Operation | Caller | Callee | Core semantics |
|---|---|---|---|
| create_ept | rpmsg_create_ept() | Backend | Allocate resources: bind backend buffer and interrupt to the local address |
| announce_create | rpmsg_dev_probe() | Backend | Publish service: notify the remote end that “a service is listening at this address” |
| announce_destroy | rpmsg_dev_remove() | Backend | Revoke service: notify the remote end that “the service at this address is about to stop” |
These three hooks together implement the complete lifecycle management of RPMSG “create-publish-revoke”, which is the most critical contract interface between the core layer and the backend
struct rpmsg_endpoint_ops
1 | /** |
Decoupling of rpmsg core and backend
The architecture of the rpmsg subsystem can be viewed as two layers:
1 | +---------------------------------------------------+ |
| Level | ops | Operation object | Typical operation |
|---|---|---|---|
| Rpmsg Device | rpmsg_device_ops | rpmsg_device(channel) | Create endpoint, announce channel existence |
| Rpmsg Endpoint | rpmsg_endpoint_ops | rpmsg_endpoint(Communication endpoint) | Send data, destroy endpoint, poll |
This layering allows the core to perform name service announcement when the device registers (via rpmsg_device_ops.announce_create), while specific data transmission and reception go through the endpoint (via rpmsg_endpoint_ops.send)。
Layered relationship:
1 | rpmsg_device (代表一个 channel) |
Forwarding logic of rpmsg core
drivers/rpmsg/rpmsg_core.cEach API is a thin wrapper, with a few typical functions as examples:
1 | int rpmsg_send(struct rpmsg_endpoint *ept, void *data, int len) |
Because send is required, core has no fallback. If XIO.
1 | int rpmsg_sendto(struct rpmsg_endpoint *ept, void *data, int len, u32 dst) |
seerpmsg_core.cAlthough the comment says sendto is optional, the semantics of optional are “the backend can choose not to support this operation”, not “core will provide compatibility for you”. If a backend only implements send and trysend, calling sendto will fail.
1 | __poll_t rpmsg_poll(struct rpmsg_endpoint *ept, struct file *filp, |
virtio_rpmsg_bus.cpoll is not implemented, so it goes throughrpmsg_poll()returns 0 when called. Anddrivers/rpmsg/rpmsg_char.c
1 | mask |= rpmsg_poll(eptdev->ept, filp, wait); |
rpmsg_char.cprovides user-space/dev/rpmsgXinterface.rpmsg_poll()The result is ORed intopoll mask. If the backend does not implement poll, user-space’s/dev/rpmsgXdoingpoll()can only detect read events (EPOLLIN), cannot detect viarpmsg_poll()detect write-ready status. However, because virtio’srpmsg_sendseries either block or return immediately, the write path does not actually depend on poll.
Summary
| ops | required | core behavior (when not implemented) | virtio backend |
|---|---|---|---|
| destroy_ept | ✅ required | N/A (will not be NULL, must be set at initialization) | virtio_rpmsg_destroy_ept |
| send | ✅ required | -ENXIO | virtio_rpmsg_send |
| trysend | ✅ required | -ENXIO | virtio_rpmsg_trysend |
| sendto | optional | -ENXIO | virtio_rpmsg_sendto |
| send_offchannel | optional | -ENXIO | virtio_rpmsg_send_offchannel |
| trysendto | optional | -ENXIO | virtio_rpmsg_trysendto |
| trysend_offchannel | optional | -ENXIO | virtio_rpmsg_trysend_offchannel |
| poll | optional | returns 0 | undefined |
EXPORT_SYMBOLS
rpmsg_create_ept()
1 | /** |
Callrpdev->opsin thecreate_ept
rpmsg_destroy_ept()
1 | /** |
rpmsg_send()
1 | /** |
rpmsg_sendto()
1 | /** |
rpmsg_send_offchannel
1 | /** |
rpmsg_trysend()
1 | /** |
rpmsg_trysendto()
1 | /** |
rpmsg_poll()
1 | /** |
rpmsg_trysend_offchannel()
1 | /** |
rpmsg_find_device()
1 | /* |
complete flowchart
click code block to expand
1 | local processors remote processor |
