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

好,咱们今天聊聊固件架构设计的四个核心原则。说实话,我见过太多项目,一开始图省事,代码全堆在一个文件里。结果呢?后期改一个功能,牵一发动全身,测试人员天天加班,项目经理急得跳脚。我自己也踩过这种坑,所以今天把这些经验掰开了讲给你听。

3.1 模块化:把大象装进冰箱,得先切块

模块化,说白了就是「高内聚、低耦合」。每个模块只干一件事,而且干好。你想想看,一个函数如果超过200行,基本就该拆了。

核心要点:

  • 每个模块有清晰的接口(API)
  • 模块之间通过接口通信,不直接访问内部数据
  • 模块内部的变化,不影响外部

我在项目中遇到过最典型的反面教材:一个传感器驱动模块,里面居然混着Wi-Fi配网逻辑。后来要换传感器,结果Wi-Fi那块也跟着崩了。嗯,从那以后我定了个规矩——每个.c文件只对应一个功能实体。

/* 好的模块化示例 */
// sensor.h
#ifndef __SENSOR_H__
#define __SENSOR_H__

typedef struct {
    float temperature;
    float humidity;
} sensor_data_t;

int sensor_init(void);
int sensor_read(sensor_data_t *data);

#endif

// sensor.c
#include "sensor.h"

static int i2c_fd = -1;  // 内部私有,外部不可见

int sensor_init(void) {
    i2c_fd = i2c_open("/dev/i2c-1");
    if (i2c_fd < 0) return -1;
    return 0;
}

int sensor_read(sensor_data_t *data) {
    // 只做传感器读取,不做别的
    uint8_t buf[4];
    i2c_read(i2c_fd, 0x44, buf, 4);
    data->temperature = (buf[0] << 8 | buf[1]) * 0.01f;
    data->humidity = (buf[2] << 8 | buf[3]) * 0.01f;
    return 0;
}

我的小技巧:写模块时,先画接口图。把输入、输出、依赖关系画清楚,再动手写代码。这样能避免后期「拆东墙补西墙」。

3.2 分层:别让应用层直接操作寄存器

分层架构,我习惯分成三层:硬件抽象层(HAL)、中间件层、应用层。每一层只跟相邻层打交道,不能越级。

层级 职责 示例
应用层 业务逻辑、策略 报警策略、用户交互
中间件层 协议栈、算法、数据管理 MQTT、加密、文件系统
硬件抽象层 封装硬件差异 GPIO、UART、I2C驱动

为什么这么做?我举个例子。你写安防固件,门磁传感器可能用GPIO中断,也可能用I2C扩展IO。如果应用层直接操作寄存器,换硬件就得重写应用。但有了HAL,应用层只调用 door_sensor_status(),底层怎么实现它不管。

注意:分层不是越多越好。我见过有人分了7层,结果一个简单的读传感器操作,要经过5层函数调用,性能惨不忍睹。嵌入式系统资源有限,3层通常够了。

3.3 可移植性:一次编写,到处编译

可移植性,说白了就是你的代码换个芯片、换个板子,改几行配置就能跑。我早期做项目时,代码跟STM32 HAL库绑得死死的。后来客户要换成GD32,我差点没把键盘砸了。

怎么做到可移植?核心是「隔离变化」。把芯片相关的代码单独放一个目录,用条件编译或者抽象接口隔开。

/* 可移植性设计示例 */
// hal_gpio.h
#ifndef __HAL_GPIO_H__
#define __HAL_GPIO_H__

typedef enum {
    GPIO_LOW = 0,
    GPIO_HIGH
} gpio_level_t;

int hal_gpio_init(void);
void hal_gpio_set(int pin, gpio_level_t level);
gpio_level_t hal_gpio_get(int pin);

#endif

// hal_gpio_stm32.c  (STM32实现)
#include "hal_gpio.h"

int hal_gpio_init(void) {
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_0;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    HAL_GPIO_Init(GPIOA, &gpio);
    return 0;
}

// hal_gpio_gd32.c  (GD32实现)
#include "hal_gpio.h"

int hal_gpio_init(void) {
    rcu_periph_clock_enable(RCU_GPIOA);
    gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_0);
    return 0;
}

我的习惯:在项目根目录放一个 port 文件夹,里面按芯片型号分目录。换平台时,只需要改编译脚本和这个文件夹里的代码。应用层一行都不用动。

3.4 可维护性:写给未来的自己看

可维护性,我觉得是这四个原则里最重要的。你想想看,一个固件项目可能维护5年、10年。写代码的人可能早就离职了。新来的同事看着一堆「a、b、c」变量名,心里得多崩溃。

我总结了几条铁律:

  • 命名要自解释int cnt 不如 int retry_count
  • 注释写「为什么」,而不是「是什么」
  • 函数不超过50行,超过就拆
  • 每个文件开头写版权和功能说明
/* 可维护性差的代码 */
int a = 0;
if (a > 10) {
    // 处理
}

/* 可维护性好的代码 */
int sensor_fault_count = 0;
if (sensor_fault_count > MAX_FAULT_THRESHOLD) {
    // 传感器连续故障超过阈值,触发报警
    // 这里加10ms延时是为了避免抖动误报
    vTaskDelay(pdMS_TO_TICKS(10));
    trigger_alarm(ALARM_SENSOR_FAULT);
}

避坑指南:我曾经接手过一个项目,全局变量用了200多个,而且命名全是 g_1g_2 这种。我花了整整两周才理清数据流。从那以后,我要求团队必须用 g_模块名_变量名 的格式命名全局变量。

3.5 四个原则怎么配合?

这四个原则不是孤立的。模块化是基础,分层是结构,可移植性是目标,可维护性是保障。我一般这样落地:

  1. 先按功能拆模块(模块化)
  2. 再按层级组织代码目录(分层)
  3. 硬件相关代码用抽象接口隔离(可移植性)
  4. 代码规范、注释、单元测试跟上(可维护性)

举个例子,一个安防主机的固件,目录结构大概这样:

project/
├── app/                # 应用层
│   ├── alarm.c         # 报警逻辑
│   ├── user_if.c       # 用户交互
│   └── event_mgr.c     # 事件管理
├── middleware/         # 中间件层
│   ├── mqtt.c          # MQTT协议
│   ├── crypto.c        # 加密算法
│   └── storage.c       # 数据存储
├── hal/                # 硬件抽象层
│   ├── hal_gpio.c
│   ├── hal_uart.c
│   └── hal_i2c.c
├── port/               # 平台移植
│   ├── stm32f4/
│   └── gd32f3/
└── common/             # 公共组件
    ├── list.c
    └── queue.c

嗯,这套结构我用在好几个项目里了,效果都不错。新同事上手快,换平台也方便。你如果刚开始做固件架构,不妨从这套模板开始,慢慢调整成适合自己的风格。

最后说一句:架构设计没有银弹。别为了追求完美而过度设计。我见过有人花两个月设计架构,结果代码只写了两周。记住,架构是为代码服务的,不是反过来。