Zephyr RTOS 嵌入式 C 编程 (使用嵌入式 RTOS POSIX API)11. 理解并在应用开发中使用 Zephyr ZBus

Zephyr ZBus

ZBus 与基于 Linux 的系统中使用的 DBus 有许多共同概念。不同的是,DBus 关注的是运行在独立虚拟内存空间中的进程间通信,而 ZBus 关注的是 Zephyr RTOS 线程间的通信。

ZBus 的基础模式是观察者模式 ,其中一个线程可以向所有对此类消息感兴趣的观察者广播消息。ZBus 也可用于多对多通信场景。ZBus 支持的通信模式包括发布/订阅和消息传递。基于 ZBus 的通信利用共享内存实现,本质上可以是同步或异步的。

ZBus 的核心抽象是通道。Zephyr 线程向通道发布消息并从通道读取消息。线程还可以观察通道,当通道被修改时从总线接收通知。

ZBus 架构的概念示意图如图 11-1 所示。
图 11-1

ZBus 架构概念概览

使用 ZBus 的应用逻辑与硬件无关。应用线程通过该总线与其他线程通信。通过 ZBus 相互通信的线程无需了解彼此的细节。从这个意义上说,这些线程彼此解耦。

ZBus 架构

如下示意图所示,ZBus 的架构由
  • 一组通道,每个通道具有唯一标识符和控制元数据信息

  • 一个虚拟分布式事件分发器(VDED),提供向观察者发送通知的总线逻辑

  • 线程 (订阅者线程)和回调 (监听器线程),负责从总线发布、读取和接收通知

如图  11-2 所示。

 

图 11-2

ZBus 架构示意图

通过 ZBus 通道可执行的操作包括发布、读取和订阅。

由于底层操作可能阻塞,发布和读取操作不能在 ISR(中断服务例程)上下文中使用。这是因为发布和复制操作涉及互斥锁步骤,随后需要从共享内存区域进行内存复制或向其复制数据。

ZBus 观察者的注册可以是静态或动态的。 静态观察者注册在编译时定义且不可移除,但可通过调用 zbus_obs_set_enable() 方法来禁用。 动态观察者注册则可根据需要在运行时添加或移除。

Zephyr 文档提供了一个在基于传感器的应用中使用 ZBus 的典型示例。该示例的场景如图 11-3 所示。

 

图 11-3

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 消息传递顺序保证

监听器作为同步观察者,将按照通道定义的顺序作为其通知和消息消费顺序。而订阅者作为异步行为体,虽然会按通道定义顺序接收通知,但将在下次运行时消费数据。因此,分配给订阅者的优先级将用于定义响应顺序。

在 ZBus 应用中,所有监听器(无论是静态还是动态)都会在任一订阅者收到该消息通知之前接收到给定消息。消息传递的顺序规则是:从先到后依次为(SRLSRS)
  • 静态监听器,然后

  • 运行时监听器,接着

  • 静态订阅者,最后

  • 运行时订阅者

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 实例 ,定义了观察者的订阅者类型及异

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

akluse

失业老程序员求打赏,求买包子钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值