2. 嵌入式固件架构设计原则:模块化、分层化、可移植性、可维护性

好,咱们接着聊。上一章我讲了架构设计到底有多重要,这一章咱们就落地,聊聊具体的设计原则。

做消防报警系统这么多年,我踩过的坑真不少。有时候回头看看,很多问题其实都是设计之初没想清楚。说白了,架构设计就是给整个固件打个地基。地基歪了,后面盖多少层楼都白搭。

我个人习惯,在动手写第一行代码之前,先问自己四个问题:

  • 这个模块能不能独立拆出来?
  • 上下层之间怎么隔离?
  • 换一颗MCU要改多少代码?
  • 三个月后我自己还能看懂吗?

这四个问题,正好对应了咱们要讲的四个原则:模块化、分层化、可移植性、可维护性

2.1 模块化:把大象装进冰箱,分三步

模块化,说白了就是「分而治之」。你想想看,一个消防报警系统,少说也有几十个功能模块——探测器采集、声光报警、联动控制、通信协议、故障诊断……如果全揉在一个文件里,那画面太美我不敢看。

我在项目中遇到过一件事。有一次接手一个老项目,整个固件就两个文件:main.cfunctions.cfunctions.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 可维护性:写给三个月后的自己看

可维护性,可能是最容易被忽视的原则。很多工程师觉得,代码能跑就行,管它好不好看。但我要说,代码是写给人看的,顺便给机器执行

我见过最离谱的代码,一个函数五百行,没有注释,变量名全是 abtmp。原作者离职后,没人敢动那个模块。最后整个项目推倒重来。

怎么提高可维护性?我总结了几个要点:

  • 命名规范:变量名、函数名要能「自解释」。别用 flag1status2,用 is_alarm_activecommunication_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 四个原则的关系

这四个原则不是孤立的,它们相辅相成:

  • 模块化是基础,把系统拆成小块。
  • 分层化是骨架,把小块按层次组织起来。
  • 可移植性是目标,让代码能在不同平台间迁移。
  • 可维护性是保障,让代码能持续演进。

打个比方,模块化就像乐高积木的每个小零件,分层化就像搭建说明书,可移植性就像零件能兼容不同型号的底板,可维护性就像说明书写得清晰易懂。

缺了任何一个,整个架构都会出问题。模块化不好,改一个功能牵一发动全身;分层化不好,换平台等于重写;可移植性不好,每次新项目都从零开始;可维护性不好,代码就是一堆会跑的垃圾。

嗯,这一章的内容就到这儿。下一章,咱们聊聊具体的架构分层模型,看看每一层到底该怎么设计。到时候我会拿一个实际的消防报警项目来拆解,保证干货满满。