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_1、g_2 这种。我花了整整两周才理清数据流。从那以后,我要求团队必须用 g_模块名_变量名 的格式命名全局变量。
3.5 四个原则怎么配合?
这四个原则不是孤立的。模块化是基础,分层是结构,可移植性是目标,可维护性是保障。我一般这样落地:
- 先按功能拆模块(模块化)
- 再按层级组织代码目录(分层)
- 硬件相关代码用抽象接口隔离(可移植性)
- 代码规范、注释、单元测试跟上(可维护性)
举个例子,一个安防主机的固件,目录结构大概这样:
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
嗯,这套结构我用在好几个项目里了,效果都不错。新同事上手快,换平台也方便。你如果刚开始做固件架构,不妨从这套模板开始,慢慢调整成适合自己的风格。
最后说一句:架构设计没有银弹。别为了追求完美而过度设计。我见过有人花两个月设计架构,结果代码只写了两周。记住,架构是为代码服务的,不是反过来。