📌 本文目标:给出一套通用的嵌入式软件分层架构模板,讲清楚每一层的职责边界、设计原则和模块间通信方式。
📌 参考项目: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 — 所有平台共用这套接口声明 */
typedef struct {
uint32_t baudrate;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity;
} hal_uart_config_t;
int hal_uart_init(uint8_t port, const hal_uart_config_t *config);
int hal_uart_send(uint8_t port, const uint8_t *data, uint16_t len, uint32_t timeout_ms);
int hal_uart_recv(uint8_t port, uint8_t *buf, uint16_t len, uint32_t timeout_ms);
int hal_uart_deinit(uint8_t port);
📖 RT-Thread 中的对应设计: rt-thread/components/drivers/ 下的 serial 框架,通过 struct rt_uart_ops 将底层操作抽象为函数指针表,上层只调用 rt_device_read/write,完全不感知硬件差异。
3.2 Driver Layer(驱动层)
职责: 基于 HAL,封装具体外设设备的完整驱动逻辑。
与 HAL 的区别:
| “往 SPI 发一帧数据” | “读取 W25Q128 Flash 的第 N 页” |
| “拉高 GPIO” | “点亮 LED3” |
| “I2C 写寄存器” | “设置 MPU6050 采样率为 1KHz” |
设计要点:
/* drv_w25qxx.h — 对上层暴露的是"Flash 操作"而不是"SPI 操作" */
int w25qxx_init(void);
int w25qxx_read(uint32_t addr, uint8_t *buf, uint32_t len);
int w25qxx_write(uint32_t addr, const uint8_t *data, uint32_t len);
int w25qxx_erase_sector(uint32_t sector_addr);
3.3 OS Abstraction Layer(OSAL 系统抽象层)
职责: 隔离 RTOS 差异。今天用 FreeRTOS,明天换 RT-Thread,上层代码不用动。
为什么需要这一层?
很多项目直接在业务代码里写 xSemaphoreGive()、osDelay(),一旦换 OS 就要全局替换。OSAL 解决这个问题。
/* osal.h — 统一接口 */
osal_mutex_t osal_mutex_create(void);
int osal_mutex_lock(osal_mutex_t mutex, uint32_t timeout_ms);
int osal_mutex_unlock(osal_mutex_t mutex);
osal_thread_t osal_thread_create(const char *name, void (*entry)(void*),
void *arg, uint32_t stack_size, uint8_t priority);
void osal_delay_ms(uint32_t ms);
💡 裸机环境下,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 — 应用层只关心"做什么",不关心"怎么做" */
void app_task_entry(void *arg)
{
sensor_data_t data;
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)
下层通知上层的标准方式。 下层不依赖上层头文件,只持有一个函数指针。
/* 驱动层定义回调类型 */
typedef void (*uart_rx_callback_t)(uint8_t *data, uint16_t len);
/* 驱动层提供注册接口 */
void uart_register_rx_callback(uart_rx_callback_t cb);
/* 应用层注册自己的处理函数 */
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 里 include app_main.h |
| 接口与实现分离 | .h 只暴露接口,.c 隐藏实现细节 | 把 static 函数写进 .h |
| 模块内聚 | 一个模块只做一件事 | drv_sensor.c 里面还包含数据上报逻辑 |
| 面向接口编程 | 用函数指针表实现多态 | 用 if (chip == STM32) … else if (chip == GD32) |
| 防御式编程 | 所有外部输入都做合法性检查 | 直接解引用传入的指针,不判空 |
| 零全局变量 | 用结构体封装模块状态,通过句柄访问 | 满屏 uint8_t flag1, flag2, count; |
七、推荐参考的开源项目
🏆 首推: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(精通)

