zbus ZBus message bus
Port of the zbus message bus to NuttX, built entirely on native NuttX primitives. ZBus implements many-to-many communication through channels (typed shared messages) observed by observers, keeping publishers and consumers fully decoupled.
A typical zbus application architecture.
Channels and observers are defined statically, in any source file, with declarative macros. The definitions are collected at link time through iterable sections (see the Iterable Sections component documentation) – there is no runtime registration and no central list to maintain.
ZBus anatomy: channels, observers and observations.
Observer types
The four observer types.
Type |
Behavior |
|---|---|
Listener |
Callback executed synchronously in the publisher
context ( |
Subscriber |
Receives channel references through a message
queue; waits with |
Message subscriber |
Receives a copy of every published message, in
order; waits with |
Async listener |
Callback executed on a dedicated task with a
copy of the message
( |
Runtime observers (zbus_chan_add_obs()/zbus_chan_rm_obs(),
CONFIG_ZBUS_RUNTIME_OBSERVERS), per-observation notification masks,
observer enable/disable, message validators and channel user data are
also supported.
Observer enable/disable and per-observation masks: disabling the observer (b) silences every channel; masking observations (c, d) silences individual channels.
Example
The figure below shows the kind of decoupled architecture zbus enables: every block only talks to channels, so each one can be replaced without touching the others.
A sensor-based application built on zbus.
#include <system/zbus.h>
struct acc_msg
{
int x;
int y;
int z;
};
static void listener_cb(const struct zbus_channel *chan)
{
const struct acc_msg *msg = zbus_chan_const_msg(chan);
printf("x=%d y=%d z=%d\n", msg->x, msg->y, msg->z);
}
ZBUS_LISTENER_DEFINE(acc_listener, listener_cb);
ZBUS_SUBSCRIBER_DEFINE(acc_subscriber, 4);
ZBUS_CHAN_DEFINE(acc_chan, /* Name */
struct acc_msg, /* Message type */
NULL, /* Validator */
NULL, /* User data */
ZBUS_OBSERVERS(acc_listener, /* Observers, in */
acc_subscriber),/* priority order */
ZBUS_MSG_INIT(.x = 0, .y = 0, .z = 0));
/* Publisher: */
struct acc_msg msg = { 1, 10, 100 };
zbus_chan_pub(&acc_chan, &msg, 1000);
/* Subscriber thread: */
const struct zbus_channel *chan;
if (zbus_sub_wait(&acc_subscriber, &chan, ZBUS_FOREVER) == 0)
{
zbus_chan_read(chan, &msg, 500);
}
A complete runnable example is available in apps/examples/zbus
(CONFIG_EXAMPLES_ZBUS), and a cmocka test suite covering the whole
API in apps/testing/zbus (CONFIG_TESTING_ZBUS).
Not ported
The following Zephyr zbus features are not available in this port:
Multi-domain proxy agent (
CONFIG_ZBUS_PROXY_AGENT): bridges channels between domains/cores over IPC. Experimental upstream and tied to the Zephyr IPC service; a NuttX equivalent would be built on rpmsg and is left as future work.Publishing from interrupt handlers: the Zephyr original allows
zbus_chan_pub()from ISRs withK_NO_WAIT. This port is a userspace library and its primitives (semaphores, message queues, lazy initialization) are not ISR-safe: interrupt handling belongs to the driver, which should hand the data to a thread (the usual NuttX pattern) that then publishes it.test_timer_driven_publisherinapps/testing/zbusshows the pattern with a kernel timer interrupt delivering a signal to a sampling thread.Priority boost (Highest Locker Protocol) (
CONFIG_ZBUS_PRIORITY_BOOST): the Zephyr hand-rolled protection against priority inversion during the notification process. Not needed: enable the nativeCONFIG_PRIORITY_INHERITANCEso the channel semaphores get equivalent protection from the kernel.net_buf pools and pool isolation (
CONFIG_ZBUS_MSG_SUBSCRIBER_BUF_*): obsolete by design in this port – message queues copy the payload onmq_send, so no shared reference-counted buffers exist at all.Static/user-provided runtime observer nodes (
CONFIG_ZBUS_RUNTIME_OBSERVERS_NODE_ALLOC_STATIC/NONE): runtime observer nodes are always heap-allocated in this port.
Differences from the Zephyr original
Timeouts are plain milliseconds (
int32_t):ZBUS_NO_WAIT(0) andZBUS_FOREVER(-1) replaceK_NO_WAIT/K_FOREVER.Subscriber queues are POSIX message queues opened lazily on first API use through the kernel
file_mq_*interface, making them usable from any task (amqd_tdescriptor would die with the opening task).Initialization is lazy (
pthread_onceon the first API call) replacing the ZephyrSYS_INIThook; no explicit init call is needed.Async listeners run on a dedicated task per listener (spawned on first use; priority and stack size are configurable) instead of the Zephyr system work queue.
Configuration
Requirements:
The board linker script must provide the zbus iterable sections, either by including
<nuttx/linker/common-rom.ld>inside.text(see thelinum-stm32h753biboard) or throughCONFIG_ITERABLE_SECTIONS_LINKER_INSERT(zero-touch mode; see its help text for the MEMORY-layout constraint).CONFIG_MQ_MAXMSGSIZEmust be at leastCONFIG_ZBUS_MSG_SUBSCRIBER_MAX_MSG_SIZEplus the size of a pointer when message subscribers or async listeners are used, otherwise their queues fail to open with-EINVAL.FLAT build (the library uses the kernel
file_mq_*interface directly).
Main options:
CONFIG_ZBUS– enable the library.CONFIG_ZBUS_CHANNEL_NAME/CONFIG_ZBUS_OBSERVER_NAME– name fields and lookup by name.CONFIG_ZBUS_CHANNEL_ID– numeric channel identifiers (ZBUS_CHAN_DEFINE_WITH_ID,zbus_chan_from_id()).CONFIG_ZBUS_MSG_SUBSCRIBER– message subscribers (+_MAX_MSG_SIZE,_QUEUE_SIZE).CONFIG_ZBUS_ASYNC_LISTENER– async listeners (+_PRIORITY,_STACKSIZEof their tasks).CONFIG_ZBUS_RUNTIME_OBSERVERS– runtime observers.CONFIG_ZBUS_CHANNEL_PUBLISH_STATS– publish timestamp/count.CONFIG_ZBUS_ASSERT_MOCK– invalid parameters return-EFAULTinstead of asserting (for tests).
Credits
The zbus design and the diagrams in this page come from the upstream Zephyr zbus documentation by Rodrigo Peixoto and contributors (Apache License 2.0).