分层架构思想:嵌入式软件分层原则与状态机定位
做嵌入式开发这些年,我见过太多「一锅粥」式的代码。驱动里混着业务逻辑,中断服务函数里写状态切换,最后调试起来简直要命。说白了,分层架构就是为了解决这个问题——让每一层各司其职,出了问题能快速定位。
为什么一定要分层?
先问个问题:你手头的项目,代码量超过 5 万行了吗?如果超过了,不分层基本就是给自己挖坑。我接手过一个老项目,底层 SPI 读写函数里居然嵌着协议解析和超时重传逻辑。改一个 bug,三个模块跟着崩。嗯,那之后我花了整整两周重构,才把代码拆成三层。
分层的好处其实很直白:
- 复用性:换芯片?只改最底层,上层不动。
- 可测试性:每层可以单独 mock 测试。
- 可读性:新人看代码,从顶层往下读,思路清晰。
- 维护性:出 bug 了,先定位层,再定位模块。
核心原则:上层依赖下层,下层不依赖上层。数据向上传递,控制向下传递。
嵌入式软件的三层架构
我个人习惯把嵌入式软件分成三层:HAL(硬件抽象层)、LLD(底层驱动层)、中间件层。这三层的关系,我画了张图,你看一眼就明白了。
各层职责详解
1. 底层驱动层(LLD)
这是离硬件最近的一层。说白了,就是直接操作寄存器、处理中断、控制时序。我在项目中遇到过一个问题:某款传感器初始化时序要求先拉高 CS 再等 10μs,然后发送 0xAA。如果把这个时序写在应用层,换一颗传感器就全得改。所以 LLD 的职责就是:把硬件操作封装成最原子的函数。
| LLD 典型函数 | 说明 |
|---|---|
spi_reg_write(addr, data) |
直接写 SPI 寄存器 |
gpio_pin_set(pin, level) |
设置 GPIO 电平 |
timer_delay_us(us) |
微秒级延时 |
注意:LLD 层不要出现业务逻辑。我曾经见过有人在 LLD 里做数据校验,结果换芯片后校验算法变了,底层代码大改。LLD 只做「怎么操作硬件」,不做「为什么操作」。
2. 硬件抽象层(HAL)
HAL 层的作用是「抹平差异」。你想想看,STM32 的 SPI 和 NXP 的 SPI,寄存器地址、配置流程都不一样。但 HAL 层提供统一的接口,比如 spi_transfer(buf, len),上层不用关心底层是哪个芯片。
我习惯在 HAL 层做两件事:
- 设备抽象:把传感器、外设抽象成「设备对象」,包含初始化、读写、控制三个接口。
- 平台无关接口:比如
hal_spi_read(),内部调用 LLD 的函数,但对外暴露统一签名。
小技巧:HAL 层可以加一层「回调注册机制」。比如某个外设的中断,在 HAL 层注册回调函数,LLD 触发中断时调用。这样 LLD 不用知道上层是谁在监听。
3. 中间件层
中间件层是「工具箱」。TCP/IP 协议栈、FATFS 文件系统、FreeRTOS、加密算法库……这些都属于中间件。它们依赖 HAL 层提供的接口,但本身不直接操作硬件。
举个例子:你要在项目里加一个 MQTT 客户端。MQTT 协议栈是中间件,它调用 HAL 层的 TCP 接口,HAL 层再调用 LLD 的以太网驱动。换网卡?只改 LLD,MQTT 代码一行不动。
状态机在驱动层的位置
好,重点来了。状态机应该放在哪一层?
我个人建议:状态机放在 LLD 层。为什么?
- 硬件状态管理:比如 SPI 总线的空闲/忙碌/错误状态,这是硬件层面的状态,不是业务状态。
- 时序控制:传感器初始化需要经历「上电 → 等待稳定 → 发送配置 → 等待应答 → 就绪」这几个状态,用状态机管理最清晰。
- 中断与轮询切换:有些外设在初始化阶段用轮询,正常运行后用中断。状态机可以优雅地管理这个切换。
关键原则:状态机管理的是「硬件状态」,不是「业务状态」。业务状态机应该放在应用层或中间件层。
我曾经踩过一个坑:把业务状态机(比如「登录中 → 已登录 → 已离线」)放在了 LLD 层。结果业务逻辑变了,底层驱动也得跟着改,耦合得一塌糊涂。后来我强制规定:LLD 层的状态机只处理硬件状态,业务状态机往上放。
一个简单的 LLD 状态机示例
拿一个常见的 UART 接收场景来说。硬件状态无非三种:空闲、接收中、接收完成。用状态机管理,代码结构清晰很多。
// LLD 层状态机定义
typedef enum {
UART_IDLE,
UART_RX_IN_PROGRESS,
UART_RX_COMPLETE,
UART_ERROR
} uart_state_t;
static uart_state_t uart_state = UART_IDLE;
void uart_irq_handler(void) {
switch (uart_state) {
case UART_IDLE:
if (UART->SR & RXNE) {
uart_state = UART_RX_IN_PROGRESS;
// 开始接收
}
break;
case UART_RX_IN_PROGRESS:
// 接收数据
if (rx_len == expected_len) {
uart_state = UART_RX_COMPLETE;
// 通知 HAL 层
hal_uart_rx_done_callback();
}
break;
case UART_RX_COMPLETE:
// 等待上层取走数据
break;
case UART_ERROR:
// 错误处理
break;
}
}
你看,这个状态机只关心 UART 硬件的接收状态。至于接收到的数据是命令还是响应,那是上层的事,LLD 不管。
分层架构的避坑指南
我曾经犯过的错:
- 跨层调用:LLD 直接调用中间件层的函数。结果中间件层换了,LLD 也得改。记住:只能上层调用下层。
- 状态机放错层:把业务状态机放在 LLD,导致底层代码频繁修改。硬件状态机只管理硬件状态。
- HAL 层太薄:HAL 层只是简单转发 LLD 的函数,没有做抽象。换芯片时发现 HAL 层接口也得改,等于白做。
我的建议:刚开始做分层时,先画一张「依赖关系图」。箭头只能从上往下指。如果发现下层依赖上层,说明分层有问题,赶紧调整。
小结
分层架构不是银弹,但它能让你的代码活得更久。LLD 管硬件、HAL 管抽象、中间件管功能,状态机在 LLD 层管理硬件状态——这个分工一旦明确,后续的调试、维护、移植都会轻松很多。