第3章:固件架构设计——分层架构与模块化原则
好,咱们直接进入正题。这一章讲的是固件架构设计,说白了就是怎么把代码组织得既清晰又抗造。我见过太多智能电表项目,前期图快,代码全揉在一个文件里,结果后期改一个功能,牵出一堆bug。嗯,这种坑我踩过不止一次。
3.1 分层架构:从硬件到应用的清晰路径
分层架构的核心思想,就是让每一层只关心自己的事。我习惯把智能电表固件分成五层:HAL、BSP、Driver、Service、App。你想想看,这样每一层都有明确的职责,改动起来心里有底。
| 层级 | 职责 | 典型内容 |
|---|---|---|
| App | 业务逻辑、用户交互 | 费率切换、数据显示、事件记录 |
| Service | 功能服务、状态管理 | 计量服务、通信服务、存储服务 |
| Driver | 外设驱动、寄存器操作 | LCD驱动、Flash驱动、UART驱动 |
| BSP | 板级支持、引脚映射 | GPIO配置、时钟配置、中断向量 |
| HAL | 硬件抽象、跨平台接口 | MCU通用接口、定时器抽象 |
我个人习惯从下往上设计。先搞定HAL层,把MCU的差异屏蔽掉。比如我换了一颗STM32到GD32,HAL层接口不变,上层代码几乎不用动。这就是分层的好处。
3.2 模块化设计:拆得开,合得拢
模块化设计,说白了就是「高内聚、低耦合」。每个模块只做一件事,并且把这件事做好。我在项目中遇到过有人把计量、通信、显示全写在一个.c文件里,结果改个波特率都要重新编译整个工程。你说这多折腾?
我建议这样划分模块:
- 计量模块:负责电压、电流、功率计算,对外提供 get_power() 接口
- 通信模块:负责DL/T645、Modbus协议解析,对外提供 send_frame() 接口
- 存储模块:负责参数保存、事件记录,对外提供 save_param() 接口
- 显示模块:负责LCD刷新、菜单管理,对外提供 display_update() 接口
每个模块内部可以有自己的状态机、缓冲区,但对外暴露的接口要尽量少。我一般一个模块只暴露3-5个函数,多了就容易乱。
3.3 接口隔离原则:别让上层知道太多
接口隔离原则,说白了就是「不要强迫上层依赖它不需要的东西」。举个例子,你的计量模块可能内部用了SPI、I2C、ADC三种方式读取数据,但对外只提供一个 get_energy() 函数。上层根本不需要知道底层用的是什么通信协议。
我曾经接手过一个项目,通信模块的接口直接暴露了串口句柄和缓冲区指针。结果上层代码到处都在操作串口,一换硬件平台,改得我头皮发麻。后来我重构了接口,只提供 send() 和 recv() 两个函数,内部怎么实现是它自己的事。
接口设计时,我习惯遵循这几个原则:
- 参数尽量简单:能用基本类型就别用结构体,能用结构体就别用指针套指针
- 返回值要明确:统一返回错误码,0表示成功,负数表示错误类型
- 不要暴露内部状态:比如模块内部有个缓冲区,别让上层知道它有多大
// 好的接口设计示例
// metering.h
#ifndef __METERING_H__
#define __METERING_H__
#include <stdint.h>
// 初始化计量模块
int32_t metering_init(void);
// 获取当前有功功率,单位瓦特
int32_t metering_get_power(float *power);
// 获取累计电能,单位千瓦时
int32_t metering_get_energy(float *energy);
// 复位累计电能
int32_t metering_reset_energy(void);
#endif
你看,这个接口只暴露了4个函数,每个函数职责清晰。上层调用时根本不需要知道底层是用的ADE7755还是RN8302,甚至不知道是SPI还是I2C。这就是接口隔离的魅力。
3.4 实际项目中的分层落地
说了这么多理论,咱们看看实际怎么落地。我以一个智能电表的计量功能为例:
- HAL层:封装MCU的SPI读写函数,比如 hal_spi_transfer()
- BSP层:配置计量芯片的CS引脚、中断引脚,比如 bsp_meter_pin_init()
- Driver层:实现计量芯片的寄存器读写,比如 drv_meter_read_register()
- Service层:实现电量计算、数据滤波,比如 svc_meter_calculate_energy()
- App层:根据费率时段切换计量模式,比如 app_meter_switch_tariff()
每一层都只调用下一层的接口。如果哪天要换计量芯片,只需要改Driver层和BSP层,Service层和App层完全不用动。这就是分层架构的价值。
嗯,这一章的内容就到这里。记住,好的架构不是一蹴而就的,而是在不断重构中打磨出来的。下一章咱们聊聊任务调度和状态机设计,那又是另一番天地了。