第3章:固件架构设计——分层架构与模块化原则

好,咱们直接进入正题。这一章讲的是固件架构设计,说白了就是怎么把代码组织得既清晰又抗造。我见过太多智能电表项目,前期图快,代码全揉在一个文件里,结果后期改一个功能,牵出一堆bug。嗯,这种坑我踩过不止一次。

3.1 分层架构:从硬件到应用的清晰路径

分层架构的核心思想,就是让每一层只关心自己的事。我习惯把智能电表固件分成五层:HAL、BSP、Driver、Service、App。你想想看,这样每一层都有明确的职责,改动起来心里有底。

层级 职责 典型内容
App 业务逻辑、用户交互 费率切换、数据显示、事件记录
Service 功能服务、状态管理 计量服务、通信服务、存储服务
Driver 外设驱动、寄存器操作 LCD驱动、Flash驱动、UART驱动
BSP 板级支持、引脚映射 GPIO配置、时钟配置、中断向量
HAL 硬件抽象、跨平台接口 MCU通用接口、定时器抽象

我个人习惯从下往上设计。先搞定HAL层,把MCU的差异屏蔽掉。比如我换了一颗STM32到GD32,HAL层接口不变,上层代码几乎不用动。这就是分层的好处。

核心原则:上层只能调用相邻下层,不能跨层调用。App不能直接调Driver,必须走Service。

3.2 模块化设计:拆得开,合得拢

模块化设计,说白了就是「高内聚、低耦合」。每个模块只做一件事,并且把这件事做好。我在项目中遇到过有人把计量、通信、显示全写在一个.c文件里,结果改个波特率都要重新编译整个工程。你说这多折腾?

我建议这样划分模块:

  • 计量模块:负责电压、电流、功率计算,对外提供 get_power() 接口
  • 通信模块:负责DL/T645、Modbus协议解析,对外提供 send_frame() 接口
  • 存储模块:负责参数保存、事件记录,对外提供 save_param() 接口
  • 显示模块:负责LCD刷新、菜单管理,对外提供 display_update() 接口

每个模块内部可以有自己的状态机、缓冲区,但对外暴露的接口要尽量少。我一般一个模块只暴露3-5个函数,多了就容易乱。

小技巧:模块内部用 static 关键字隐藏私有函数和全局变量。外部只能看到头文件里声明的接口。

3.3 接口隔离原则:别让上层知道太多

接口隔离原则,说白了就是「不要强迫上层依赖它不需要的东西」。举个例子,你的计量模块可能内部用了SPI、I2C、ADC三种方式读取数据,但对外只提供一个 get_energy() 函数。上层根本不需要知道底层用的是什么通信协议。

我曾经接手过一个项目,通信模块的接口直接暴露了串口句柄和缓冲区指针。结果上层代码到处都在操作串口,一换硬件平台,改得我头皮发麻。后来我重构了接口,只提供 send() 和 recv() 两个函数,内部怎么实现是它自己的事。

接口设计时,我习惯遵循这几个原则:

  1. 参数尽量简单:能用基本类型就别用结构体,能用结构体就别用指针套指针
  2. 返回值要明确:统一返回错误码,0表示成功,负数表示错误类型
  3. 不要暴露内部状态:比如模块内部有个缓冲区,别让上层知道它有多大
// 好的接口设计示例
// 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。这就是接口隔离的魅力。

避坑指南:我曾经见过有人把硬件寄存器地址直接写在头文件里,然后上层代码直接操作寄存器。这种写法一旦换MCU,整个项目都得重写。记住,硬件细节永远不要暴露到上层。

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层完全不用动。这就是分层架构的价值。

嗯,这一章的内容就到这里。记住,好的架构不是一蹴而就的,而是在不断重构中打磨出来的。下一章咱们聊聊任务调度和状态机设计,那又是另一番天地了。