从零拆解 embedmq:一个纯 C 实现的嵌入式线程间事件总线
引言嵌入式项目里,线程间通信是绕不开的问题。传感器线程读到数据,UI 线程要更新显示,网络线程要上报——它们之间怎么传消息? 最直接的做法是让模块互相持有对方的指针或队列句柄,但代价是强耦合:改一个模块,另一个也要跟着改。 embedmq 是我写的一个零依赖 C11 库,把线程间消息分发压缩成三个函数:create、register、post。本文从源码层面拆解它的每一个设计决策。 一、传统做法的痛点裸机:flag 泛滥12345678910volatile bool g_uart_ready = false;volatile bool g_sensor_ready = false;void main(void) { while (1) { if (g_uart_ready) { process_uart(); g_uart_ready = false; } if (g_sensor_ready) { update_display(); g_sensor_ready = fa...
嵌入式 Linux 里的设计模式
写代码久了会发现一件事:很多问题的结构是重复的。协议解析要追踪”现在处理到哪一步了”,多个模块要响应同一份数据,不同类型的设备要对外提供一样的接口——这些问题换个项目换个语言还是会出现,解法也大同小异。 把这些反复出现的解法总结下来,就是设计模式。1994 年 GoF 出了一本书《Design Patterns》,归纳了 23 种模式,后来成了软件工程里的经典参考。 但设计模式不是要往代码里强行套的模板,也不是炫技用的。它只是一套命名系统——给那些”有经验的程序员都会这么写,但说不清楚叫什么”的结构起了个名字,方便沟通和复用。真正的价值在于:当你遇到一个结构性问题,能快速识别出它属于哪类,然后套用已经被验证过的解法,而不是每次都从头发明轮子。 GoF 的 23 个模式里,很多在嵌入式 Linux 开发里用不上或者用处不大。这篇只挑五个真正高频的讲:状态机、观察者、工厂、命令模式、责任链。 一、状态机(FSM)状态机(Finite State Machine)描述的是一个系统在不同状态之间跳转的逻辑。任何时刻,系统只处于一个确定的状态;收到某个输入(事件)后,系统根据当前状态和输...
MQTT 入门:从原理到实战
物联网设备有一个共同的需求:把数据实时传出去。温度传感器要上报温度,门磁要上报开关状态,烟雾报警器要在触发时第一时间通知手机。 这个需求听起来简单,但用 HTTP 做会很快碰到墙。MQTT 就是为了解决这个问题而生的。 一、为什么需要 MQTTHTTP 的问题HTTP 的模型是这样的:客户端发请求,服务器回响应。一问一答,客户端主动,服务器被动。 如果你想用 HTTP 实时监控一个温度传感器,只能轮询: 1234while (1) { http_get("http://sensor/temperature"); sleep(1);} 每秒发一个请求。大部分时候数据根本没变,这个请求是完全无效的。100 个设备同时轮询,就是 100 个并发连接一直占着服务器,带宽和服务器资源都在做无用功。 对嵌入式设备来说还有另一个问题:HTTP 本身比较重,每个请求都要带 Header,一个简单的温度数据可能只有几个字节,但 HTTP 的 Header 就有几百字节。电池供电的设备承受不起这个开销。 还有一个根本性的问题:HTTP 没有”推...
嵌入式内存管理:从 Flash 到 Cache,代码的每一字节去哪了
嵌入式开发只有两件事:你要 CPU 干什么,你要内存怎么用。后者更容易翻车——栈溢出、内存碎片、DMA 跑飞、Cache 一致性问题,每一样都能让你抱着逻辑分析仪看到天亮。 这篇文章从芯片内存布局开始,覆盖裸机 MCU 和 Linux 嵌入式两端的全貌。 一、你的芯片长什么样STM32F407 的内存地图,从 0x0000_0000 读到 0xFFFF_FFFF: 1234567891011121314150x0000_0000 ┌──────────┐ │ Flash │ 典型的 1MB,你的代码和常量在这里0x0800_0000 ├──────────┤ │ SRAM1 │ 112KB,main stack + heap + 全局变量0x2000_0000 ├──────────┤ │ SRAM2 │ 16KB,额外的0x2001_C000 ├──────────┤ │ CCM RAM │ 64KB,紧耦合内存,CPU 独占,DMA 碰不到0x1000_0000 ├─...
C++ 异常机制:为什么嵌入式不用它,用什么替代
C++ 异常是标准的错误处理机制,几乎所有 C++ 教材都会讲。但嵌入式项目和 Google 的大型代码库都选择禁掉它——Google C++ Style Guide 明确规定不允许在新代码里使用异常,绝大多数嵌入式工程也用 -fno-exceptions 编译。 这篇讲清楚异常是怎么工作的、代价在哪里、以及实际项目里用什么替代。 一、异常的基本用法1234567891011121314151617#include <stdexcept>float divide(float a, float b) { if (b == 0.0f) throw std::invalid_argument("division by zero"); return a / b;}int main() { try { float r = divide(10.0f, 0.0f); } catch (const std::invalid_argument &e) ...
嵌入式开发者视角的 Google C++ Style Guide 实战解读
一、这东西到底有什么用?2013 年 Google 把内部 C++ 规范扔上了 GitHub。现在 38000+ Star,Chromium 在用,LLVM 在参考,国内大厂的规范里也多多少少能看到它的影子。 但它从一开始就没打算当”温和的建议”。它禁异常、禁 RTTI、禁 C 风格转型、禁全局变量、禁静态存储期对象。每一条单独拎出来都能在技术群里吵一个下午。 这些规则背后有一个简单的事实:这份规范是为 100M+ 行代码、上万工程师、维护几十年的代码库写的。这个场景跟嵌入式出奇地像——二进制要小、控制流要稳、出问题不能靠抛异常甩锅。 我不是来翻译官方文档的。下面从写了几十万行嵌入式 C/C++ 的经验出发,拆哪些能直接用、哪些得改改、哪些 Google 自己也没那么认真。 二、核心哲学:为什么偏要优化给”读者”看?Google Style 的第一句话就能劝退不少人: Optimize for the reader, not the writer. 说白了:写的时候多花 5 秒,让别人(以及三个月后的你自己)读的时候省 5 分钟。 这在嵌入式项目里意味着什么?拿...
Linux 并发编程:线程、锁与同步
单线程程序按顺序执行,简单但低效——等待 I/O 的时候 CPU 闲着,多核处理器只用了一个核。并发编程让程序同时做多件事,充分利用硬件资源。 代价是复杂度:多个线程共享内存,同时读写同一块数据会出问题,需要同步机制来协调。这篇从线程基础讲起,覆盖 pthread(POSIX 标准)和 C++11 并发接口,并与 FreeRTOS 做对比,帮你建立完整的并发编程认知。 一、线程基础线程是什么线程是进程内的执行单元。一个进程可以有多个线程,这些线程共享同一块地址空间——代码段、堆、全局变量都是共享的,但每个线程有自己独立的栈和寄存器状态。 和进程的区别: 进程 线程 地址空间 独立 共享(同进程内) 创建开销 大(fork 要复制页表) 小 通信方式 IPC(管道、共享内存) 直接读写共享变量 崩溃影响 不影响其他进程 一个线程崩溃可能导致整个进程崩溃 线程共享的资源:堆内存、全局变量、文件描述符、信号处理。线程独占的资源:栈、寄存器、线程局部存储(TLS)、errno。 graph TD subgraph 进程 ...
Linux 系统编程:进程、文件 I/O、IPC 与网络
Linux 系统编程是直接调用内核提供的系统调用来完成进程管理、文件操作、网络通信等任务。和应用层库不同,系统调用是程序和操作系统内核之间最直接的接口。 这篇覆盖嵌入式 Linux 开发里最常用的几块:进程与信号、文件与 I/O、进程间通信、网络编程。 一、进程与信号进程基础程序是磁盘上的可执行文件,进程是程序运行起来之后的实体。同一个程序可以同时跑多个进程,比如你开两个终端各跑一个 vim,这是两个进程,各自有独立的状态,互不干扰。 每个进程有独立的地址空间——代码段、数据段、堆、栈都是自己的,内核通过页表保证进程之间不能互相读写内存。这个隔离是操作系统稳定性的基础:一个进程崩溃,其他进程不受影响。 进程在运行过程中有几种状态: 运行(Running):正在占用 CPU 执行 可运行(Runnable):准备好了,等待调度器分配 CPU 睡眠(Sleeping):在等待某个事件(I/O、信号、定时器),不占 CPU 僵尸(Zombie):进程已经退出,但父进程还没调用 wait 回收,进程表项还在 停止(Stopped):收到 SIGSTOP 被暂停 ...
嵌入式 C/C++ 工程化:从能跑到能维护
写出能跑的代码不难,难的是六个月后自己还能看懂,换人维护不崩溃,新功能加进去不引入 bug。这就是工程化要解决的问题。 嵌入式项目有自己的特殊性:硬件依赖重、交叉编译环境复杂、资源受限导致测试困难。本文从项目结构、构建系统、代码规范、单元测试到 CI,讲的不是理论,而是实际能落地的做法。 一、项目结构为什么需要规范的目录结构很多嵌入式项目的早期状态是这样的:所有 .c 和 .h 文件堆在同一个目录,Makefile 手写,头文件互相 include 没有层次,第三方库直接复制进来和业务代码混在一起。项目小的时候没问题,一旦文件数量上去,依赖关系就会变成一张说不清楚的网。 规范目录结构的意义不是为了好看,而是让每个文件的定位一目了然,让构建系统能自动找到该找的东西,让新人接手时不需要靠口口相传才能理解项目。 推荐结构12345678910111213141516171819202122project/├── CMakeLists.txt # 顶层构建入口├── cmake/ # CMake 工具链、辅助脚本│ └── arm...
FreeRTOS 学习笔记(八):事件组
信号量和任务通知都是”等一个事件发生”。现实中的场景经常是”等多个条件同时满足”——比如”UART 收完一帧数据而且定时器已经到期”,或者”三个传感器任意一个就绪我就开始融合计算”。 事件组就是干这个的。 它用一个 24 位掩码表示事件(虽然用 EventBits_t 传参,实际只有低 24 位可用,高 8 位 FreeRTOS 内部用)。每个位代表一个事件,1 表示发生。 创建: 123EventGroupHandle_t xEventGroupCreate(void);// 静态版本EventGroupHandle_t xEventGroupCreateStatic(StaticEventGroup_t *pxEventGroupBuffer); 不需要指定大小——永远是 24 位。 设置事件位: 1234567// 任务上下文EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet...









