第2章:固件工程结构设计——模块化分层思想

做充电桩固件开发,我踩过的第一个大坑就是「代码一锅炖」。

那时候刚接手一个项目,前任工程师把所有代码都塞在几个大文件里。硬件换了,要改底层;协议变了,要改应用层。改一处,崩三处。你说崩溃不崩溃?

后来我学乖了。模块化分层,说白了就是把代码按职责拆开。各管各的,互不干扰。今天我就把这一套思路掰开揉碎了讲给你听。

2.1 为什么要分层?

你想想看,充电桩固件要处理的事情其实挺杂的:

  • 控制继电器通断、读取电表数据
  • 处理OCPP协议、与后台通信
  • 管理充电流程、计费逻辑
  • 处理用户刷卡、屏幕显示

这些功能如果混在一起写,后期维护就是噩梦。我见过一个项目,就因为改了一个GPIO引脚定义,结果充电逻辑也跟着出问题。为什么?因为底层硬件操作和上层业务逻辑写在了同一个函数里。

核心原则:上层不关心底层怎么实现,底层不关心上层要做什么。中间通过接口通信。

2.2 三层架构:HAL层、中间件层、应用层

我个人习惯把固件分成三层。这个结构我用在好几个量产项目上,效果都不错。

层级 职责 典型内容 依赖关系
应用层 业务逻辑 充电流程、计费、OCPP状态机 依赖中间件层
中间件层 协议封装、数据管理 OCPP协议栈、Modbus、文件系统 依赖HAL层
HAL层 硬件抽象 GPIO、UART、SPI、定时器 直接操作寄存器或CMSIS

举个例子。你在应用层要「打开继电器」,代码应该是这样的:

// 应用层代码 - 充电控制模块
void charge_start(void) {
    // 检查状态
    if (relay_get_status() == RELAY_OFF) {
        relay_control(RELAY_ON);
        // 记录日志
        log_info("Charging started");
    }
}

你看,应用层根本不知道继电器是接在哪个GPIO上,也不知道是高低电平有效。它只管调用relay_control()。这就是分层的好处。

2.3 CMSIS标准与HAL库选择

说到HAL层,就绕不开CMSIS。CMSIS是ARM公司搞的一套标准,说白了就是给Cortex-M内核的MCU定了个统一的「方言」。

我建议新手直接使用厂商提供的HAL库,比如STM32的HAL库。为什么?

  • 省时间:不用自己写寄存器操作
  • 可移植:换芯片型号,改改配置就行
  • 有社区:遇到问题网上搜得到

我的经验:HAL库虽然方便,但性能不是最优的。如果你做的是高频开关电源控制这类对时序要求严苛的功能,建议关键路径直接用寄存器操作。我在一个项目中就遇到过HAL库的SPI传输延迟导致通信超时,最后改成直接操作寄存器才搞定。

CMSIS的核心文件通常包括:

CMSIS/
├── core_cm4.h          // Cortex-M4内核寄存器定义
├── core_cm4_simd.h     // SIMD指令支持
├── system_stm32f4xx.h  // 系统时钟配置
└── startup_stm32f4xx.s // 启动文件

2.4 工程目录结构规范

一个好的目录结构,能让团队协作效率翻倍。我现在的项目一般长这样:

project/
├── app/                    # 应用层
│   ├── charge/            # 充电管理
│   ├── ocpp/              # OCPP协议
│   └── user/              # 用户交互
├── middleware/             # 中间件层
│   ├── modbus/            # Modbus协议
│   ├── fatfs/             # 文件系统
│   └── lwip/              # TCP/IP协议栈
├── hal/                   # HAL层
│   ├── inc/               # 头文件
│   └── src/               # 源文件
├── bsp/                   # 板级支持包
│   ├── stm32f4xx_hal_msp.c
│   └── stm32f4xx_it.c
├── doc/                   # 文档
├── test/                  # 测试代码
├── Makefile               # 主构建文件
└── README.md

注意:不要把所有头文件都扔到一个inc文件夹里。每个模块的头文件应该跟着模块走。比如app/charge/charge.h,这样模块独立性强,方便复用。

2.5 Makefile与CMakeLists.txt编写实战

构建工具的选择,我建议小项目用Makefile,大项目用CMake。为什么?

Makefile简单直接,适合几十个源文件的项目。但一旦项目大了,跨平台了,Makefile的维护成本就上来了。CMake能生成各种平台的构建文件,灵活性高很多。

先看一个简单的Makefile示例:

# 编译器
CC = arm-none-eabi-gcc
# 目标芯片
MCU = -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16
# 源文件
SRCS = $(wildcard hal/src/*.c) \
       $(wildcard middleware/*.c) \
       $(wildcard app/**/*.c)
# 头文件路径
INCS = -Ihal/inc -Imiddleware -Iapp
# 编译选项
CFLAGS = $(MCU) -O2 -Wall $(INCS)

all: firmware.elf

firmware.elf: $(SRCS:.c=.o)
	$(CC) $(CFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm -f *.o firmware.elf

再看CMakeLists.txt的写法:

cmake_minimum_required(VERSION 3.10)
project(charger_firmware C ASM)

# 设置目标芯片
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)

# 添加源文件
file(GLOB_RECURSE HAL_SRCS hal/src/*.c)
file(GLOB_RECURSE MID_SRCS middleware/*.c)
file(GLOB_RECURSE APP_SRCS app/*.c)

# 添加头文件路径
include_directories(
    hal/inc
    middleware
    app
)

# 生成可执行文件
add_executable(firmware.elf
    ${HAL_SRCS}
    ${MID_SRCS}
    ${APP_SRCS}
)

# 编译选项
target_compile_options(firmware.elf PRIVATE
    -mcpu=cortex-m4 -mthumb -O2 -Wall
)

避坑指南:我曾经在Makefile里忘了加-mfloat-abi=hard,结果浮点运算全走软件模拟,性能直接掉了30%。排查了两天才发现。所以编译选项一定要和芯片手册对清楚。

2.6 小结

模块化分层不是花架子,是真能救命的。我见过太多项目因为结构混乱,最后不得不重写。记住三点:

  • 职责单一:每个模块只做一件事
  • 接口清晰:层与层之间用函数接口通信
  • 目录规范:从第一天就按规范组织代码

下一章,我们聊聊自动化测试框架怎么搭。说实话,这部分才是真正能提升开发效率的关键。