4. 工程目录结构设计

在STM32产品级工程中,目录结构不是“把文件放整齐”这么简单,它直接决定了代码的可移植性、可测试性和团队协作效率。一个没有分层约束的工程,往往在半年后演变为“所有文件堆在Core和User下、驱动和应用互相调用、换一颗MCU就要重写业务逻辑”的泥潭。本节从分层思想出发,给出可直接落地的目录组织方案。

一、核心原理:分层与依赖方向

分层设计的本质是控制依赖方向。上层可以调用下层,下层绝不能反向依赖上层。在STM32工程中,通常划分为五层:应用层、中间件层、驱动层、硬件抽象层(HAL/LL)、配置层。配置层与文档层、脚本层属于支撑性目录,不参与运行时调用链,但贯穿整个工程生命周期。

依赖方向应严格自上而下:应用层 → 中间件层 → 驱动层 → HAL/LL → 寄存器。任何跨层反向调用(例如驱动层直接调用应用层的业务函数)都会破坏可移植性。判断分层是否合格,有一个简单标准:把应用层整体删除后,驱动层和中间件层仍应能独立编译通过。

STM32 产品级工程分层与目录映射 应用层 Application 业务逻辑、状态机、任务调度、产品功能 中间件层 Middleware 协议栈、文件系统、算法库、事件总线 驱动层 Drivers 外设驱动、传感器、执行器、板级支持 硬件抽象层 HAL / LL ST官方库、寄存器封装、芯片相关代码 配置层 Config 引脚映射、功能开关、参数表、版本信息 工程目录树(推荐) Project/ ├─ App/ # 应用层 ├─ Middleware/ # 中间件层 ├─ Drivers/ # 驱动层 ├─ HAL/ # 硬件抽象层 ├─ Config/ # 配置层 ├─ Doc/ # 文档层 ├─ Scripts/ # 脚本层 ├─ Bsp/ # 板级支持 ├─ ThirdParty/ # 第三方库 ├─ Build/ # 构建输出 └─ README.md 禁止反向依赖 Drivers 不得 include App 头文件 Middleware 不得直接操作寄存器

二、各层职责与目录组织

驱动层(Drivers)负责把芯片外设和板载器件封装成统一接口。建议按器件类型再分子目录,例如 Drivers/Inc 与 Drivers/Src,或按模块划分 drv_uart.c、drv_spi_flash.c、drv_oled.c。驱动层只暴露句柄和操作函数,不包含业务判断。

中间件层(Middleware)放置与具体硬件无关的通用能力,如 Modbus 协议栈、环形缓冲区、状态机框架、日志组件、参数管理。中间件层通过驱动层提供的接口访问硬件,自身不直接调用 HAL。

应用层(App)是产品逻辑的唯一归属地。建议按功能域拆分,如 App/Sensor、App/Comm、App/Control,每个功能域内部再分 Inc 与 Src。应用层可以调用中间件和驱动,但不得被下层调用。

配置层(Config)集中管理所有可变参数:引脚定义、功能宏开关、通信地址、版本号、校准系数。典型文件包括 board_config.h、app_config.h、version.h。配置层应做到“改配置不改逻辑”,换板子时只动这一层。

文档层(Doc)与脚本层(Scripts)是工程可维护性的保障。文档层存放硬件说明、协议文档、测试报告、变更记录;脚本层存放编译、烧录、格式化、版本生成等自动化脚本。

三、目录结构清单

目录 职责 典型内容 依赖方向
App/产品业务逻辑任务、状态机、控制算法可依赖 Middleware、Drivers
Middleware/通用软件能力协议栈、日志、参数管理可依赖 Drivers
Drivers/外设与器件驱动UART、SPI Flash、传感器可依赖 HAL
HAL/芯片抽象ST HAL/LL 库仅依赖寄存器
Config/集中配置引脚、宏、版本、参数表被各层引用
Doc/工程文档硬件说明、协议、测试报告不参与编译
Scripts/自动化脚本build.bat、flash.py、version.py不参与编译

四、案例:从“一锅粥”到分层重构

某工业采集器最初把 UART 解析、Modbus 组包、传感器读取全部写在 main.c 中,换用 STM32F4 后无法复用。重构后目录如下:

Project/
├─ App/
│  ├─ Inc/  app_main.h  app_sample.h
│  └─ Src/  app_main.c  app_sample.c
├─ Middleware/
│  ├─ Modbus/  modbus_rtu.c  modbus_rtu.h
│  └─ RingBuf/ ringbuf.c     ringbuf.h
├─ Drivers/
│  ├─ Inc/  drv_uart.h  drv_adc.h
│  └─ Src/  drv_uart.c  drv_adc.c
├─ Config/
│  ├─ board_config.h
│  ├─ app_config.h
│  └─ version.h
├─ Doc/
│  ├─ hardware.md
│  └─ protocol_modbus.md
└─ Scripts/
   ├─ build.bat
   └─ gen_version.py

重构后,Modbus 中间件不再直接操作寄存器,而是调用 drv_uart_send();应用层只关心“采集—组包—上报”的业务流程。更换 MCU 时,只需替换 HAL 与驱动层实现,应用层和中间件层几乎零修改。

落地建议:在工程根目录放置 README.md,用一张依赖图说明各层关系;在 Config/version.h 中统一维护版本号,由脚本自动生成,避免手工修改遗漏。

五、常见误区与复盘

  • 误区一:驱动层包含业务判断。例如在 UART 中断里直接解析协议并执行控制。正确做法是驱动只负责收发字节,协议解析交给中间件。
  • 误区二:配置散落各处。引脚宏定义在驱动里、功能开关在应用里,导致换板时到处搜索。应统一收敛到 Config 层。
  • 误区三:文档与脚本缺失。工程能编译但没人知道如何烧录、如何生成版本。脚本层应至少包含一键构建与版本生成。
  • 误区四:目录层级过深。超过四级的嵌套会增加 include 路径维护成本,建议每层不超过两级子目录。
复盘要点:分层不是目的,可替换、可测试、可协作才是。每次新增模块前先问三个问题——它属于哪一层?它依赖谁?谁依赖它?回答清楚再建目录,比事后重构成本低得多。

总结来看,STM32 产品级工程的目录结构应以“依赖单向、职责单一、配置集中、文档脚本配套”为原则。驱动层管硬件、中间件层管通用能力、应用层管业务、配置层管差异、文档与脚本层管可维护性。把这七层组织清楚,工程才具备从样机走向量产的骨架。