第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 小结
模块化分层不是花架子,是真能救命的。我见过太多项目因为结构混乱,最后不得不重写。记住三点:
- 职责单一:每个模块只做一件事
- 接口清晰:层与层之间用函数接口通信
- 目录规范:从第一天就按规范组织代码
下一章,我们聊聊自动化测试框架怎么搭。说实话,这部分才是真正能提升开发效率的关键。