我手搓了一套适用于任何嵌入式项目的跨线程通信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

相关推荐
dddwjzx1 小时前
嵌入式Linux C应用编程入门——信号
linux·嵌入式
想你依然心痛3 小时前
固件架构演进:从裸机到RTOS到Linux的决策矩阵——复杂度、实时性、成本三维解析
架构·嵌入式·响应式编程·数据流·流处理·资源控制·背压
想你依然心痛4 小时前
响应式编程在嵌入式中的实践——Reactive Streams与背压
嵌入式·响应式编程·数据流·流处理·资源控制·背压
K成长日志1 天前
BLE协议之设备地址
网络·物联网·网络协议·嵌入式·蓝牙·ble
凉、介1 天前
ARMv8 架构下的刷 Cache 操作
c语言·笔记·学习·架构·嵌入式·arm
会周易的程序员2 天前
microLog 的本地日志读取接口 log_reader — 本地日志文件读取工具开发指南
linux·物联网·架构·嵌入式·日志·iot·aiot
charlie1145141912 天前
Cinux: 加载第一个内核:从 bootloader 跳进 C++
linux·开发语言·c++·嵌入式
dddwjzx2 天前
嵌入式Linux C应用编程入门——标准IO库
嵌入式
pai同学2 天前
ESP-IDF+vscode开发ESP32第十二讲——event
嵌入式
凉、介2 天前
KVM + QEMU 虚拟化
笔记·学习·嵌入式·arm·qemu·虚拟化·kvm