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

相关推荐
嵌入式阿蔡15 小时前
CoAP / LwM2M 轻量协议:受限设备为什么不用 MQTT
单片机·物联网·嵌入式
长江后浪博客1 天前
ESP32-SOLO-1 在 ESP-IDF v5.5 下启动不断重启问题分析与解决
单片机·嵌入式硬件·嵌入式·esp32·freertos·esp-idf
Jason_zhao_MR2 天前
Linux与采集MCU通信为什么我们用了双SPI
linux·arm开发·单片机·嵌入式硬件·嵌入式
嵌入式阿蔡2 天前
云平台设备接入实战:三元组鉴权、属性上报与命令下发(阿里云/华为云)
嵌入式硬件·物联网·嵌入式·存储
分支预测失败2 天前
RISC-V 上下文切换实战:从 Trap 入口到 Linux 进程调度
后端·嵌入式
Freak嵌入式3 天前
基于 MicroPython+DMA 链式触发的 Scatter-Gather 数据聚合实现
嵌入式
Freak嵌入式3 天前
MicroPython ADC 采样拖慢主循环?试试 DMA + 乒乓缓冲,全程解放 CPU
嵌入式
Freak嵌入式3 天前
从代码到项目:MicroPython+MCP4725 自定义类实现信号发生器功能的完整流程
嵌入式
与你4033 天前
STM32F103_AB_OTA_从零复现教程
嵌入式
Freak嵌入式3 天前
一文搞懂 MCP4725 波形发生器:MicroPython 代码实现、定时器驱动与调试流程全梳理
嵌入式