本文讨论的是嵌入式软件分层的通用原则并以常见的 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 / 外部 FlashLCD / OLED 屏电机、继电器、蜂鸣器无线通信模块设备层的核心价值是把底层总线访问和上层设备语义分开。例如上层希望调用的是temp_sensor_read()flash_write_page()motor_set_speed()而不是直接到处写HAL_SPI_Transmit()HAL_I2C_Mem_Read()HAL_GPIO_WritePin()设备层原则屏蔽总线和底层访问细节提供设备语义化接口不写业务流程不直接掺杂产品逻辑一句话理解device 负责“把器件变成可理解、可替换的设备接口”。4. bsp板级支持层BSPBoard Support Package层负责封装MCU 片上资源和板级资源。常见内容包括GPIOUARTSPII2CADCPWM定时器外部中断DMA板级初始化这一层的核心是把平台相关、板卡相关的细节封装起来对上提供稳定接口。比如哪个 UART 接哪个外设GPIO 高低电平是否反向板载 LED 接在哪个引脚某个 SPI 是不是复用 DMA这些都应该在 bsp 层被吸收掉而不是泄漏到业务代码中。BSP 层原则屏蔽 HAL / CubeMX 细节对上提供稳定板级接口不写设备语义不写业务逻辑一句话理解bsp 解决的是“这块板子怎么用”的问题。5. platform平台层平台层承接项目的最底层环境通常包括HALCMSISRTOS启动文件芯片厂商库CubeMX 自动生成代码这一层通常是平台官方提供的自动生成的与芯片架构密切相关的平台层原则尽量少改不承载业务代码上层不要直接依赖变更控制要谨慎一句话理解platform 是项目运行的“地基”但不是业务开发的主战场。6. component组件层组件层是一个横向复用层用于沉淀与业务无关、与硬件无关的基础能力。常见组件包括RingBufferCRCPID状态机框架软定时器FIFO字符串处理通用算法内存池事件分发器这些内容的特点是不依赖具体产品业务不依赖特定硬件可以跨多个项目复用组件层原则与业务无关与硬件无关可独立测试不反向依赖 app / service / device / bsp一句话理解component 是“可沉淀、可复用、可迁移”的基础能力库。四、这套分层里最核心的 7 条原则如果把整套规范压缩成最重要的设计原则本质上就是以下 7 条1单一职责一个模块只做一类事不混职责。比如设备驱动只做设备访问业务状态机只做业务决策板级封装只做资源适配不要一边读传感器一边顺手做业务判断再顺手发协议。2高内聚、低耦合模块内部要聚焦模块之间依赖要清晰且尽量少。好的模块应该是内部逻辑彼此紧密相关对外暴露接口尽可能少改内部实现不影响上层使用3接口清晰模块之间通过稳定接口交互而不是靠extern全局变量到处穿透。不建议上层直接访问底层全局句柄多模块共享一堆裸变量不通过接口直接改状态建议用头文件声明清晰 API输入输出明确生命周期明确错误码明确4隐藏实现细节上层只关心“做什么”不关心“怎么做”。例如应用层只需要知道是否读取成功当前温度是多少而不需要知道是 I2C 还是 SPI是 DMA 还是轮询是否用了校验重试5禁止越层访问不能绕过中间层直接下探到底层。例如app 不应该直接调 HALservice 不应该直接访问寄存器device 不应该直接越过 bsp 去碰 platform 细节因为一旦越层边界就失效后续维护会越来越难。6可替换好的分层设计应该让底层变化尽量不影响上层。比如换 MCUapp 基本不用改换传感器业务逻辑尽量不动换板卡device 和 service 尽量稳定这才说明分层真正起到了隔离变化的作用。7面向接口编程依赖稳定抽象而不是依赖具体实现。换句话说上层依赖“能力”不依赖“某个具体实现细节”这样项目才能逐步形成真正可扩展的架构。五、运行期调用关系应该怎么设计在运行期推荐采用如下调用关系app↓service / device↓bsp↓platform其中app 面向业务service 和device提供上层能力bsp 封装板级资源platform 提供底层运行环境而component是横向复用层可被以下层使用appservicedevicebsp也就是说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 HALCMSISFreeRTOS启动代码CubeMX 生成代码6是不是与业务和硬件都无关、可复用如果是放component例如环形缓冲区CRC 算法PID 控制器通用状态机框架判断优先级怎么排如果一个模块看起来“好像哪层都能放”建议按这个顺序判断先看职责再看依赖最后看复用性。这句话非常关键。因为一个模块该放哪不是看“它长得像谁”而是看它本质在解决什么问题它依赖哪些层它未来是否需要复用八、分层不是增加复杂度而是避免失控有些人会觉得“项目不大分这么多层是不是太重了”这个担心可以理解但要注意分层不是为了制造复杂度而是为了防止复杂度失控。项目小的时候不分层似乎也能跑。但随着时间推移项目一定会遇到版本迭代硬件变更功能增加多人协作Bug 修复模块复用平台迁移真正让项目变难维护的从来不是“代码量大”而是变化没有边界。而分层设计的价值就在于给这些变化加上边界。九、一句话总结如果要快速理解这套架构可以直接用下面这段话做统一认知app 负责业务service 负责通用服务device 负责设备抽象bsp 负责板级资源封装platform 负责底层平台环境component 负责可复用基础组件再配上一条最重要的原则职责要单一依赖要单向变化要隔离。十、结语嵌入式项目的代码最怕的不是写得慢而是后面没人敢改。一旦职责混乱、边界失控项目就会进入一种非常痛苦的状态改一个点影响一大片修一个 Bug带出两个新问题每次重构都像拆炸弹每次硬件变更都像推倒重来所以分层的意义不在于“形式规范”而在于把系统中不同类型的变化控制在各自应该存在的边界内。这也是软件架构真正的价值所在。
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业务变化比如命令定义变化控制策略升级状态机调整业务流程变化参数管理逻辑变更如果项目没有分层那么这些变化往往会互相扩散改业务动到底层驱动换设备影响命令处理板级引脚一改上层逻辑也得跟着改某个函数能不能删没人敢确定最后就会变成一种很典型的状态代码不是不能跑而是不能维护。所以分层真正要解决的不是“代码风格”问题而是让不同类型的问题在各自边界内闭环处理。也就是说平台问题在平台层解决板级问题在板级层解决设备问题在设备层解决业务问题在应用层解决这样变化才不会无序蔓延。二、推荐的六层架构在嵌入式项目中推荐采用下面这套六层结构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 / 外部 FlashLCD / OLED 屏电机、继电器、蜂鸣器无线通信模块设备层的核心价值是把底层总线访问和上层设备语义分开。例如上层希望调用的是temp_sensor_read()flash_write_page()motor_set_speed()而不是直接到处写HAL_SPI_Transmit()HAL_I2C_Mem_Read()HAL_GPIO_WritePin()设备层原则屏蔽总线和底层访问细节提供设备语义化接口不写业务流程不直接掺杂产品逻辑一句话理解device 负责“把器件变成可理解、可替换的设备接口”。4. bsp板级支持层BSPBoard Support Package层负责封装MCU 片上资源和板级资源。常见内容包括GPIOUARTSPII2CADCPWM定时器外部中断DMA板级初始化这一层的核心是把平台相关、板卡相关的细节封装起来对上提供稳定接口。比如哪个 UART 接哪个外设GPIO 高低电平是否反向板载 LED 接在哪个引脚某个 SPI 是不是复用 DMA这些都应该在 bsp 层被吸收掉而不是泄漏到业务代码中。BSP 层原则屏蔽 HAL / CubeMX 细节对上提供稳定板级接口不写设备语义不写业务逻辑一句话理解bsp 解决的是“这块板子怎么用”的问题。5. platform平台层平台层承接项目的最底层环境通常包括HALCMSISRTOS启动文件芯片厂商库CubeMX 自动生成代码这一层通常是平台官方提供的自动生成的与芯片架构密切相关的平台层原则尽量少改不承载业务代码上层不要直接依赖变更控制要谨慎一句话理解platform 是项目运行的“地基”但不是业务开发的主战场。6. component组件层组件层是一个横向复用层用于沉淀与业务无关、与硬件无关的基础能力。常见组件包括RingBufferCRCPID状态机框架软定时器FIFO字符串处理通用算法内存池事件分发器这些内容的特点是不依赖具体产品业务不依赖特定硬件可以跨多个项目复用组件层原则与业务无关与硬件无关可独立测试不反向依赖 app / service / device / bsp一句话理解component 是“可沉淀、可复用、可迁移”的基础能力库。四、这套分层里最核心的 7 条原则如果把整套规范压缩成最重要的设计原则本质上就是以下 7 条1单一职责一个模块只做一类事不混职责。比如设备驱动只做设备访问业务状态机只做业务决策板级封装只做资源适配不要一边读传感器一边顺手做业务判断再顺手发协议。2高内聚、低耦合模块内部要聚焦模块之间依赖要清晰且尽量少。好的模块应该是内部逻辑彼此紧密相关对外暴露接口尽可能少改内部实现不影响上层使用3接口清晰模块之间通过稳定接口交互而不是靠extern全局变量到处穿透。不建议上层直接访问底层全局句柄多模块共享一堆裸变量不通过接口直接改状态建议用头文件声明清晰 API输入输出明确生命周期明确错误码明确4隐藏实现细节上层只关心“做什么”不关心“怎么做”。例如应用层只需要知道是否读取成功当前温度是多少而不需要知道是 I2C 还是 SPI是 DMA 还是轮询是否用了校验重试5禁止越层访问不能绕过中间层直接下探到底层。例如app 不应该直接调 HALservice 不应该直接访问寄存器device 不应该直接越过 bsp 去碰 platform 细节因为一旦越层边界就失效后续维护会越来越难。6可替换好的分层设计应该让底层变化尽量不影响上层。比如换 MCUapp 基本不用改换传感器业务逻辑尽量不动换板卡device 和 service 尽量稳定这才说明分层真正起到了隔离变化的作用。7面向接口编程依赖稳定抽象而不是依赖具体实现。换句话说上层依赖“能力”不依赖“某个具体实现细节”这样项目才能逐步形成真正可扩展的架构。五、运行期调用关系应该怎么设计在运行期推荐采用如下调用关系app↓service / device↓bsp↓platform其中app 面向业务service 和device提供上层能力bsp 封装板级资源platform 提供底层运行环境而component是横向复用层可被以下层使用appservicedevicebsp也就是说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 HALCMSISFreeRTOS启动代码CubeMX 生成代码6是不是与业务和硬件都无关、可复用如果是放component例如环形缓冲区CRC 算法PID 控制器通用状态机框架判断优先级怎么排如果一个模块看起来“好像哪层都能放”建议按这个顺序判断先看职责再看依赖最后看复用性。这句话非常关键。因为一个模块该放哪不是看“它长得像谁”而是看它本质在解决什么问题它依赖哪些层它未来是否需要复用八、分层不是增加复杂度而是避免失控有些人会觉得“项目不大分这么多层是不是太重了”这个担心可以理解但要注意分层不是为了制造复杂度而是为了防止复杂度失控。项目小的时候不分层似乎也能跑。但随着时间推移项目一定会遇到版本迭代硬件变更功能增加多人协作Bug 修复模块复用平台迁移真正让项目变难维护的从来不是“代码量大”而是变化没有边界。而分层设计的价值就在于给这些变化加上边界。九、一句话总结如果要快速理解这套架构可以直接用下面这段话做统一认知app 负责业务service 负责通用服务device 负责设备抽象bsp 负责板级资源封装platform 负责底层平台环境component 负责可复用基础组件再配上一条最重要的原则职责要单一依赖要单向变化要隔离。十、结语嵌入式项目的代码最怕的不是写得慢而是后面没人敢改。一旦职责混乱、边界失控项目就会进入一种非常痛苦的状态改一个点影响一大片修一个 Bug带出两个新问题每次重构都像拆炸弹每次硬件变更都像推倒重来所以分层的意义不在于“形式规范”而在于把系统中不同类型的变化控制在各自应该存在的边界内。这也是软件架构真正的价值所在。