我手搓了一套适用于任何嵌入式项目的跨线程通信API

大家好,我是 Wayson,平时经常在终端里折腾各种 Linux 底层代码和跨平台编译。

在做嵌入式和 Linux 应用层开发时,不管我们用什么架构,最后往往都会掉进同一个坑里:模块间的通信逻辑最终变成了一团乱麻。

让人高血压的通信痛点

随便抓一个现有的项目看看:

  1. 在 FreeRTOS 里 :你有一个传感器任务读取温度,一个 UI 任务负责显示。于是你创建了一个 QueueHandle_t,在 app_init 里传给这两个任务。这还好。但接着,WiFi 任务也需要温度数据,日志任务也需要。很快,你的初始化代码里塞满了各种 Queue 句柄,牵一发而动全身。
  2. 在无操作系统的裸机(Bare-metal)里 :又是另一番景象。你的 main 函数超级循环里堆满了这样的代码:if (flag_sensor) ... if (flag_button) ... if (flag_timer)。随着项目膨胀,你根本无法追踪这些全局标志位到底是在哪个中断里被拉高的。
  3. 在 Linux 应用层里 :则是满天飞的模块指针,各个 C 文件互相 #include,强耦合在一起。

市面上有没有好用的总线?有,比如 DBus 或者 ZeroMQ,但它们要么依赖太重(跑个 daemon),要么疯狂 malloc,根本塞不进资源受限的 MCU 里。

因为实在找不到一个轻量、通用且能在严苛环境下运行的解决方案,我决定自己动手,撸了一个只有 3 个核心 API 的 C 语言事件总线 ------ embedmq

核心破局思路:embedmq

graph TD subgraph 传统的面条式耦合 A[传感器模块] -->|指针调用/共享Queue| B[UI 模块] A -->|全局标志位| C[日志模块] B -->|互相等待| C end subgraph embedmq 事件驱动解耦 D[传感器模块] -.->|post sensor.temp| E((embedmq 总线)) E -.->|唤醒 & 回调| F[UI 模块] E -.->|唤醒 & 回调| G[日志模块] end style E fill:#8a2be2,stroke:#fff,stroke-width:2px,color:#fff

设计 embedmq 时,我给自己定死了几个要求:

  • 绝对解耦:生产者和消费者互相不认识,只通过字符串 ID 认亲。
  • 零内存分配 :必须支持静态分配,初始化后绝对不调用任何 malloc
  • 运行时零开销 :热路径上绝对不能有 strcmp 这种字符串匹配操作。
  • 跨平台统一:一套代码,能在 Linux、FreeRTOS 和裸机上跑通。

最终,它被浓缩成了这样极其简单的调用方式:

C

c 复制代码
// 消费者:只管订阅
embedmq_register(q, "sensor.temp", on_temp, NULL); 

// 生产者:任何线程/中断只管发
embedmq_post(q, "sensor.temp", &data, sizeof(data)); 

它是怎么榨干性能的?(架构亮点)

为了适应极低算力的 MCU,我在内部做了一些极端的权衡:

1. UUID 路由:干掉运行时的字符串比对

事件的名字虽然是易读的字符串(如 "sensor.temp"),但在 embedmq_register 注册时,系统会用 FNV-1a 算法将其一次性哈希成 uint32_t 的 UUID。

当你在热路径(比如高频传感器数据读取)调用 post 时,内部的分发和查找全部是基于整数的 O(log n) 二分查找。甚至,我还留了一个 embedmq_post_id 接口,允许你在紧循环里直接用缓存的 UUID 发送,榨干最后一个 CPU 周期。

2. 极致紧凑的无锁环形缓冲

由于不能动态分配内存,在静态模式(Static Mode)下,所有的内部状态(句柄结构体、handler 表、环形缓冲以及暂存区)都被紧凑地塞进调用者提供的一块连续 Buffer 里。在处理消息跨越环形缓冲区末尾(Wrap)时,我没有用浪费空间的哨兵机制,而是做了透明的两次 memcpy

3. 统一的 PAL (平台抽象层)

针对不同环境,底层的驱动方式完全不同,但暴露给上层的 API 是统一的:

  • Linux : 基于 pthread 和 POSIX 计数信号量。队列空时消费者睡眠,来数据唤醒,避免忙等。
  • FreeRTOS: 同样使用计数信号量,这部分逻辑已经在 CI 里的 FreeRTOS POSIX 模拟器中验证通过。
  • 裸机 (None) : 使用 C11 的原子操作 (Atomic Spinlock)。由于没有线程概念,你需要把 embedmq_poll(q) 挂在你的超级循环里来驱动消息分发。

性能表现

在 x86-64 Linux(Release 编译,单生产+单消费)下的实测数据:

  • embedmq_post() 吞吐量:300 万条/秒
  • 端到端平均延迟(从 post 到 handler 执行): ~38 µs

写在最后

造这个轮子的初衷,就是想让底层的 C 语言开发也能享受到现代化的"事件驱动"体验,告别"意大利面条式"的耦合逻辑。

目前代码已经完全开源,采用了宽松的 MIT 协议。里面甚至带了一个 C++14 的 header-only 封装,完美支持 RAII 和 Lambda 捕获。

项目的详细信息可参考embedmq原项目:

项目地址GitHub - w4ysonch/embedmq

相关推荐
济6173 小时前
BMS系统专栏:电池状态监控任务
嵌入式硬件·嵌入式·bms电池系统管理
济6173 小时前
BMS系统专栏: BMS_ProtectTask 电池保护任务
嵌入式硬件·嵌入式·bms电池管理
番茄灭世神17 小时前
RTC授时时间戳转换工具
c语言·单片机·嵌入式
charlie1145141911 天前
嵌入式Linux驱动开发——从轮询到中断
linux·开发语言·驱动开发·嵌入式
2023自学中1 天前
imx6ull开发板,sd卡启动运行linux,手动给开发板的 emmc 做分区、烧系统
linux·嵌入式·开发板
阿泽·黑核1 天前
05 keyflow 扩展设计方案:矩阵键盘/组合键/事件队列/中断驱动
线性代数·矩阵·计算机外设·嵌入式·agent·vibe coding
用户805533698031 天前
Linux 工作队列:把中断里做不了的事推迟到进程上下文
linux·嵌入式
pie_thn2 天前
嵌入式应用开发笔记之web端设备控制台
嵌入式·编程
济6172 天前
BMS系统专栏:BQ76920 锂电 AFE 芯片深度解析
嵌入式硬件·嵌入式·bms电池管理