2. 嵌入式固件架构设计原则:模块化、分层化、可移植性、可维护性
好,咱们接着聊。上一章我讲了架构设计到底有多重要,这一章咱们就落地,聊聊具体的设计原则。
做消防报警系统这么多年,我踩过的坑真不少。有时候回头看看,很多问题其实都是设计之初没想清楚。说白了,架构设计就是给整个固件打个地基。地基歪了,后面盖多少层楼都白搭。
我个人习惯,在动手写第一行代码之前,先问自己四个问题:
- 这个模块能不能独立拆出来?
- 上下层之间怎么隔离?
- 换一颗MCU要改多少代码?
- 三个月后我自己还能看懂吗?
这四个问题,正好对应了咱们要讲的四个原则:模块化、分层化、可移植性、可维护性。
2.1 模块化:把大象装进冰箱,分三步
模块化,说白了就是「分而治之」。你想想看,一个消防报警系统,少说也有几十个功能模块——探测器采集、声光报警、联动控制、通信协议、故障诊断……如果全揉在一个文件里,那画面太美我不敢看。
我在项目中遇到过一件事。有一次接手一个老项目,整个固件就两个文件:main.c 和 functions.c。functions.c 足足有八千行。改一个探测器型号,要翻遍整个文件找所有相关的宏定义和函数。那感觉,就像在垃圾堆里找一枚针。
所以,模块化到底怎么做?我总结了三条:
- 高内聚:一个模块只做一件事,并且把它做好。比如探测器驱动模块,只负责采集数据和状态判断,别把通信协议也塞进去。
- 低耦合:模块之间通过接口通信,别直接访问对方的全局变量。接口越简单越好,参数越少越好。
- 接口清晰:每个模块暴露出来的函数,命名要有规律,功能要单一。我习惯用
模块名_动作_对象的命名方式。
举个例子,一个探测器模块的接口可以这样设计:
// detector.h
#ifndef __DETECTOR_H__
#define __DETECTOR_H__
#include "stdint.h"
// 探测器状态枚举
typedef enum {
DETECTOR_STATE_NORMAL = 0,
DETECTOR_STATE_ALARM,
DETECTOR_STATE_FAULT,
DETECTOR_STATE_DISABLE
} detector_state_t;
// 初始化探测器(指定通道)
void detector_init(uint8_t channel);
// 获取探测器当前状态
detector_state_t detector_get_state(uint8_t channel);
// 复位探测器
void detector_reset(uint8_t channel);
// 自检探测器
void detector_self_test(uint8_t channel);
#endif /* __DETECTOR_H__ */
你看,这个模块只做三件事:初始化、获取状态、复位和自检。别的模块想用探测器,只需要调用这几个函数就行,根本不用关心探测器底层是用的ADC还是IO口模拟。
小技巧:每个模块的 .h 文件,就是它的「用户手册」。别人看你的头文件,就应该知道这个模块怎么用,不需要去看 .c 文件。如果头文件超过200行,说明这个模块可能太胖了,该减肥了。
2.2 分层化:各司其职,互不越界
模块化解决的是「横向拆分」的问题,分层化解决的是「纵向隔离」的问题。
我习惯把固件分成三层:硬件抽象层(HAL)、中间层、应用层。每一层都有自己的职责,上层可以调用下层,但下层绝对不能反过来调用上层。
为什么会这样?你想想看,如果应用层直接操作寄存器,那换一颗MCU,所有应用代码都得重写。但如果应用层只调用HAL层的接口,那换MCU只需要改HAL层,应用层纹丝不动。
| 层次 | 职责 | 包含内容 | 依赖关系 |
|---|---|---|---|
| 应用层 | 业务逻辑 | 报警策略、联动控制、人机交互 | 只依赖中间层 |
| 中间层 | 协议解析、数据处理 | 通信协议栈、数据缓存、状态机 | 只依赖HAL层 |
| 硬件抽象层 | 硬件操作封装 | GPIO、UART、ADC、定时器驱动 | 直接操作寄存器 |
我记得有一次做项目,客户中途要求换MCU,从STM32F103换成GD32F103。当时我心里咯噔一下,但转念一想,我们的架构是分层的,HAL层封装得很好。结果呢?只花了三天就完成了移植,改了不到200行代码。应用层和中间层一行没动。
注意:分层不是越细越好。我见过有人分了七层,每层就几个函数,结果调用链长得像老太太的裹脚布。嵌入式系统资源有限,分层太多反而影响性能。三层到四层,足够了。
2.3 可移植性:一次编写,到处运行
可移植性,说白了就是「换平台不换逻辑」。做消防报警系统的,经常会遇到不同项目用不同MCU的情况。如果每次换MCU都要重写一遍应用逻辑,那效率太低了。
我个人的做法是:把平台相关的代码全部隔离到HAL层。应用层和中间层只使用标准化的接口,不直接操作任何寄存器。
举个例子,延时函数:
// hal_delay.h
#ifndef __HAL_DELAY_H__
#define __HAL_DELAY_H__
// 毫秒级延时
void hal_delay_ms(uint32_t ms);
// 微秒级延时
void hal_delay_us(uint32_t us);
#endif /* __HAL_DELAY_H__ */
在STM32上,这个函数用SysTick实现;在GD32上,用Timer实现;在国产某款MCU上,用空循环实现。但不管底层怎么变,应用层调用的都是 hal_delay_ms(100),完全不用改。
避坑指南:我曾经在某个项目里,直接在应用层用了 HAL_Delay()(STM32 HAL库自带的函数)。后来换MCU,发现新平台没有这个函数,结果满世界找替换,改得头皮发麻。从那以后,我坚持所有硬件操作必须经过自己的HAL层封装,哪怕只是多写一层转发。
2.4 可维护性:写给三个月后的自己看
可维护性,可能是最容易被忽视的原则。很多工程师觉得,代码能跑就行,管它好不好看。但我要说,代码是写给人看的,顺便给机器执行。
我见过最离谱的代码,一个函数五百行,没有注释,变量名全是 a、b、tmp。原作者离职后,没人敢动那个模块。最后整个项目推倒重来。
怎么提高可维护性?我总结了几个要点:
- 命名规范:变量名、函数名要能「自解释」。别用
flag1、status2,用is_alarm_active、communication_error_count。 - 注释到位:不是每行都注释,而是在关键逻辑、复杂算法、边界条件处加注释。我习惯在文件头部写一段「模块说明」,包括作者、日期、功能描述、修改记录。
- 代码风格统一:缩进用空格还是Tab?大括号换行还是不换行?这些细节要统一。我建议团队用代码格式化工具,比如 clang-format,自动统一风格。
- 避免魔法数字:别在代码里直接写
if (temp > 85),用宏定义#define TEMP_ALARM_THRESHOLD 85。这样别人一看就知道85是报警阈值,而不是随便写的数字。
一个反面教材:
// 这是啥?没人知道
void func(void)
{
if (a > 50) {
b = 1;
c = 2;
for (i = 0; i < 10; i++) {
d[i] = e[i] * 3;
}
}
}
改成这样,是不是清楚多了?
/**
* @brief 温度超限处理函数
* @param current_temp 当前温度值(单位:摄氏度)
* @note 当温度超过50度时,启动风扇并记录温度数据
*/
void handle_temperature_overlimit(uint8_t current_temp)
{
#define TEMP_THRESHOLD 50 // 温度报警阈值
#define FAN_SPEED_HIGH 1 // 风扇高速档
#define FAN_SPEED_LOW 2 // 风扇低速档
#define TEMP_RECORD_COUNT 10 // 温度记录数量
#define TEMP_SCALE_FACTOR 3 // 温度缩放系数
if (current_temp > TEMP_THRESHOLD) {
set_fan_speed(FAN_SPEED_HIGH);
set_fan_mode(FAN_SPEED_LOW);
for (uint8_t i = 0; i < TEMP_RECORD_COUNT; i++) {
temperature_records[i] = raw_temp_values[i] * TEMP_SCALE_FACTOR;
}
}
}
你看,加了注释、用了宏定义、变量名也清晰了。三个月后你回来看这段代码,一眼就能明白它在干什么。
我的习惯:每次提交代码前,我会问自己一个问题:「如果明天我被车撞了,同事能不能看懂我的代码?」如果答案是否定的,那就再改改。
2.5 四个原则的关系
这四个原则不是孤立的,它们相辅相成:
- 模块化是基础,把系统拆成小块。
- 分层化是骨架,把小块按层次组织起来。
- 可移植性是目标,让代码能在不同平台间迁移。
- 可维护性是保障,让代码能持续演进。
打个比方,模块化就像乐高积木的每个小零件,分层化就像搭建说明书,可移植性就像零件能兼容不同型号的底板,可维护性就像说明书写得清晰易懂。
缺了任何一个,整个架构都会出问题。模块化不好,改一个功能牵一发动全身;分层化不好,换平台等于重写;可移植性不好,每次新项目都从零开始;可维护性不好,代码就是一堆会跑的垃圾。
嗯,这一章的内容就到这儿。下一章,咱们聊聊具体的架构分层模型,看看每一层到底该怎么设计。到时候我会拿一个实际的消防报警项目来拆解,保证干货满满。