1. 分层设计思想入门:为什么需要分层?从面包板跳线到工程思维的转变
我第一次带团队做单片机项目时,遇到过一个让我印象深刻的场景。一个刚毕业的同事,花了两周时间写了一个智能小车的控制程序。代码跑起来了,功能也正常。但当我打开他的工程文件时,发现整个程序只有一个 main.c 文件,足足 2000 多行。
我问他:「如果现在要换一个电机驱动芯片,你需要改多少行代码?」
他愣了几秒,然后开始翻代码。翻了十分钟,告诉我:「大概要改 400 多行,因为 PWM 控制、逻辑判断、甚至按键响应都混在一起了。」
这就是典型的「面包板跳线式编程」——所有功能像跳线一样直接连在一起,看着能用,但一动就全乱。
从面包板到工程思维
回想一下你刚开始学单片机时的样子。在面包板上插几根杜邦线,LED 亮了,蜂鸣器响了,心里特别有成就感。那时候的程序也很简单:
void main() {
while(1) {
if(按键按下) {
LED亮;
延时500ms;
LED灭;
}
}
}
这种写法,我称之为「裸奔式编程」。对于单个 LED 闪烁、单个按键控制,完全够用。但项目一旦复杂起来,问题就暴露了。
核心矛盾:硬件越来越复杂,需求越来越多,但你的代码结构还停留在「面包板思维」。
不分层的代价
我在一个工业控制项目中吃过亏。当时要做一个多路温度采集系统,用了 8 个 DS18B20 传感器。同事把传感器驱动、数据处理、LCD 显示、按键菜单全部写在一个文件里。结果呢?
- 改一个传感器型号——需要翻遍整个文件找所有与传感器相关的代码
- 换一个 LCD 屏幕——显示代码和业务逻辑缠在一起,拆都拆不开
- 加一个功能——比如增加数据存储,发现无处下手,因为所有模块都耦合在一起
那个项目最后延期了两个月。原因不是技术难,而是代码太乱,改一处崩三处。
我曾经踩过的坑:一个看似简单的功能修改,因为代码没有分层,导致我花了三天时间排查一个「按键抖动导致误触发」的问题。后来发现,问题出在按键扫描代码和电机控制代码共用了一个全局变量。如果当时做了分层隔离,这种低级错误根本不会发生。
分层设计是什么?
说白了,分层就是把代码按照职责拆成不同的「层」。每一层只做自己该做的事,层与层之间通过明确的接口通信。
你想想看,一个典型的单片机项目,通常包含这些职责:
| 职责 | 举例 | 属于哪一层 |
|---|---|---|
| 直接操作寄存器 | GPIO 置高置低、定时器配置 | 硬件抽象层 |
| 驱动外设芯片 | 初始化 DS18B20、读取温度值 | 驱动层 |
| 业务逻辑 | 温度超过 50°C 就报警 | 应用层 |
| 人机交互 | 按键扫描、菜单显示 | 界面层 |
每一层都像是一个独立的模块。硬件抽象层不知道什么叫「报警」,应用层不知道 GPIO 寄存器地址是多少。它们各司其职,通过函数调用或消息传递来协作。
分层带来的好处
我个人的习惯是,不管项目大小,先画一个分层图。哪怕只有三个文件,也要把层次理清楚。这样做的好处,我在实际项目中体会很深:
- 可移植性:换芯片时,只需要改硬件抽象层。应用层代码基本不用动。我做过一个项目,从 STM32F1 移植到 GD32,只花了一天时间,因为底层封装得好。
- 可维护性:每个文件职责单一,找 bug 的范围大大缩小。驱动出问题查驱动层,逻辑出问题查应用层。
- 可测试性:可以单独测试每一层。比如写一个模拟的传感器驱动,就能在硬件还没做好的时候先调试业务逻辑。
- 团队协作:不同的人可以同时开发不同层。一个人写驱动,一个人写应用,互不干扰。
一个小技巧:刚开始做分层时,不要追求完美。先做到「硬件操作」和「业务逻辑」分开,就已经比 90% 的初学者强了。剩下的层次可以慢慢加。
一个简单的分层示例
假设我们要做一个温控风扇:温度超过 30°C 时启动风扇,低于 25°C 时关闭。
不分层的写法:
// 所有代码混在一起
void main() {
while(1) {
// 直接操作传感器
start_conversion();
delay(750);
int temp = read_temperature();
// 直接操作风扇
if(temp > 30) {
GPIOA->ODR |= (1 << 5); // 开风扇
} else if(temp < 25) {
GPIOA->ODR &= ~(1 << 5); // 关风扇
}
}
}
分层后的写法:
// 硬件抽象层:hal_gpio.c
void hal_gpio_set_pin(uint8_t pin, uint8_t level) {
if(level) GPIOA->ODR |= (1 << pin);
else GPIOA->ODR &= ~(1 << pin);
}
// 驱动层:ds18b20.c
float ds18b20_read_temp(void) {
start_conversion();
delay_ms(750);
return read_temperature();
}
// 应用层:app_fan.c
void app_fan_control(float temp) {
if(temp > 30.0f) {
hal_gpio_set_pin(5, 1); // 开风扇
} else if(temp < 25.0f) {
hal_gpio_set_pin(5, 0); // 关风扇
}
}
// 主函数:main.c
void main() {
while(1) {
float temp = ds18b20_read_temp();
app_fan_control(temp);
delay_ms(1000);
}
}
看出区别了吗?分层之后,每个文件只做一件事。如果以后要换一个温度传感器,只需要改 ds18b20.c,其他文件完全不用动。
分层设计的核心原则
嗯,这里要注意几个原则,是我这些年总结出来的:
- 单向依赖:上层可以调用下层,但下层不能调用上层。应用层可以调用驱动层,但驱动层不能反过来调用应用层。
- 接口稳定:层与层之间的接口一旦确定,尽量不要改。接口变了,所有调用它的地方都要改,那就失去了分层的意义。
- 职责单一:每一层只做自己职责范围内的事。驱动层只管「怎么读写硬件」,不管「什么时候读写」。
- 适度抽象:不要过度设计。三层能解决的问题,不要搞五层。我见过有人把简单的 LED 控制分了四层,结果代码量翻了三倍,维护起来更痛苦。
记住一句话:分层是为了让代码更容易理解、修改和复用,不是为了炫技。如果分层让你的项目变得更复杂,那说明你分错了。
本章知识结构图
下面这张图展示了从「面包板思维」到「分层设计思维」的转变路径,以及分层设计带来的核心价值:
什么时候开始分层?
我经常被问到这个问题。我的建议是:
- LED 闪烁、按键控制——不用分层,一个 main.c 就够了
- 超过 3 个外设——建议开始分层,至少把驱动和业务分开
- 多人协作——必须分层,否则代码根本没法合并
- 产品级项目——严格分层,还要考虑可移植性和可维护性
我的经验:哪怕是一个人的小项目,我也建议从第二个版本开始分层。第一个版本可以快速验证功能,第二个版本重构时引入分层。这样既能快速出成果,又能保证代码质量。