MCU项目软件分层设计规范--以 MCU 项目为例

本文讨论的是嵌入式软件分层的通用原则,并以常见的 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)业务变化

比如:

  • 命令定义变化
  • 控制策略升级
  • 状态机调整
  • 业务流程变化
  • 参数管理逻辑变更

如果项目没有分层,那么这些变化往往会互相扩散:

  • 改业务,动到底层驱动
  • 换设备,影响命令处理
  • 板级引脚一改,上层逻辑也得跟着改
  • 某个函数能不能删,没人敢确定

最后就会变成一种很典型的状态:

代码不是不能跑,而是不能维护。

所以,分层真正要解决的,不是"代码风格"问题,而是:

让不同类型的问题,在各自边界内闭环处理。

也就是说:

  • 平台问题,在平台层解决
  • 板级问题,在板级层解决
  • 设备问题,在设备层解决
  • 业务问题,在应用层解决

这样变化才不会无序蔓延。


二、推荐的六层架构

在嵌入式项目中,推荐采用下面这套六层结构:

  1. app: 应用层,收敛业务变化
  2. service: 服务层,收敛通用能力
  3. device: 设备层,收敛设备差异
  4. bsp: 板级支持层,收敛板级资源差异
  5. platform: 平台层,承接底层平台环境
  6. 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 控制器
  • 通用状态机框架

判断优先级怎么排?

如果一个模块看起来"好像哪层都能放",建议按这个顺序判断:

先看职责,再看依赖,最后看复用性。

这句话非常关键。

因为一个模块该放哪,不是看"它长得像谁",而是看:

  1. 它本质在解决什么问题
  2. 它依赖哪些层
  3. 它未来是否需要复用

八、分层不是增加复杂度,而是避免失控

有些人会觉得:

"项目不大,分这么多层是不是太重了?"

这个担心可以理解,但要注意:

分层不是为了制造复杂度,而是为了防止复杂度失控。

项目小的时候,不分层似乎也能跑。

但随着时间推移,项目一定会遇到:

  • 版本迭代
  • 硬件变更
  • 功能增加
  • 多人协作
  • Bug 修复
  • 模块复用
  • 平台迁移

真正让项目变难维护的,从来不是"代码量大",而是:

变化没有边界。

而分层设计的价值,就在于给这些变化加上边界。


九、一句话总结

如果要快速理解这套架构,可以直接用下面这段话做统一认知:

  • app 负责业务
  • service 负责通用服务
  • device 负责设备抽象
  • bsp 负责板级资源封装
  • platform 负责底层平台环境
  • component 负责可复用基础组件

再配上一条最重要的原则:

职责要单一,依赖要单向,变化要隔离。


十、结语

嵌入式项目的代码,最怕的不是写得慢,而是后面没人敢改。

一旦职责混乱、边界失控,项目就会进入一种非常痛苦的状态:

  • 改一个点,影响一大片
  • 修一个 Bug,带出两个新问题
  • 每次重构都像拆炸弹
  • 每次硬件变更都像推倒重来

所以,分层的意义不在于"形式规范",而在于:

把系统中不同类型的变化,控制在各自应该存在的边界内。

这也是软件架构真正的价值所在。

相关推荐
piaoyiren21 小时前
DS18B20 温度采集 + LCD1602 分页显示
单片机·嵌入式硬件·usart·ds18b20·lcd1602
piaoyiren1 天前
STM32 使用LCD1602简单显示字符
stm32·单片机·嵌入式硬件·lcd1602
小羊先生car1 天前
软件:STM32-F1系列-读取Flash(2026/7/14)
stm32·单片机·嵌入式硬件
weixin_464078071 天前
Altium Designer自定义变压器
嵌入式硬件
QQ5286211241 天前
芯片程序提取的具体流程和方法
stm32·单片机·嵌入式硬件·程序·pcb抄板
LingzhiPi1 天前
零知派ESP32-S3-智能小车控制系统(4.2)-红外双目跟随模块使用
单片机·嵌入式硬件
智者知已应修善业2 天前
【proteus交通灯故障检测的完善方案】2025-4-29
经验分享·笔记·单片机·嵌入式硬件·proteus
LingzhiPi2 天前
零知ESP32--RC522NFC考勤打卡系统
c++·单片机·嵌入式硬件
炸膛坦客2 天前
常见代码问题与解答:(二)因外部晶振频率改变导致 MCU 锁死的原因及处理方法
单片机·嵌入式硬件