二、固件架构设计原则:分层架构、模块化设计、低耦合高内聚、可移植性
好,咱们直接进入正题。这一章聊的是固件架构的四大原则。说实话,我见过太多项目,一开始代码写得飞起,三个月后连自己都看不懂。为什么会这样?说白了,就是架构没想清楚。
我个人习惯,在写第一行代码之前,先花半天时间把架构图画出来。这就像盖房子,你得先有图纸,不能边砌墙边想窗户开在哪。下面我把这四条原则掰开揉碎了讲,每一条都是我踩过坑之后总结出来的。
2.1 分层架构:把代码像洋葱一样剥开
分层架构,说白了就是让代码各司其职。我一般分成三层:硬件抽象层(HAL)、中间层(Middleware)、应用层(Application)。
你想想看,如果传感器驱动和业务逻辑混在一起,换个传感器就得重写整个程序,那得多痛苦?
我的分层习惯:
- 硬件抽象层(HAL): 直接跟寄存器、外设打交道。比如I2C读写、GPIO控制。
- 中间层(Middleware): 封装通用功能。比如传感器数据解析、PID控制算法。
- 应用层(Application): 业务逻辑。比如温度超过30度就开风扇。
我在项目中遇到过一件事:有个同事把温湿度传感器的I2C地址直接写在了业务逻辑里。后来换了另一款传感器,他得满世界找那个地址,改了三次才改对。嗯,这就是不分层的后果。
// 错误示范:不分层
void check_temperature(void) {
uint8_t data[2];
i2c_read(0x76, 0x00, data, 2); // 硬编码地址
float temp = (data[0] << 8 | data[1]) * 0.01;
if (temp > 30.0) {
gpio_set(5, 1); // 硬编码引脚
}
}
// 正确示范:分层
// HAL层
float hal_sensor_read_temperature(void) {
uint8_t data[2];
i2c_read(SENSOR_ADDR, TEMP_REG, data, 2);
return (data[0] << 8 | data[1]) * 0.01;
}
// 应用层
void app_control_fan(void) {
float temp = hal_sensor_read_temperature();
if (temp > TEMP_THRESHOLD) {
hal_fan_on();
}
}
小技巧: 每层之间只通过接口函数通信,不要跨层调用。比如应用层不要直接调I2C驱动,必须经过HAL层。
2.2 模块化设计:一个萝卜一个坑
模块化设计,说白了就是把功能拆成独立的小单元。每个模块只做一件事,并且把这件事做好。
我一般这样划分模块:
| 模块名称 | 职责 | 接口 |
|---|---|---|
| sensor_manager | 管理所有传感器 | sensor_read(), sensor_init() |
| actuator_control | 控制执行器 | actuator_on(), actuator_off() |
| pid_controller | PID计算 | pid_calculate(), pid_reset() |
| data_logger | 数据记录 | log_write(), log_read() |
我曾经接手过一个项目,一个文件里塞了3000行代码,有传感器驱动、有PID算法、还有WiFi通信。你想改个阈值,得先花两天时间搞清楚哪些变量是全局的。嗯,那感觉就像在迷宫里找出口。
避坑指南: 我曾经把一个PID模块和传感器模块写在一起,结果想复用PID算法时,发现它依赖了传感器模块的全局变量。最后只能重写。所以,模块之间不要共享全局变量,用接口传参。
2.3 低耦合高内聚:让模块之间少说话,自己多干活
低耦合,就是模块之间依赖少。高内聚,就是模块内部功能紧密相关。
你想想看,如果A模块调用了B模块的10个函数,B又调用了C的5个函数,那改一个地方就得牵一发动全身。这就是耦合度太高。
我一般这样控制耦合:
- 接口最小化: 每个模块只暴露必要的函数,内部函数用static隐藏。
- 依赖注入: 模块需要的资源通过参数传入,不要自己去找。
- 事件驱动: 模块之间通过消息队列通信,而不是直接调用。
// 高耦合的例子
void sensor_task(void) {
float temp = read_temperature();
if (temp > 30) {
fan_on(); // 直接调用风扇模块
log_write("Fan on"); // 直接调用日志模块
}
}
// 低耦合的例子
void sensor_task(void) {
float temp = read_temperature();
event_publish(EVENT_TEMP_UPDATE, &temp);
// 风扇模块和日志模块自己订阅事件
}
我的经验: 用事件驱动可以大幅降低耦合。我在温室控制器里就用了简单的发布-订阅模式,传感器模块只管发数据,至于谁要用,它不关心。
2.4 可移植性:一次编写,到处编译
可移植性,说白了就是换芯片、换平台时,改的代码越少越好。
我一般这样做:
- 硬件抽象层隔离: 所有硬件相关代码都放在HAL层,应用层不直接操作寄存器。
- 条件编译: 用宏定义区分不同平台。
- 标准接口: 比如用POSIX风格的接口,而不是某个芯片特有的API。
// 可移植的代码示例
#ifdef STM32_PLATFORM
#include "stm32_i2c.h"
#define I2C_READ(addr, reg, data, len) stm32_i2c_read(addr, reg, data, len)
#elif ESP32_PLATFORM
#include "esp_i2c.h"
#define I2C_READ(addr, reg, data, len) esp_i2c_read(addr, reg, data, len)
#endif
float hal_sensor_read_temperature(void) {
uint8_t data[2];
I2C_READ(SENSOR_ADDR, TEMP_REG, data, 2);
return (data[0] << 8 | data[1]) * 0.01;
}
避坑指南: 我曾经为了省事,直接在应用层用了STM32的HAL库函数。后来项目要移植到GD32上,结果发现HAL库不兼容,只能重写。从那以后,我坚持在HAL层做一层封装,应用层只调用我自己的接口。
2.5 总结:这些原则怎么用?
嗯,说了这么多,其实就一句话:写代码前先想清楚架构。
我个人习惯,在项目开始前画一张架构图,标清楚每层的职责、模块之间的接口。然后按照这个图去写代码。这样即使项目做到一半换人接手,新人也很快能上手。
你想想看,如果代码架构清晰,调试的时候是不是也省心很多?出了问题,先定位是哪一层,再定位是哪个模块,最后看具体函数。而不是在几千行代码里大海捞针。
最后送大家一句话: 好的架构不是设计出来的,是改出来的。但一开始就乱写,后面想改都改不动。所以,从第一个项目开始,就养成好习惯吧。