嵌入式HAL与OSAL组件通信及分层架构设计 - 第1节

为什么嵌入式软件需要架构?

说实话,我见过太多嵌入式项目从「一个人写个流水灯」演变成「五个人改一个全局变量就崩」的惨剧。你想想看,一个智能家居网关,刚开始就控制几盏灯,代码全塞在 main.c 里,跑得挺欢。等加了温湿度传感器、窗帘电机、安防报警,再来个 OTA 升级——好家伙,改一行代码要重新编译整个工程,测试一遍要半天。

这就是没有架构的代价。

嵌入式软件架构,说白了就是给代码画个「施工蓝图」。它不解决某个具体功能怎么写,它解决的是:代码该怎么组织,模块之间怎么配合,将来怎么改才不崩。

核心观点:架构不是用来「好看」的,是用来「抗变化」的。硬件换了、需求加了、团队大了——架构好的项目,改一个模块就行;架构烂的项目,重写。

没有架构的典型症状

我在项目评审时,只要看到下面这些「味道」,就知道架构出问题了:

  • 全局变量满天飞——一个 extern int flag 能被 8 个文件引用,改一个值,全盘崩溃。
  • 中断服务函数里写业务逻辑——ISR 里调 printf、做浮点运算、甚至等信号量,系统响应能快才怪。
  • 硬件驱动和业务代码揉在一起——换个传感器型号,要改 20 个文件,因为每个文件里都直接操作了 I2C 寄存器。
  • 没有「层」的概念——应用代码直接调用 HAL 库,哪天 MCU 从 STM32 换成 GD32,所有代码都得重写。

嗯,这些坑我基本都踩过。有一次做车载 T-Box,就因为中断里做了个耗时的 CRC 校验,导致 CAN 报文丢帧,路测时差点出事故。从那以后,我对「架构分层」这件事,再也不敢马虎。

分层架构的核心价值

分层架构不是嵌入式独有的,但它在嵌入式领域特别实用。为什么?因为嵌入式系统天然就有「硬件层 → 驱动层 → 系统层 → 应用层」的物理分层逻辑。你想想看,CPU 寄存器、外设控制器、RTOS 内核、业务逻辑——它们本来就是不同层次的东西。

分层架构的核心价值,我总结为三点:

价值 说明 我的经验
隔离变化 硬件升级、驱动换版,不影响上层业务 曾经把 NRF24L01 无线模块换成 LoRa,只改了驱动层,应用代码一行没动
复用能力 同一套驱动/中间件,跨项目使用 我维护的「通用传感器框架」在 5 个产品里复用,节省了 60% 的开发时间
并行开发 定义好接口后,驱动、系统、应用可以同时开工 一个 4 人团队,2 周完成原本 6 周的工作量,靠的就是接口先行
一个小技巧:分层不是越多越好。对于资源受限的 MCU(比如 64KB Flash),3~4 层就够了。层数太多,接口传递的开销反而得不偿失。

解耦的本质:让模块「各管各的」

解耦这个词,听起来很玄乎。其实说白了就一句话:A 模块出了问题,B 模块不受影响。

怎么做到?靠的是「接口」和「依赖倒置」。

接口:模块之间的「合同」

我习惯把接口比作「USB 协议」。你插 U 盘、键盘、摄像头,只要它们遵守 USB 协议,电脑就能用。至于 U 盘里面是 NAND Flash 还是 NOR Flash,电脑不关心。

在嵌入式软件里,接口就是一组函数声明:

// 传感器驱动接口(头文件)
typedef struct {
    int32_t (*init)(void);
    int32_t (*read)(uint8_t *buf, uint32_t len);
    int32_t (*ioctl)(uint32_t cmd, void *arg);
} sensor_drv_t;

// 应用层只依赖这个接口,不关心具体传感器
int32_t app_temperature_read(sensor_drv_t *drv, float *temp);

你看,应用层只跟 sensor_drv_t 这个结构体打交道。底层是 DHT11 还是 SHT30,应用层根本不知道。这就是解耦。

依赖倒置:让高层定义规则

传统的做法是「高层调用底层」,比如应用层直接调驱动层的 i2c_read()。但这样耦合太紧——驱动层一改,应用层就得跟着改。

依赖倒置的做法是:高层定义接口,底层去实现这个接口。还是上面的例子,sensor_drv_t 是应用层定义的,驱动层去填充这个结构体。谁说了算?应用层说了算。

避坑指南:我曾经在一个项目里,把接口定义放在驱动层的头文件里。结果应用层为了用这个接口,不得不 #include 整个驱动层的头文件,间接依赖了驱动层的所有宏定义和类型。正确的做法是:接口头文件单独放,谁都不依赖。

架构全景图:嵌入式软件的分层模型

下面这张图,是我在多个项目里沉淀下来的分层模型。它不是什么标准,但经过实战检验,适合大多数资源中等的嵌入式系统(比如 Cortex-M3/M4 系列)。

嵌入式系统软件分层架构全景图 应用层(Application Layer) 业务逻辑 · 状态机 · 协议解析 · 用户交互 中间件层(Middleware Layer) 文件系统 · 网络协议栈 · 消息队列 · 日志服务 OS/抽象层(OS & Abstraction Layer) RTOS内核 · 任务调度 · 内存管理 · 硬件抽象层(HAL) 驱动层(Driver Layer) GPIO · UART · I2C · SPI · ADC · Timer · DMA 硬件层(MCU + 外设) 依赖方向:上层依赖下层接口 解耦关键:层间只通过接口通信

这张图里,每一层只依赖它的下一层,而且是通过「接口」来依赖,不是直接调用。举个例子:应用层想读传感器数据,它不会直接调 HAL_I2C_Master_Transmit(),而是调中间件层提供的 sensor_read() 接口。中间件层再调 OS 抽象层的 I2C 接口,最终由驱动层操作寄存器。

这样做的好处是什么?

  • 换 MCU 时,只需要改驱动层和 OS 抽象层,应用层和中间件层不动。
  • 换传感器时,只需要改驱动层,上层接口不变。
  • 加新功能时,在中间件层或应用层加模块,不影响现有代码。
我的建议:不要一开始就追求「完美分层」。先画一张架构图,把模块分到对应的层里。然后问自己一个问题:「如果明天硬件换了,我要改几个文件?」如果答案超过 3 个,说明分层还不够彻底。

本章小结:架构不是银弹,但没架构是灾难

嵌入式软件架构,说到底就是两件事:分层和解耦。分层让代码有清晰的归属,解耦让模块能独立演化。这两件事做好了,你的项目就能从「一个人写到死」变成「一个团队持续迭代」。

我个人习惯,在项目启动的第一周,花 30% 的时间做架构设计。很多人觉得这是浪费时间——代码都没写呢,画什么图?但实际经验告诉我,这 30% 的时间,会在项目后期省下 300% 的返工时间。