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

相关推荐
一直C3 小时前
【ARM嵌入式开发】LCD液晶屏显示原理、色彩模式与显存地址映射详解
lcd·嵌入式·arm·寄存器·cortex-a7
Hespethorn3 小时前
洪泛、路由、网络栈:LoRa 自组网三条路线的设计取舍与量化对比
物联网·嵌入式
用户0510122572967 小时前
RGA(二)——RK3588 的核拓扑与能力差异
linux·嵌入式
用户0510122572967 小时前
RGA(一)——基础知识
linux·嵌入式
用户0510122572967 小时前
RGA(三)——实宽高、虚宽高与对齐约束
linux·嵌入式
燕卫博7 小时前
SSD201 BSP 开发教程
嵌入式·bsp·ssd201
一直C8 小时前
【嵌入式 ARM 驱动开发】PWM 脉宽调制原理与 GT9147 电容触控屏驱动开发详解
嵌入式硬件·lcd·嵌入式·arm汇编·imx6ull·cortex-a7
haliu9 小时前
【FHE 同态加密】我们如何实现同态加密推理(十四):为什么 `RESULT=PASS` 不是判据(纯 C11 · 零依赖)
人工智能·嵌入式·c·fhe·推理引擎·c11·边缘推理·同态加密推理
jianqiang.xue20 小时前
【CStackGUI 实战】深色主题终端:app.dark 默认深色 + 顶层命名样式表 + 运行时切换
单片机·嵌入式·cstackgui·c语言gui·可视化拖拽
追涨杀跌老能手1 天前
GD32H759 + RT-Thread 工控实战--第7篇 OSPI Flash
开源·嵌入式