Zephyr ZBus
ZBus 与基于 Linux 的系统中使用的 DBus 有许多共同概念。不同的是,DBus 关注的是运行在独立虚拟内存空间中的进程间通信,而 ZBus 关注的是 Zephyr RTOS 线程间的通信。
ZBus 的基础模式是观察者模式 ,其中一个线程可以向所有对此类消息感兴趣的观察者广播消息。ZBus 也可用于多对多通信场景。ZBus 支持的通信模式包括发布/订阅和消息传递。基于 ZBus 的通信利用共享内存实现,本质上可以是同步或异步的。
ZBus 的核心抽象是通道。Zephyr 线程向通道发布消息并从通道读取消息。线程还可以观察通道,当通道被修改时从总线接收通知。
ZBus 架构概念概览
使用 ZBus 的应用逻辑与硬件无关。应用线程通过该总线与其他线程通信。通过 ZBus 相互通信的线程无需了解彼此的细节。从这个意义上说,这些线程彼此解耦。
ZBus 架构
-
一组通道,每个通道具有唯一标识符和控制元数据信息
-
一个虚拟分布式事件分发器(VDED),提供向观察者发送通知的总线逻辑
-
线程 (订阅者线程)和回调 (监听器线程),负责从总线发布、读取和接收通知

ZBus 架构示意图
通过 ZBus 通道可执行的操作包括发布、读取和订阅。
由于底层操作可能阻塞,发布和读取操作不能在 ISR(中断服务例程)上下文中使用。这是因为发布和复制操作涉及互斥锁步骤,随后需要从共享内存区域进行内存复制或向其复制数据。
ZBus 观察者的注册可以是静态或动态的。 静态观察者注册在编译时定义且不可移除,但可通过调用 zbus_obs_set_enable() 方法来禁用。 动态观察者注册则可根据需要在运行时添加或移除。

ZBus 使用场景示例
在此场景中,当定时器触发时,它会将一个动作推送到工作队列,该队列将发布到 Start 触发器通道。这是让中断处理程序向 ZBus 发送消息的标准方式。订阅了 Start 触发器通道的传感器线程在收到通知后会开始获取传感器数据。事件分发器将执行 blink 回调函数,因为它监听着 Start 触发器通道。当传感器数据准备就绪后,传感器线程会将其发布到 Sensor 数据通道。
核心线程作为传感器数据通道订阅者 ,会接收并处理通过 ZBus 发送的传感器数据,将其存储在其内部采样缓冲区中。这一过程会不断重复,直到采样缓冲区填满。此时,核心线程会对存储的采样信息进行聚合处理,打包后发布到 Payload 通道。作为 Payload 通道订阅者的 LoRa 线程接收到该消息后进行传输。传输完成后,LoRa 线程会发布一个相应事件,因为此回调是 Transmission done 通道的监听器。
ZBus 的强大之处在于其灵活的使用方式。例如,代码可以修改为使用按钮按下中断而非定时器中断来触发上述事件序列。若要将传输介质从 LoRa 改为蓝牙低功耗(BLE),只需将 LoRa 线程替换为功能类似的 BLE 线程即可。
ZBus 与代码可重用性
假设某个模块具有一组明确定义的行为,并且仅使用 ZBus 通道而非硬件通道。这样的模块可以轻松地在其他应用中复用。只要新应用实现了该模块需要交互的接口(通道集合),就能使该模块与新应用协同工作。
ZBus 的局限性
ZBus 更适合解决某些特定类型的问题,了解其局限性非常重要。例如,ZBus 基准测试会表明,ZBus 并不适合在线程间传输高速数据流(每秒兆比特或更高速率)。这类特定需求可能更适合使用 Pipe 内核对象来实现。需要考虑的局限性包括消息传递保证和消息顺序保证方面的限制。
ZBus 消息传递保证与消息传输速率
尽管 Zbus 总会将消息传递给监听器,但对于订阅者却未必如此。这是因为订阅者会收到通知,而消息的读取则取决于订阅者的具体实现方式。
-
使监听器快速响应,并在必要时通过将耗时处理提交到工作队列来卸载这些任务
-
为生产者线程分配高优先级以避免消息丢失
-
确保有足够的 CPU 资源可供观察者及时消费所生成的数据
-
考虑使用消息队列或管道来实现字节流的快速传输
ZBus 消息传递顺序保证
监听器作为同步观察者,将按照通道定义的顺序作为其通知和消息消费顺序。而订阅者作为异步行为体,虽然会按通道定义顺序接收通知,但将在下次运行时消费数据。因此,分配给订阅者的优先级将用于定义响应顺序。
-
静态监听器,然后
-
运行时监听器,接着
-
静态订阅者,最后
-
运行时订阅者
ZBuses 实际编程应用
这涉及定义 ZBus 通道、ZBus 消息、必要的回调函数、订阅者和监听器。与大多数"Zephyr 相关事项"一样,实际细节需要掌握各种 ZBus 框架宏、数据结构以及 ZBus API。本节将对这些内容进行概述。
消息由某些与应用相关的数据结构定义。
struct acc_msg {
int x;
int y;
int z;
};
ZBus 框架大量使用宏定义,这有助于隐藏许多底层代码细节。ZBUS_CHAN_DEFINE 宏用于定义通道。
ZBUS_CHAN_DEFINE(_name, _type, _validator, _user_data, _observers, _init_val)
-
_name – 通道名称。
-
_type – 消息类型。必须为结构体或联合体。
-
_validator – 验证器函数。
-
_user_data – 指向用户数据的指针。
-
_observers – 观察者列表。顺序表示观察者优先级,排在第一位的具有最高优先级。
-
_init_val – 消息初始化值
ZBUS_CHAN_DEFINE(acc_chan, /* 名称 */
struct acc_msg, /* 消息类型 */
NULL, /* 验证器 */
NULL, /* 用户数据 */
ZBUS_OBSERVERS(my_listener, my_subscriber), /* 观察者 */
ZBUS_MSG_INIT(.x = 0, .y = 0, .z = 0) /* 初始值 {0} */
);
实际上,每个 ZBus 通道都关联着一个 zbus_channel 结构体 ,其中包含了控制通道访问和使用所需的信息。
struct zbus_channel {
const char *const name;
void *const message;
const size_t message_size;
void *const user_data;
bool (*const validator)(const void *msg, size_t msg_size);
struct zbus_channel_data *const data;
};
-
name – 通道名称
-
message_size – 通道消息的大小
-
user_data – 可用于扩展 ZBus 功能的数据,但在使用此字段前必须先声明通道
-
message – 指向消息所在实际共享内存区域的引用
-
validator – 指向消息验证器函数的函数指针,该函数将在实际发布消息前执行验证
-
无效消息将不会被发布。
-
如果该字段为空,则每条消息都将被视为有效。
-
-
mutex – 指向访问控制互斥锁的指针,用于在访问通道时避免竞态条件
-
observers – 通道观察者列表,可以为空或包含以任意顺序混合的监听器和订阅者
void listener_callback_example (const struct zbus_channel *chan)
{
const struct acc_msg *acc;
if (&acc_chan == chan) {
acc = zbus_chan_const_msg(chan);
LOG_DBG("From listener -> Acc x=%d, y=%d, z=%d", acc->x, acc->y, acc->z);
}
}
执行 acc = zbus_chan_const_msg(chan); 后,acc 直接指向该消息。
在前面的示例中,访问了消息数据结构中的字段,即 acc->x, acc->y, acc->z,然后将它们作为参数传递给 LOG_DBG。
Zephyr 提供了以下工具宏用于定义监听器和订阅者。
ZBUS_LISTENER_DEFINE 用于定义监听器,ZBUS_SUBSCRIBER_DEFINE 用于定义订阅者。
ZBUS_SUBSCRIBER_DEFINE(_name, _queue_size) 定义并初始化一个订阅者。
它通过初始化定义订阅者的 struct zbus_observer 实例 ,定义了观察者的订阅者类型及异

最低0.47元/天 解锁文章
1049

被折叠的 条评论
为什么被折叠?



