第三章:嵌入式软件架构设计原则
好,咱们今天聊聊架构设计原则。说实话,我见过太多嵌入式项目,一开始代码写得飞起,三个月后连自己都看不懂了。为什么会这样?说白了,就是架构没打好底子。
嵌入式软件和桌面软件不一样。资源有限,实时性要求高,出了问题可能还得飞过去现场调试。所以架构设计,从一开始就得想清楚。我个人习惯,先定原则,再动手写代码。
3.1 模块化:把大问题拆成小问题
模块化,就是把一个复杂系统拆成若干个独立的功能单元。每个模块只做一件事,做好一件事。
我在项目中遇到过这样一个案例:一个智能家居网关,初期把所有功能都塞在一个文件里——WiFi连接、MQTT协议解析、传感器数据采集、本地存储……全混在一起。结果呢?改一个WiFi重连逻辑,把传感器采集的定时器给改崩了。这就是典型的「牵一发而动全身」。
模块化的核心要点:
- 单一职责:一个模块只负责一个功能
- 接口清晰:模块之间通过定义好的接口通信
- 独立编译:每个模块可以单独编译测试
举个例子,一个典型的嵌入式系统可以拆成这样:
// 模块划分示例
project/
├── drivers/ // 硬件驱动层
│ ├── uart.c
│ ├── spi.c
│ └── gpio.c
├── middleware/ // 中间件层
│ ├── mqtt.c
│ ├── flash_fs.c
│ └── ota.c
├── app/ // 应用层
│ ├── sensor_mgr.c
│ ├── network_mgr.c
│ └── device_ctrl.c
└── os/ // 操作系统抽象层
├── task.c
└── queue.c
嗯,这里要注意:模块化不是越细越好。拆得太碎,接口数量爆炸,反而增加复杂度。我一般建议,一个模块的代码量控制在500-2000行之间。
3.2 分层:让每一层各司其职
分层架构,说白了就是「上层调用下层,下层不依赖上层」。你想想看,如果底层驱动直接调用了应用层的函数,那这个系统基本就废了——改个驱动,应用层也得跟着改。
嵌入式系统常见的分层方式:
| 层次 | 职责 | 典型内容 |
|---|---|---|
| 应用层 | 业务逻辑 | 传感器数据处理、用户交互 |
| 中间件层 | 协议栈、文件系统 | MQTT、FATFS、OTA |
| 驱动抽象层 | 硬件接口封装 | UART、SPI、I2C 操作接口 |
| 硬件驱动层 | 直接操作寄存器 | GPIO配置、中断处理 |
我曾经接手过一个项目,驱动层直接调用了应用层的回调函数。结果换了一个MCU,所有驱动都得重写,因为应用层的回调接口变了。这就是分层没做好的代价。
我的建议:每一层都定义好「向上提供的接口」和「向下需要的接口」。接口用结构体封装,方便替换实现。
3.3 高内聚低耦合:模块内部要紧密,模块之间要松散
高内聚,就是模块内部的功能要紧密相关。低耦合,就是模块之间的依赖要尽可能少。
你想想看,如果一个模块既处理传感器数据,又负责网络通信,还管着LED闪烁——这就是低内聚。改传感器逻辑的时候,不小心把网络通信的缓冲区给改了,bug就来了。
低耦合怎么做?我分享一个实用技巧:
// 不好的设计:模块之间直接调用
void sensor_task(void) {
int data = read_sensor();
network_send(data); // 直接依赖网络模块
}
// 好的设计:通过消息队列解耦
void sensor_task(void) {
int data = read_sensor();
msg_queue_send(MSG_SENSOR_DATA, &data); // 只发消息,不管谁处理
}
void network_task(void) {
msg_t msg;
msg_queue_recv(&msg);
if (msg.id == MSG_SENSOR_DATA) {
network_send(msg.data);
}
}
我在项目中遇到过最典型的耦合问题:全局变量满天飞。一个全局变量被5个模块读写,改一个地方,其他4个地方全崩。后来我强制要求:模块之间只能通过函数接口或消息队列通信,禁止直接访问全局变量。
避坑指南:我曾经因为一个全局的「系统状态标志」被多个模块同时修改,导致死锁。后来改成每个模块维护自己的状态,通过事件通知机制同步。从此世界清净了。
3.4 关注点分离:把不同的事情分开处理
关注点分离,就是把不同维度的逻辑拆开。比如:业务逻辑和硬件操作分开,数据处理和界面显示分开,实时任务和非实时任务分开。
说白了,就是「别把鸡蛋放在一个篮子里」。一个函数里既做数学计算,又操作GPIO,还打印日志——这就是典型的关注点混在一起。
我常用的做法:
- 业务逻辑:只关心「做什么」,不关心「怎么做」
- 硬件操作:只关心「怎么控制硬件」,不关心「为什么控制」
- 日志调试:独立模块,不影响主流程
- 错误处理:统一策略,不分散在各处
举个例子:
// 关注点混在一起(不推荐)
void handle_button_press(void) {
// 业务逻辑
if (press_count > 5) {
// 硬件操作
GPIO_ResetBits(GPIOB, GPIO_Pin_12);
// 日志
printf("enter config mode\n");
// 状态管理
system_mode = MODE_CONFIG;
}
}
// 关注点分离(推荐)
void handle_button_press(void) {
if (is_config_mode_triggered()) { // 只判断业务逻辑
event_post(EVENT_ENTER_CONFIG); // 发事件,不直接操作
}
}
void event_handler(event_t evt) {
switch (evt) {
case EVENT_ENTER_CONFIG:
led_set_config_mode(); // 硬件操作交给驱动
log_info("enter config mode"); // 日志交给日志模块
system_set_mode(MODE_CONFIG); // 状态管理交给状态模块
break;
}
}
嗯,这里要注意:关注点分离不是让代码变复杂,而是让每个关注点可以独立修改、独立测试。你想想看,如果业务逻辑和硬件操作混在一起,换一个LED灯的控制方式,业务逻辑也得跟着改——这不合理。
总结一下这四个原则的关系:
- 模块化是基础,把系统拆成小块
- 分层是组织方式,让小块有层次
- 高内聚低耦合是质量指标,衡量拆得好不好
- 关注点分离是设计思路,指导怎么拆
这四个原则不是孤立的,它们互相支撑。模块化做得好,自然就高内聚低耦合;分层清晰了,关注点自然就分开了。
最后说一句:这些原则不是教条,是经验。我在项目里踩过的坑,总结出来就是这些原则。你刚开始可能觉得「多此一举」,但等你维护一个10万行代码的嵌入式项目时,就会明白——好的架构设计,省的是你未来的时间。