2. 实时操作系统基础
做嵌入式开发这么多年,我见过太多“一锅端”的代码。所有功能揉在一个文件里,GPIO操作、算法逻辑、协议解析全混着写。这种代码,说白了就是给自己挖坑。今天咱们聊聊怎么把架构理清楚。
2.1 为什么要分层?
先问个问题:你写代码时,有没有想过“如果换一颗MCU,我这套代码能直接跑吗?”
大部分人的答案是否定的。原因很简单——硬件相关的代码和业务逻辑缠在一起了。分层的目的,就是把这些耦合解开。
核心原则:每一层只关心自己的事,不越界。上层不直接操作寄存器,下层不关心业务逻辑。
我个人习惯把嵌入式软件分成三个层次:
- 应用层——负责业务逻辑、状态机、用户交互
- 中间件层——负责协议栈、算法库、文件系统等通用组件
- 驱动层——负责直接操作硬件寄存器、外设控制
这三层之间,通过定义好的接口通信。接口一旦定下来,各层就可以独立开发、独立测试。
2.2 经典三层模型详解
咱们一张图把结构说清楚:
这张图看着简单,但真正落地时,很多人会搞混各层的边界。我一个个说。
2.3 各层职责与边界
2.3.1 应用层
应用层是离用户最近的一层。它负责:
- 业务逻辑编排(比如“按键按下后,启动电机”)
- 状态机管理(比如“待机→运行→告警”)
- 任务调度(RTOS的任务创建与优先级管理)
- 用户交互(LCD显示、按键响应)
关键约束:应用层不能直接调用驱动函数。它只能通过中间件层提供的接口来间接操作硬件。
我的经验:曾经有个项目,应用层直接调了SPI的寄存器写函数。后来换MCU,所有应用代码都得改。如果当时通过中间件层封装一下,只需要改驱动层就够了。
2.3.2 中间件层
中间件层是承上启下的关键。它提供:
- 通信协议栈(TCP/IP、CANopen、Modbus)
- 文件系统(FATFS、LittleFS)
- 算法库(PID控制、FFT、滤波)
- 硬件抽象接口(HAL层封装)
中间件层不关心具体硬件,它只定义接口。比如“写一个字节到UART”,中间件层只调用一个函数指针,具体实现由驱动层提供。
| 中间件组件 | 依赖的接口 | 典型实现 |
|---|---|---|
| FATFS文件系统 | disk_read / disk_write | SD卡驱动、Flash驱动 |
| LwIP协议栈 | low_level_output / low_level_input | 以太网MAC驱动 |
| Modbus协议 | uart_send / uart_recv | UART驱动 |
2.3.3 驱动层
驱动层是离硬件最近的一层。它直接操作寄存器、配置外设、处理中断。驱动层的核心职责:
- 寄存器读写(通过HAL宏或直接内存映射)
- 外设初始化与配置(时钟、GPIO、DMA等)
- 中断服务函数(ISR)
- 提供标准化的接口给中间件层调用
注意:驱动层内部可以依赖具体MCU的头文件(比如stm32f4xx.h),但对外暴露的接口必须与硬件无关。接口参数用标准类型(uint8_t、int32_t),不要用寄存器位域。
2.4 接口设计的关键
三层模型能不能跑起来,全看接口设计。我建议用结构体函数指针来定义接口:
/* 驱动层对外接口定义 */
typedef struct {
int32_t (*init)(void);
int32_t (*write)(uint8_t *data, uint32_t len);
int32_t (*read)(uint8_t *data, uint32_t len);
int32_t (*ioctl)(uint32_t cmd, void *arg);
} uart_driver_t;
/* 中间件层通过指针调用,不关心具体实现 */
extern uart_driver_t uart1_drv;
extern uart_driver_t uart2_drv;
这样做的好处是:换MCU时,只需要重新实现这些函数指针指向的函数,中间件层和应用层完全不用动。
2.5 避坑指南
我踩过的坑,列出来给大家参考:
- 别让应用层直接包含驱动头文件——一旦包含,耦合就产生了。应用层只能通过中间件层接口调用。
- 中间件层不要硬编码硬件参数——比如波特率、引脚号,这些应该通过配置结构体传入,或者从驱动层读取。
- 驱动层不要做业务判断——比如“如果数据超时,就重发”,这是中间件层的事。驱动层只管收发。
- 接口参数类型要统一——我见过有人用char*传数据,有人用uint8_t*,混在一起容易出bug。统一用标准类型。
核心总结:分层架构的本质是“职责分离”。每一层只做自己的事,通过接口通信。这样换硬件时,只需要改驱动层;改业务逻辑时,只需要改应用层。互不干扰。