第四章:固件工程结构
好,咱们今天聊聊固件工程的结构。说白了,就是你的代码该怎么放、怎么组织、怎么跑起来。我见过不少新手,代码写得挺溜,但一打开工程目录就懵了——文件散落一地,链接脚本看不懂,Makefile 更是天书。嗯,这章咱们就把这些硬骨头啃下来。
4.1 工程目录组织
先说说目录怎么安排。我个人习惯,一个固件工程至少要有这么几个目录:
project_root/
├── app/ # 应用层代码
├── bsp/ # 板级支持包
├── driver/ # 外设驱动
├── lib/ # 第三方库
├── startup/ # 启动文件
├── linker/ # 链接脚本
├── build/ # 编译输出
└── doc/ # 文档
为什么要这么分?你想想看,如果所有 .c 文件都堆在根目录下,找起来多费劲。我在项目中遇到过,有个同事把 200 多个源文件全放一个文件夹里,结果每次编译都要等半天——因为 Makefile 要扫描所有文件。后来我帮他重新组织,编译时间直接砍了一半。
bsp_uart.c 就比 uart.c 好,因为你知道它是板级相关的。
4.2 启动文件分析
启动文件,这是芯片上电后第一个执行的代码。很多人觉得它神秘,其实说白了就是干三件事:
- 设置堆栈指针
- 初始化中断向量表
- 跳转到 main 函数
以 ARM Cortex-M 为例,启动文件通常长这样:
; 启动文件 startup_stm32f4xx.s
; 定义堆栈大小
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; 中断向量表
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; 栈顶指针
DCD Reset_Handler ; 复位中断
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; 硬错误
; ... 其他中断向量
; 复位中断处理函数
AREA |.text|, CODE, READONLY
Reset_Handler PROC
EXPORT Reset_Handler
; 初始化 .bss 段
LDR R0, =__bss_start
LDR R1, =__bss_end
MOV R2, #0
bss_loop CMP R0, R1
IT LT
STR R2, [R0], #4
BLT bss_loop
; 跳转到 main
BL main
ENDP
4.3 链接脚本(.ld)编写
链接脚本,嗯,这玩意儿决定了你的代码和数据怎么放到芯片的存储空间里。说白了,就是告诉链接器:哪段代码放 Flash,哪段数据放 RAM。
一个典型的链接脚本:
/* 链接脚本 example.ld */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text :
{
*(.isr_vector) /* 中断向量表 */
*(.text) /* 代码段 */
*(.rodata) /* 只读数据 */
_etext = .;
} > FLASH
.data : AT(_etext)
{
_sdata = .;
*(.data) /* 初始化数据 */
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss) /* 未初始化数据 */
_ebss = .;
} > RAM
}
这里有个关键点:.data 段在 Flash 里存储初始值,但运行时在 RAM 里。启动文件里那段复制代码,就是干这个活的。我记得有次调试,发现全局变量值不对,查了半天才发现是链接脚本里 AT 地址写错了。
- MEMORY 定义芯片的物理存储区域
- SECTIONS 定义如何分配代码和数据
- AT 关键字指定加载地址和运行地址
4.4 Makefile 基础
最后说说 Makefile。这东西是固件开发的标配,虽然现在有 IDE 帮你生成,但我觉得还是得懂原理。你想想看,如果哪天 IDE 抽风了,你还能用手搓 Makefile 来编译。
一个基础的 Makefile:
# 编译器设置
CC = arm-none-eabi-gcc
LD = arm-none-eabi-ld
OBJCOPY = arm-none-eabi-objcopy
# 编译选项
CFLAGS = -mcpu=cortex-m4 -mthumb -O2 -Wall
LDFLAGS = -T linker.ld -nostartfiles
# 源文件
SRCS = main.c uart.c gpio.c
OBJS = $(SRCS:.c=.o)
# 目标文件
TARGET = firmware.elf
# 编译规则
all: $(TARGET).bin
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
$(TARGET): $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
$(TARGET).bin: $(TARGET)
$(OBJCOPY) -O binary $< $@
clean:
rm -f *.o *.elf *.bin
.PHONY: all clean
Makefile 的核心就是依赖关系。比如 main.o 依赖 main.c,如果 main.c 改了,就重新编译 main.o。我在项目中遇到过,有人把依赖写反了,结果改了头文件却不重新编译,调试了一整天才发现。
-MM 选项自动生成头文件依赖,这样就不用手动维护了。命令大概是:gcc -MM main.c,它会输出 main.o: main.c header1.h header2.h。
嗯,这一章的内容就这些。工程目录、启动文件、链接脚本、Makefile,这四个东西是固件开发的基石。你把这些搞明白了,后面写代码就顺了。下一章咱们聊聊中断系统和实时性设计,那可是继电保护装置的核心。
本章小结:
- 目录组织要模块化,每个模块一个子目录
- 启动文件负责初始化硬件环境,跳转到 main
- 链接脚本定义代码和数据的存储布局
- Makefile 管理编译流程,自动化构建
公众号:蓝海资料掘金营,微信deep3321