本文讨论的是嵌入式软件分层的通用原则,并以常见的 MCU 裸机或 RTOS 项目为例,介绍一套可落地的分层方式。
文中涉及 HAL、CMSIS、CubeMX、BSP、GPIO、UART、SPI、I2C 等内容,主要适用于 STM32、GD32、NXP、ESP32 等 MCU 平台。
对于 Linux 嵌入式、SoC 平台,虽然具体层次划分会有所不同,但职责分离、依赖单向和隔离变化的原则是一致的。
嵌入式项目为什么一定要分层?
很多嵌入式项目,刚开始代码量不大,大家写起来都很快。
但项目一旦进入长期维护阶段,问题就会逐渐暴露出来:
- 业务逻辑和底层驱动缠在一起
- 板级改动影响上层功能
- 换一个传感器,要连着改很多地方
- 代码虽然能跑,但谁都不敢轻易动
- 新人接手时,不知道从哪里改才不会出事故
这些问题,本质上都不是"代码写得不够努力",而是系统边界没有设计清楚。
所以,分层并不是为了"目录好看",也不是为了"显得专业",而是为了真正解决嵌入式项目中的长期维护问题。
分层的本质:通过明确职责边界和依赖方向,隔离变化传播,降低维护成本。
一、为什么嵌入式项目必须做分层
在嵌入式项目中,通常会同时面对四类变化:
1)平台变化
比如:
- MCU 更换
- HAL 库升级
- RTOS 替换
- 启动文件调整
- CubeMX 重新生成代码
2)板级变化
比如:
- GPIO 引脚调整
- UART / SPI / I2C 资源变更
- DMA 通道变化
- 中断映射调整
- 不同硬件版本板卡差异
3)设备变化
比如:
- 传感器型号更换
- Flash / EEPROM 替换
- 显示屏变更
- 通信模组替换
- 电机或执行器不同供应商切换
4)业务变化
比如:
- 命令定义变化
- 控制策略升级
- 状态机调整
- 业务流程变化
- 参数管理逻辑变更
如果项目没有分层,那么这些变化往往会互相扩散:
- 改业务,动到底层驱动
- 换设备,影响命令处理
- 板级引脚一改,上层逻辑也得跟着改
- 某个函数能不能删,没人敢确定
最后就会变成一种很典型的状态:
代码不是不能跑,而是不能维护。
所以,分层真正要解决的,不是"代码风格"问题,而是:
让不同类型的问题,在各自边界内闭环处理。
也就是说:
- 平台问题,在平台层解决
- 板级问题,在板级层解决
- 设备问题,在设备层解决
- 业务问题,在应用层解决
这样变化才不会无序蔓延。
二、推荐的六层架构
在嵌入式项目中,推荐采用下面这套六层结构:
- app: 应用层,收敛业务变化
- service: 服务层,收敛通用能力
- device: 设备层,收敛设备差异
- bsp: 板级支持层,收敛板级资源差异
- platform: 平台层,承接底层平台环境
- component: 组件层,沉淀可复用能力
如果用一句话概括这六层的定位:
从上到下,越往下越接近硬件;从左到右,component 提供横向复用能力。
三、六层分别负责什么
1. app:应用层
应用层只负责产品业务本身。
典型内容包括:
- 任务调度
- 命令处理
- 业务流程
- 控制策略
- 状态管理
- 模式切换
- 联动逻辑
应用层应该始终回答一个问题:
产品要做什么?
而不应该关心:
- GPIO 怎么配
- UART 用哪个实例
- SPI 怎么收发
- 某个设备具体怎么驱动
应用层原则
- 只关注业务目标和流程
- 不直接操作 HAL、CubeMX、寄存器
- 不关心板级细节
- 不承担底层初始化装配职责
一句话理解:
app 负责"做什么",不负责"底下怎么做"。
2. service:服务层
服务层负责给应用层提供可复用的通用服务能力。
常见内容包括:
- 参数管理
- 通信服务
- 协议处理
- 存储服务
- 日志服务
- 诊断服务
- 升级服务
- 告警服务
服务层的特点是:
- 它不是某个具体设备
- 也不是某个单独业务
- 而是能被多个业务共同使用的能力
比如:
- Modbus 协议栈属于 service
- 参数读写管理属于 service
- 故障日志记录属于 service
服务层原则
- 面向多个业务复用
- 提供稳定服务接口
- 不承载产品策略
- 不直接写具体设备语义
一句话理解:
service 是"通用能力层",为业务提供公共支撑。
3. device:设备层
设备层负责对具体外部器件或功能模块进行统一抽象。
常见对象包括:
- 温湿度传感器
- 压力传感器
- EEPROM / 外部 Flash
- LCD / OLED 屏
- 电机、继电器、蜂鸣器
- 无线通信模块
设备层的核心价值,是把底层总线访问和上层设备语义分开。
例如:
上层希望调用的是:
- temp_sensor_read()
- flash_write_page()
- motor_set_speed()
而不是直接到处写:
- HAL_SPI_Transmit()
- HAL_I2C_Mem_Read()
- HAL_GPIO_WritePin()
设备层原则
- 屏蔽总线和底层访问细节
- 提供设备语义化接口
- 不写业务流程
- 不直接掺杂产品逻辑
一句话理解:
device 负责"把器件变成可理解、可替换的设备接口"。
4. bsp:板级支持层
BSP(Board Support Package)层负责封装MCU 片上资源和板级资源。
常见内容包括:
- GPIO
- UART
- SPI
- I2C
- ADC
- PWM
- 定时器
- 外部中断
- DMA
- 板级初始化
这一层的核心,是把平台相关、板卡相关的细节封装起来,对上提供稳定接口。
比如:
- 哪个 UART 接哪个外设
- GPIO 高低电平是否反向
- 板载 LED 接在哪个引脚
- 某个 SPI 是不是复用 DMA
这些都应该在 bsp 层被吸收掉,而不是泄漏到业务代码中。
BSP 层原则
- 屏蔽 HAL / CubeMX 细节
- 对上提供稳定板级接口
- 不写设备语义
- 不写业务逻辑
一句话理解:
bsp 解决的是"这块板子怎么用"的问题。
5. platform:平台层
平台层承接项目的最底层环境,通常包括:
- HAL
- CMSIS
- RTOS
- 启动文件
- 芯片厂商库
- CubeMX 自动生成代码
这一层通常是:
- 平台官方提供的
- 自动生成的
- 与芯片架构密切相关的
平台层原则
- 尽量少改
- 不承载业务代码
- 上层不要直接依赖
- 变更控制要谨慎
一句话理解:
platform 是项目运行的"地基",但不是业务开发的主战场。
6. component:组件层
组件层是一个横向复用层,用于沉淀与业务无关、与硬件无关的基础能力。
常见组件包括:
- RingBuffer
- CRC
- PID
- 状态机框架
- 软定时器
- FIFO
- 字符串处理
- 通用算法
- 内存池
- 事件分发器
这些内容的特点是:
- 不依赖具体产品业务
- 不依赖特定硬件
- 可以跨多个项目复用
组件层原则
- 与业务无关
- 与硬件无关
- 可独立测试
- 不反向依赖 app / service / device / bsp
一句话理解:
component 是"可沉淀、可复用、可迁移"的基础能力库。
四、这套分层里,最核心的 7 条原则
如果把整套规范压缩成最重要的设计原则,本质上就是以下 7 条:
1)单一职责
一个模块只做一类事,不混职责。
比如:
- 设备驱动只做设备访问
- 业务状态机只做业务决策
- 板级封装只做资源适配
不要一边读传感器,一边顺手做业务判断,再顺手发协议。
2)高内聚、低耦合
模块内部要聚焦,模块之间依赖要清晰且尽量少。
好的模块应该是:
- 内部逻辑彼此紧密相关
- 对外暴露接口尽可能少
- 改内部实现,不影响上层使用
3)接口清晰
模块之间通过稳定接口交互,而不是靠 extern 全局变量到处穿透。
不建议:
- 上层直接访问底层全局句柄
- 多模块共享一堆裸变量
- 不通过接口直接改状态
建议:
- 用头文件声明清晰 API
- 输入输出明确
- 生命周期明确
- 错误码明确
4)隐藏实现细节
上层只关心"做什么",不关心"怎么做"。
例如应用层只需要知道:
- 是否读取成功
- 当前温度是多少
而不需要知道:
- 是 I2C 还是 SPI
- 是 DMA 还是轮询
- 是否用了校验重试
5)禁止越层访问
不能绕过中间层,直接下探到底层。
例如:
- app 不应该直接调 HAL
- service 不应该直接访问寄存器
- device 不应该直接越过 bsp 去碰 platform 细节
因为一旦越层,边界就失效,后续维护会越来越难。
6)可替换
好的分层设计,应该让底层变化尽量不影响上层。
比如:
- 换 MCU,app 基本不用改
- 换传感器,业务逻辑尽量不动
- 换板卡,device 和 service 尽量稳定
这才说明分层真正起到了隔离变化的作用。
7)面向接口编程
依赖稳定抽象,而不是依赖具体实现。
换句话说:
- 上层依赖"能力"
- 不依赖"某个具体实现细节"
这样项目才能逐步形成真正可扩展的架构。
五、运行期调用关系应该怎么设计
在运行期,推荐采用如下调用关系:
app
↓
service / device
↓
bsp
↓
platform
其中:
- app 面向业务
- service 和
device提供上层能力 - bsp 封装板级资源
- platform 提供底层运行环境
而 component 是横向复用层,可被以下层使用:
- app
- service
- device
- bsp
也就是说,component 不是主调用链上的一个"垂直层",而是一个可复用的基础能力层。
六、初始化关系,不等于运行期调用关系
这是很多项目特别容易混淆的一点。
运行期调用关系 和初始化装配关系,不是一回事。
初始化应该由统一入口进行编排,而不是让业务层自己去拉起底层。
推荐初始化方式如下:
main
├── bsp_board_init()
├── device_init_all()
├── service_init_all()
├── app_init()
└── app_start()
这里最关键的一条是:
app_init()不应该直接调用bsp_board_init()。
原因很简单:
- app 是业务层
- bsp 是板级层
- 如果业务层负责底层初始化,就等于边界倒置了
更合理的做法是:
- 由
main或系统初始化入口统一编排 - 各层只初始化各自职责范围内的内容
- 上层只在"环境已具备"的前提下启动
这样结构会更清晰,也更利于移植和测试。
七、一个新模块到底该放哪一层?
很多团队在做分层时,最常见的问题不是"不知道要分层",而是:
知道要分层,但不知道一个新模块该放哪里。
这里可以用 6 个问题快速判断:
1)是不是业务流程、策略、状态决策?
如果是,放 app
例如:
- 工作模式切换
- 命令执行流程
- 设备联动策略
- 故障处理状态机
2)是不是给多个业务提供公共能力?
如果是,放 service
例如:
- 参数管理
- 协议收发
- 日志记录
- 诊断服务
3)是不是在抽象某个具体外部设备?
如果是,放 device
例如:
- 温湿度传感器驱动封装
- 外部 Flash 封装
- 电机模块控制接口
4)是不是在封装 MCU 片上资源?
如果是,放 bsp
例如:
- UART 驱动适配
- GPIO 读写封装
- SPI 总线接口
- 板级初始化
5)是不是官方库、HAL、RTOS、启动文件?
如果是,放 platform
例如:
- STM32 HAL
- CMSIS
- FreeRTOS
- 启动代码
- CubeMX 生成代码
6)是不是与业务和硬件都无关、可复用?
如果是,放 component
例如:
- 环形缓冲区
- CRC 算法
- PID 控制器
- 通用状态机框架
判断优先级怎么排?
如果一个模块看起来"好像哪层都能放",建议按这个顺序判断:
先看职责,再看依赖,最后看复用性。
这句话非常关键。
因为一个模块该放哪,不是看"它长得像谁",而是看:
- 它本质在解决什么问题
- 它依赖哪些层
- 它未来是否需要复用
八、分层不是增加复杂度,而是避免失控
有些人会觉得:
"项目不大,分这么多层是不是太重了?"
这个担心可以理解,但要注意:
分层不是为了制造复杂度,而是为了防止复杂度失控。
项目小的时候,不分层似乎也能跑。
但随着时间推移,项目一定会遇到:
- 版本迭代
- 硬件变更
- 功能增加
- 多人协作
- Bug 修复
- 模块复用
- 平台迁移
真正让项目变难维护的,从来不是"代码量大",而是:
变化没有边界。
而分层设计的价值,就在于给这些变化加上边界。
九、一句话总结
如果要快速理解这套架构,可以直接用下面这段话做统一认知:
- app 负责业务
- service 负责通用服务
- device 负责设备抽象
- bsp 负责板级资源封装
- platform 负责底层平台环境
- component 负责可复用基础组件
再配上一条最重要的原则:
职责要单一,依赖要单向,变化要隔离。
十、结语
嵌入式项目的代码,最怕的不是写得慢,而是后面没人敢改。
一旦职责混乱、边界失控,项目就会进入一种非常痛苦的状态:
- 改一个点,影响一大片
- 修一个 Bug,带出两个新问题
- 每次重构都像拆炸弹
- 每次硬件变更都像推倒重来
所以,分层的意义不在于"形式规范",而在于:
把系统中不同类型的变化,控制在各自应该存在的边界内。
这也是软件架构真正的价值所在。