课程导论与项目全景:为什么需要分层?

大家好,欢迎来到这门实战课。

先聊个实在的。你写单片机代码多久了?有没有遇到过这种情况:项目刚开始时,代码跑得挺顺。三个月后,加一个新功能要改三个文件。半年后,改一个 bug 能引出两个新 bug。一年后,没人敢动那坨代码了。

嗯,这就是典型的「代码腐烂」。我见过太多项目死在这上面。

一个真实项目的腐烂史

几年前我带过一个智能家居网关项目。硬件平台是 STM32F407,外挂 WiFi、蓝牙、LCD 屏、温湿度传感器、继电器阵列。刚开始团队 3 个人,两周出了原型。

但问题很快就来了。

时间节点 代码规模 典型问题
第 1 个月 ~3000 行 所有代码写在 main.c 里,全局变量满天飞
第 3 个月 ~12000 行 WiFi 驱动和业务逻辑混在一起,换个 ESP8266 固件要改 5 个文件
第 6 个月 ~35000 行 LCD 显示函数里居然有继电器操作,没人说得清依赖关系
第 9 个月 ~60000 行 新同事入职两周,改了一个延时函数,整个 UI 卡死——因为那个延时被 17 个地方调用

为什么会这样?说白了,就是没有分层。硬件操作、业务逻辑、协议解析、用户界面,全搅在一起。你想想看,一个函数里既有 I2C 读写,又有状态机跳转,还有串口打印——这代码怎么维护?

我曾经踩过的坑:有一次客户要求把温湿度传感器从 SHT30 换成 DHT22。我以为只是改个驱动文件,结果发现传感器读取逻辑散落在 8 个模块里,改了整整三天,还引入了一个临界区 bug。从那以后,我强制团队所有项目必须做硬件抽象层。

分层到底解决了什么?

我个人的理解很简单:分层就是把「做什么」和「怎么做」分开。

  • 硬件驱动层:只管「怎么读写寄存器」。比如 SPI 发一个字节,I2C 读一个寄存器。
  • 硬件抽象层:把具体芯片的操作封装成通用接口。比如「读温度」,不管底下是 SHT30 还是 DHT22。
  • 中间件层:协议解析、数据缓存、状态机。不关心数据从哪来,只关心怎么处理。
  • 应用层:业务逻辑。比如「温度超过 30 度就开风扇」。

你可能会问:分层会不会增加代码量?会。但增加的是可复用的代码,不是重复的代码。我在项目中算过一笔账:

对比项 不分层 分层
首次开发时间 快(省了接口设计) 慢 20%~30%(接口设计耗时)
换硬件平台 重写 60%~80% 代码 只改驱动层,约 10%~15%
新功能添加 平均 3~5 天 平均 1~2 天
Bug 定位时间 2~8 小时 0.5~1 小时
团队新人上手 2~4 周 3~7 天

数据是我从三个项目里统计的。虽然不绝对精确,但趋势很明显。

课程目标:你要带走什么?

这门课不是讲理论。30 章内容,每一章都有可运行的代码。最终你会拿到一个完整的嵌入式软件框架,包含:

  • 一套通用的 MCU 硬件抽象层接口(GPIO、UART、SPI、I2C、Timer)
  • 一个 传感器驱动封装范例(以 SHT30 和 MPU6050 为例)
  • 一个 状态机框架(可移植到任何单片机)
  • 一个 消息队列与事件总线(模块间解耦的核心)
  • 完整的 项目工程模板(Keil / IAR / CMake 三套)
核心交付物:一个经过三个实际项目验证的「单片机软件分层参考架构」。你学完可以直接套在自己的项目上。

知识体系全景

下面这张图,是我做这门课之前画的架构总图。你第一眼看可能觉得复杂,别急,我们一章一章拆。

单片机软件分层架构全景图 单片机软件分层架构全景图 应用层(Application Layer) 业务逻辑 · 状态机 · 用户交互 · 策略调度 中间件层(Middleware Layer) 消息队列 · 事件总线 · 协议解析 · 数据缓存 硬件抽象层(HAL - Hardware Abstraction Layer) GPIO · UART · SPI · I2C · Timer · ADC 通用接口 驱动层(Driver Layer) 寄存器操作 · 芯片时序 · 中断服务 · DMA 配置 MCU 硬件(STM32 / GD32 / AT32 ...) 核心原则 • 上层不直接调用下层 • 每层只通过接口通信 • 依赖倒置:抽象不依赖具体 数据流向 请求:上 → 下 响应:下 → 上 事件:双向传递

这张图贯穿整个课程。每一章都会对应到图上的某一层或某几个层之间的接口。你学完一章,就能在图上找到它的位置。

最终交付物预览

课程结束时,你会拿到一个完整的工程仓库。结构大概是这样:

project_root/
├── drivers/              # 芯片驱动层
│   ├── gpio_stm32f4.c
│   ├── uart_stm32f4.c
│   └── i2c_stm32f4.c
├── hal/                  # 硬件抽象层接口
│   ├── hal_gpio.h
│   ├── hal_uart.h
│   └── hal_i2c.h
├── middleware/           # 中间件
│   ├── event_bus.c/h
│   ├── msg_queue.c/h
│   └── state_machine.c/h
├── sensors/              # 传感器驱动(基于 HAL)
│   ├── sht30.c/h
│   └── mpu6050.c/h
├── app/                  # 应用层
│   ├── temp_control.c/h
│   └── display_manager.c/h
├── port/                 # 移植层(不同 MCU 的适配)
│   └── stm32f407/
└── build/                # 构建脚本
    ├── keil/
    ├── iar/
    └── cmake/
我的建议:不要等到学完再动手。每章结束后,把代码下载到你的开发板上跑一遍。哪怕只改一个引脚配置,也比只看不动强十倍。