系统架构设计原则:模块化、分层设计、高内聚低耦合、可扩展性与可维护性
做嵌入式系统设计,说白了就是在跟复杂度打交道。项目规模一大,代码量一上来,如果没有一套清晰的设计原则撑着,后期维护就是一场噩梦。我见过太多团队,前期图快,架构乱成一锅粥,最后改一个 bug 引出三个新 bug。
这一章,咱们就聊聊嵌入式系统架构设计的几个核心原则。这些原则不是纸上谈兵,是我在多个量产项目中反复验证过的。
模块化设计:把大问题拆成小问题
模块化,就是把一个复杂的系统,拆成若干个功能独立的模块。每个模块负责一件明确的事。
举个例子,一个智能家居网关,你可以拆成:
- Wi-Fi 通信模块:负责网络连接和数据收发
- Zigbee 协议栈模块:处理 Zigbee 设备的数据
- 设备管理模块:维护设备列表和状态
- 用户配置模块:存储和读取用户设置
- 日志模块:记录系统运行状态
每个模块都有清晰的接口定义。模块内部怎么实现,其他模块不用管。我习惯给每个模块单独建一个 .c 和 .h 文件,头文件里只暴露必要的函数接口。
模块化的核心好处:
- 并行开发:不同工程师可以同时开发不同模块
- 独立测试:每个模块可以单独测试,定位问题快
- 复用性强:写好的模块,下个项目直接拿来用
我的习惯:模块划分时,先画一个模块依赖图。箭头越少越好。如果两个模块互相依赖,说明划分有问题,需要重新思考。
分层设计:让每一层各司其职
分层设计,就是把系统按抽象层级从低到高排列。底层处理硬件细节,上层处理业务逻辑。
典型的嵌入式软件分层:
| 层级 | 职责 | 示例 |
|---|---|---|
| 应用层 | 业务逻辑、用户交互 | 温度报警逻辑、按键处理 |
| 服务层 | 协议封装、数据管理 | MQTT 客户端、文件系统 |
| 驱动抽象层 | 统一硬件接口 | SPI、I2C、UART 抽象接口 |
| 硬件驱动层 | 直接操作寄存器 | GPIO 控制、定时器配置 |
每一层只能调用它下面一层的接口。应用层不能直接操作寄存器,驱动抽象层不能处理业务逻辑。这个规矩一旦打破,代码就乱套了。
我曾经踩过的坑:在一个温控器项目中,应用层代码直接调用了 SPI 驱动的底层函数。后来换了另一款 MCU,SPI 寄存器地址变了,结果应用层代码也得跟着改。这就是分层没做好的代价。
正确的做法是:应用层调用服务层,服务层调用驱动抽象层,驱动抽象层再调用硬件驱动层。这样换 MCU 时,只需要重写最底层的硬件驱动,上面的代码基本不用动。
高内聚低耦合:模块内部要紧密,模块之间要松散
这两个词经常一起出现,但说的是两件事。
- 高内聚:一个模块内部的各个元素,应该紧密相关,共同完成一个明确的功能。
- 低耦合:模块之间的依赖关系要尽可能少,接口要简单清晰。
怎么判断内聚性好不好?如果一个模块里的函数,有一半跟模块的主要功能无关,那就是内聚性差。比如一个“通信模块”里,混进了“LED 闪烁控制”的代码,这就该重构了。
耦合度怎么控制?我有个简单的判断标准:修改一个模块的 .c 文件,如果导致其他模块也要跟着改,那耦合就太高了。
降低耦合的常用手段:
- 使用回调函数,而不是直接调用
- 通过消息队列传递数据,而不是共享全局变量
- 定义抽象接口,隐藏实现细节
举个例子,一个传感器数据采集模块,采集到数据后,不应该直接调用显示模块的函数。而是应该通过回调或者消息队列,把数据发出去。显示模块自己决定怎么处理这些数据。
// 低耦合的设计:使用回调函数
typedef void (*sensor_data_callback_t)(float temperature, float humidity);
void sensor_init(sensor_data_callback_t callback);
void sensor_start(void);
// 应用层注册回调
void app_handle_sensor_data(float temp, float hum) {
// 处理数据,比如更新显示或触发报警
}
void main(void) {
sensor_init(app_handle_sensor_data);
sensor_start();
while(1) {
// 主循环做其他事
}
}
这样,传感器模块和显示模块之间,只有一个回调函数的接口。换显示方式,传感器模块完全不用改。
可扩展性:为未来的变化留好接口
做产品,需求变更是常态。今天支持 3 个传感器,明天可能就要支持 10 个。今天用 Wi-Fi 通信,明天可能就要加蓝牙。
可扩展性,就是系统在不破坏现有功能的前提下,能方便地增加新功能。
我常用的方法:
- 插件式架构:把功能模块做成插件,通过配置文件决定加载哪些插件
- 策略模式:把算法封装成策略,运行时动态切换
- 预留接口:在硬件设计时就留出多余的 GPIO、SPI、I2C 接口
我的建议:设计接口时,参数尽量用结构体,而不是一个个单独的参数。这样以后要加参数,只需要在结构体里加字段,接口声明不用变。
可维护性:让代码好读、好改、好测
可维护性,说白了就是代码写出来,三个月后你自己还能看懂,别人也能接手。
几个关键点:
- 命名规范:变量名、函数名要能表达其用途。别用 a、b、c 这种名字。
- 注释到位:不是每行都注释,而是解释“为什么这么做”,而不是“做了什么”。
- 代码风格统一:缩进、括号位置、空行,整个项目保持一致。
- 配置集中管理:所有可调参数,放在一个头文件里,别散落在各个 .c 文件中。
我曾经接手过一个项目:代码里到处都是魔数(magic number),比如 delay(500) 里的 500 是什么意思?没人知道。后来我花了整整两天,才把所有的魔数替换成有意义的宏定义。从那以后,我要求团队所有项目必须用宏定义或枚举来命名常量。
知识体系结构图
下面这张图,把本章的几个核心原则和它们之间的关系梳理了一下。你可以把它当作设计时的检查清单。
这五个原则不是孤立的。模块化做得好,自然就高内聚低耦合。分层设计做得好,可维护性就高。你想想看,如果每个模块都职责清晰、接口简单,扩展新功能是不是就轻松多了?
嗯,这一章的内容就这些。记住,架构设计不是一次性的事。项目开发过程中,要不断审视代码结构,发现不合理的地方及时重构。好的架构是改出来的,不是画出来的。