📌 本文目标:给出一套通用的嵌入式软件分层架构模板,讲清楚每一层的职责边界、设计原则和模块间通信方式。
📌 参考项目:RT-Thread — 国内最优秀的嵌入式开源项目之一,其设备驱动框架和分层设计堪称教科书级别。
📌 适用范围:MCU 裸机 / RTOS / 嵌入式 Linux 应用层均可参考此思想。
一、为什么需要架构设计?
大多数嵌入式项目的死法不是"做不出来",而是改不动。
| 没有架构的项目 | 有架构的项目 |
|---|---|
| 换一颗芯片,整个项目重写 | 只需重写 HAL 层,上层不动 |
| 加一个功能,到处改代码 | 新增一个模块,注册进框架即可 |
| 只有写的人能看懂 | 新人看目录结构就能理解系统 |
| 一个 Bug 引发连锁崩溃 | 层间隔离,故障不扩散 |
架构的本质是"分离关注点":让每一层只关心自己该做的事,通过约定好的接口与其他层交互。
二、通用分层架构总览
┌──────────────────────────────────────────────────┐ │ Application Layer │ ← 业务逻辑(状态机、策略、流程) ├──────────────────────────────────────────────────┤ │ Service Layer │ ← 功能组件(日志、配置、OTA、通信协议) ├──────────────────────────────────────────────────┤ │ OS Abstraction Layer (OSAL) │ ← 操作系统抽象(裸机/RTOS 可切换) ├──────────────────────────────────────────────────┤ │ Driver Layer │ ← 设备驱动(面向功能的抽象接口) ├──────────────────────────────────────────────────┤ │ Hardware Abstraction Layer (HAL) │ ← 寄存器操作(面向硬件的直接封装) ├──────────────────────────────────────────────────┤ │ Hardware (PCB) │ ← 物理硬件 └──────────────────────────────────────────────────┘核心原则:上层可以调用下层,下层绝不依赖上层。
三、各层详解
3.1 Hardware Abstraction Layer(HAL 层)
职责:屏蔽芯片差异,封装寄存器操作为统一接口。
设计要求:
- 一个外设对应一组接口(
hal_uart.h、hal_spi.h、hal_gpio.h) - 接口只暴露"做什么",不暴露"怎么做"
- 换芯片时,只需重新实现这一层
示例接口定义:
/* hal_uart.h — 所有平台共用这套接口声明 */typedefstruct{uint32_tbaudrate;uint8_tdata_bits;uint8_tstop_bits;uint8_tparity;}hal_uart_config_t;inthal_uart_init(uint8_tport,consthal_uart_config_t*config);inthal_uart_send(uint8_tport,constuint8_t*data,uint16_tlen,uint32_ttimeout_ms);inthal_uart_recv(uint8_tport,uint8_t*buf,uint16_tlen,uint32_ttimeout_ms);inthal_uart_deinit(uint8_tport);📖RT-Thread 中的对应设计:
rt-thread/components/drivers/下的 serial 框架,通过struct rt_uart_ops将底层操作抽象为函数指针表,上层只调用rt_device_read/write,完全不感知硬件差异。
3.2 Driver Layer(驱动层)
职责:基于 HAL,封装具体外设设备的完整驱动逻辑。
与 HAL 的区别:
| HAL 层 | Driver 层 |
|---|---|
| “往 SPI 发一帧数据” | “读取 W25Q128 Flash 的第 N 页” |
| “拉高 GPIO” | “点亮 LED3” |
| “I2C 写寄存器” | “设置 MPU6050 采样率为 1KHz” |
设计要点:
- 一个设备一个模块(
drv_w25qxx.c、drv_mpu6050.c) - 对上提供功能接口,而非硬件接口
- 内部维护设备状态(是否初始化、是否忙碌)
/* drv_w25qxx.h — 对上层暴露的是"Flash 操作"而不是"SPI 操作" */intw25qxx_init(void);intw25qxx_read(uint32_taddr,uint8_t*buf,uint32_tlen);intw25qxx_write(uint32_taddr,constuint8_t*data,uint32_tlen);intw25qxx_erase_sector(uint32_tsector_addr);3.3 OS Abstraction Layer(OSAL 系统抽象层)
职责:隔离 RTOS 差异。今天用 FreeRTOS,明天换 RT-Thread,上层代码不用动。
为什么需要这一层?
很多项目直接在业务代码里写xSemaphoreGive()、osDelay(),一旦换 OS 就要全局替换。OSAL 解决这个问题。
/* osal.h — 统一接口 */osal_mutex_tosal_mutex_create(void);intosal_mutex_lock(osal_mutex_tmutex,uint32_ttimeout_ms);intosal_mutex_unlock(osal_mutex_tmutex);osal_thread_tosal_thread_create(constchar*name,void(*entry)(void*),void*arg,uint32_tstack_size,uint8_tpriority);voidosal_delay_ms(uint32_tms);💡 裸机环境下,
osal_delay_ms实现为阻塞延时,osal_mutex_lock实现为关中断;RTOS 环境下切换为对应 API。同一套业务代码,不改一行就能在裸机和 RTOS 间切换。
3.4 Service Layer(服务/中间件层)
职责:提供通用功能组件,为应用层提供"开箱即用"的能力。
常见组件:
┌─────────────────────────────────────────────────────┐ │ Service Layer │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │ │ │ Log │ │ Config │ │ OTA │ │ CLI │ │ │ │ 日志 │ │ 参数管理│ │ 远程升级│ │ 命令行 │ │ │ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │ │ │ MQTT │ │ Modbus │ │ CJSON │ │ FOTA │ │ │ │ 客户端 │ │ 协议栈 │ │ 解析器 │ │ 差分升级│ │ │ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │ └─────────────────────────────────────────────────────┘设计要点:
- 每个组件独立编译、独立测试
- 组件之间不直接调用,通过事件/回调通信
- 可裁剪:不需要的组件删掉,不影响其他部分
📖RT-Thread 中的对应设计:
rt-thread/components/目录,每个组件(finsh、dfs、net、at)都是独立 package,通过 menuconfig 按需开启。
3.5 Application Layer(应用层)
职责:纯业务逻辑,不关心硬件、不关心 OS、不关心通信细节。
设计要点:
- 以状态机或事件驱动方式组织流程
- 只调用 Service 层和 Driver 层的接口
- 所有硬件相关的"脏活"已被下层屏蔽
/* app_main.c — 应用层只关心"做什么",不关心"怎么做" */voidapp_task_entry(void*arg){sensor_data_tdata;while(1){/* 采集 → 处理 → 上报,全是语义化调用 */sensor_read(&data);data_process(&data);if(alarm_check(&data)){alarm_trigger(ALARM_LEVEL_HIGH);}cloud_report(&data);osal_delay_ms(1000);}}四、层间通信机制
各层之间如何协作?三种核心模式:
4.1 回调函数(Callback)
下层通知上层的标准方式。下层不依赖上层头文件,只持有一个函数指针。
/* 驱动层定义回调类型 */typedefvoid(*uart_rx_callback_t)(uint8_t*data,uint16_tlen);/* 驱动层提供注册接口 */voiduart_register_rx_callback(uart_rx_callback_tcb);/* 应用层注册自己的处理函数 */uart_register_rx_callback(my_data_handler);📖 RT-Thread 中大量使用此模式:设备驱动的
rx_indicate回调、软件定时器的timeout回调等。
4.2 事件/消息队列
任务间解耦的最佳方式。发送方不需要知道谁在接收。
生产者(中断/驱动) → [消息队列] → 消费者(应用任务)4.3 发布-订阅(Pub/Sub)
一对多通知。一个事件发生,多个模块响应。
[传感器模块] -- publish("temp_high") --> [报警模块] --> [日志模块] --> [云端上报模块]五、目录结构规范
一个规范的嵌入式项目,打开目录就应该能看懂架构:
project/ ├── app/ # 应用层 │ ├── app_main.c │ ├── app_sensor.c │ └── app_cloud.c │ ├── service/ # 服务层(中间件) │ ├── mqtt/ │ ├── logger/ │ ├── config/ │ └── ota/ │ ├── driver/ # 驱动层 │ ├── drv_led.c │ ├── drv_w25qxx.c │ ├── drv_esp8266.c │ └── drv_mpu6050.c │ ├── osal/ # OS 抽象层 │ ├── osal.h # 统一接口 │ ├── osal_freertos.c # FreeRTOS 实现 │ └── osal_bare.c # 裸机实现 │ ├── hal/ # 硬件抽象层 │ ├── stm32f4/ # 具体芯片实现 │ │ ├── hal_uart.c │ │ ├── hal_spi.c │ │ └── hal_gpio.c │ └── hal_def.h # 统一类型定义 │ ├── lib/ # 第三方库(纯算法、不依赖硬件) │ ├── cjson/ │ ├── lwrb/ # 轻量级环形缓冲区 │ └── mbedtls/ │ ├── config/ # 项目配置 │ ├── board_config.h # 引脚定义、时钟配置 │ └── proj_config.h # 功能裁剪开关 │ ├── docs/ # 文档 │ ├── architecture.md │ └── api_reference.md │ └── tools/ # 构建脚本、烧录工具 ├── CMakeLists.txt └── flash.sh六、核心设计原则(速查表)
| 原则 | 说明 | 反例 |
|---|---|---|
| 单向依赖 | 上层调用下层,下层不 include 上层头文件 | hal_uart.c里 includeapp_main.h |
| 接口与实现分离 | .h只暴露接口,.c隐藏实现细节 | 把 static 函数写进.h |
| 模块内聚 | 一个模块只做一件事 | drv_sensor.c里面还包含数据上报逻辑 |
| 面向接口编程 | 用函数指针表实现多态 | 用if (chip == STM32) ... else if (chip == GD32) |
| 防御式编程 | 所有外部输入都做合法性检查 | 直接解引用传入的指针,不判空 |
| 零全局变量 | 用结构体封装模块状态,通过句柄访问 | 满屏uint8_t flag1, flag2, count; |
七、推荐参考的开源项目
🏆 首推:RT-Thread
| 项目 | RT-Thread/rt-thread |
|---|---|
| ⭐ Stars | 10K+ |
| 语言 | C |
| 架构亮点 | 完整的分层设计:内核 → 设备框架 → 组件 → 应用 |
| 学习重点 | components/drivers/下的 I/O Device 框架 |
为什么推荐 RT-Thread 作为架构学习范本?
- 设备驱动框架是分层设计的极致体现:
应用层: rt_device_read(dev, ...) ↓ 设备框架: struct rt_device → ops → read() ↓ 驱动层: stm32_uart_read() ← 具体芯片实现 ↓ HAL 层: STM32 HAL_UART_Receive()无论底层是 STM32、GD32 还是 RISC-V,应用层调用的都是rt_device_read——这就是分层的威力。
组件化设计,按需裁剪:
- 通过 Kconfig 机制,每个组件可独立开关
- 不需要网络?关掉
NET组件,编译体积直接缩小 - 这种"可裁剪"的思想值得所有项目学习
代码规范堪称教材:
- 统一的命名前缀(
rt_) - 完整的 Doxygen 注释
- 每个函数都有参数校验(
RT_ASSERT)
- 统一的命名前缀(
其他值得参考的项目
| 项目 | 地址 | 学什么 |
|---|---|---|
| EasyLogger | armink/EasyLogger | 如何设计一个可裁剪、可扩展的嵌入式组件 |
| FlashDB | armink/FlashDB | KV 数据库设计、Flash 磨损均衡 |
| letter-shell | NevermindZZT/letter-shell | 命令注册框架、函数指针表的工程应用 |
| MultiButton | 0x1abin/MultiButton | 事件驱动 + 状态机的经典小项目(仅 200 行) |
| lwrb | MaJerle/lwrb | 环形缓冲区的工业级实现 |
💡学习建议:先看 MultiButton(200行代码,2小时读完),理解事件驱动和状态机;再看 EasyLogger(中等规模),理解组件化设计;最后看 RT-Thread 设备框架(大型项目),理解完整分层架构。
八、从"能用"到"好用":一个演进的例子
以"串口接收数据"这件小事为例,展示架构思维如何一步步升级:
Level 0:初学者写法
所有逻辑堆在中断里,全局变量满天飞,无法维护。
Level 1:中断 + 标志位
中断里收数据存缓冲区、置标志位,主循环里判断标志位处理。耦合度高,但至少中断里不做耗时操作了。
Level 2:环形缓冲区 + 回调
中断里只往 Ring Buffer 塞数据,驱动层提供回调注册接口,应用层注册自己的处理函数。模块间解耦。
Level 3:设备框架 + 消息队列
驱动注册为"设备",应用层通过统一的device_read接口获取数据。中间通过消息队列异步传递,完全解耦。
Level 3 就是 RT-Thread 的做法,也是工业级嵌入式软件的标准做法。
九、总结:架构设计 Checklist
开始一个新项目前,对照这张表检查:
□ 是否划分了清晰的层次?每层职责是否单一? □ 换一颗芯片,需要改动的代码是否局限在 HAL 层? □ 加一个新功能,是否能在不改动其他模块的情况下完成? □ 每个模块是否可以独立编译、独立测试? □ 中断中是否只做"最少的事"(存数据 + 发通知)? □ 全局变量是否已经消灭(或封装进模块结构体)? □ 是否有统一的错误码定义和错误处理策略? □ 目录结构是否能让新人"看一眼就懂"?🎯一句话总结:好的嵌入式架构 = 分层隔离 + 接口抽象 + 事件驱动 + 可裁剪。
看一遍 RT-Thread 的
components/drivers/目录,胜读十篇架构文章。
推荐阅读顺序:MultiButton(入门) → EasyLogger(进阶) → RT-Thread Device Framework(精通)