1. 项目背景与核心需求
在嵌入式开发中,使用Keil MDK编译GD32系列MCU工程时,经常会遇到一个头疼的问题:即使代码中没有使用某些外设(如ADC、DAC、定时器等),编译器仍然会将相关库函数和初始化代码包含在最终生成的二进制文件中。这会导致程序体积无谓增大,尤其对于Flash资源紧张的GD32F1/F3系列芯片(通常只有16-64KB Flash),这种浪费可能直接影响功能实现。
我最近在优化一个GD32F103C8T6项目时就遇到了这种情况。原本只有30KB左右的逻辑代码,编译后生成的hex文件却接近48KB,差点超出芯片的64KB Flash限制。通过分析.map文件发现,ADC、I2C等未使用外设的驱动代码占了近15KB空间。经过一系列优化,最终将固件体积压缩到32KB以内,腾出了宝贵的存储空间。
2. 问题根源分析
2.1 GD32标准库的结构特点
GD32的标准外设库(类似STM32的StdPeriph库)采用模块化设计,每个外设对应独立的.c/.h文件(如gd32f10x_adc.c、gd32f10x_timer.c)。这些文件通过宏定义控制编译条件,理论上可以单独排除未使用的外设。但在实际项目中,开发者通常会包含整个gd32f10x_conf.h配置文件,该文件又默认包含了所有外设的头文件。
c复制// gd32f10x_conf.h 典型配置
#include "gd32f10x_adc.h"
#include "gd32f10x_dac.h"
#include "gd32f10x_dma.h"
// ...其他外设头文件
2.2 Keil编译器的链接行为
Keil的ARMCC编译器在链接阶段会执行"链接时优化"(LTO),但其默认行为相对保守。即使某段代码未被显式调用,只要它被编译进对象文件(.o),链接器仍会保留这些代码,除非满足以下条件:
- 代码段被明确标记为"weak"符号
- 开启最高级别优化(-O3)并配合特定链接选项
- 使用
--opt=--unused等参数主动移除未引用段
2.3 外设初始化的隐式调用
许多GD32外设库会在启动阶段执行隐式初始化。例如:
- 系统时钟配置可能涉及定时器时钟使能
- 中断向量表默认包含所有外设中断服务程序
- 库内部的静态构造函数(如
__attribute__((constructor)))
这些"隐藏"的依赖关系使得编译器难以准确判断哪些外设代码真正未被使用。
3. 解决方案与实操步骤
3.1 修改库配置文件
最直接的方案是精确控制gd32f10x_conf.h的内容,仅保留实际使用的外设头文件:
c复制// 注释掉未使用的外设头文件
//#include "gd32f10x_adc.h"
//#include "gd32f10x_dac.h"
#include "gd32f10x_gpio.h" // 只保留实际使用的外设
#include "gd32f10x_usart.h"
注意:修改后需确保工程中没有间接引用这些外设的情况,否则会导致编译错误。
3.2 调整Keil工程选项
在Project → Options for Target → C/C++选项卡中:
- 启用最高级别优化:
Optimization: Level 3 (-O3) - 添加特定宏定义:
__OPTIMIZE__=1, __USE_LTO=1 - 在Misc Controls中添加:
code复制--opt=--unused --opt=--unused_sections
在Linker选项卡中:
- 勾选"Use Memory Layout from Target Dialog"
- 在Scatter File中添加
UNINIT段处理未使用代码
3.3 自定义分散加载文件
创建自定义的scatter文件(如GD32F10x.sct),明确指定需要保留的库函数:
code复制LR_IROM1 0x08000000 0x00010000 { ; Flash区域
ER_IROM1 0x08000000 0x00010000 { ; 代码段
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00005000 { ; RAM区域
.ANY (+RW +ZI)
}
UNUSED 0x00000000 { ; 未使用代码段
gd32f10x_adc.o (+RO)
gd32f10x_dac.o (+RO)
/* 添加其他需要排除的外设库 */
}
}
3.4 使用库的模块化编译
更彻底的方案是重构工程结构:
- 在工程中移除标准外设库的全局引用
- 为每个使用的外设创建独立的库模块
- 在Keil中按需添加这些模块
具体步骤:
- 创建
Lib/GPIO、Lib/USART等目录 - 将对应外设的.c/.h文件放入相应目录
- 在工程中仅添加需要的模块
- 修改头文件引用路径
4. 效果验证与优化对比
4.1 编译结果对比
以GD32F103C8T6的USART通信示例工程为例:
| 优化措施 | Hex文件大小 | 减少量 |
|---|---|---|
| 原始配置 | 48.7KB | - |
| 仅修改conf.h | 42.1KB | 6.6KB |
| conf.h + 编译器优化 | 36.8KB | 11.9KB |
| 完整方案(含scatter文件) | 31.2KB | 17.5KB |
4.2 内存占用分析
使用fromelf --text -c -v生成详细内存报告:
code复制Code (inc. data) RO Data RW Data ZI Data Debug
25612(1236) 4856 1564 6148 98544 Object Totals
关键指标:
- RO Data(只读数据):从6.2KB降至4.8KB
- Code(代码段):从30.1KB降至25.6KB
4.3 常见问题排查
问题1:优化后出现未定义符号错误
- 原因:某些被移除的外设可能被库内部函数隐式依赖
- 解决:使用
--keep=函数名链接选项保留必要符号
问题2:中断服务程序丢失
- 现象:开启优化后外设中断无法触发
- 解决:在scatter文件中强制保留中断向量:
code复制gd32f10x_it.o (RESET, +First)
问题3:优化级别过高导致时序异常
- 现象:USART通信出现误码
- 解决:对关键函数添加
__attribute__((optimize("O1")))降级优化
5. 进阶优化技巧
5.1 链接脚本微调
在scatter文件中添加以下规则可进一步压缩体积:
code复制UNUSED 0x00000000 {
*(.ARM.exidx*)
*(.ARM.extab*)
*(.glue_7)
*(.glue_7t)
*(.vfp11_veneer)
*(.v4_bx)
}
5.2 关键函数属性标记
对性能敏感的函数使用特定属性:
c复制// 防止被优化掉
__attribute__((used)) void Essential_Func(void);
// 强制内联
__attribute__((always_inline)) void Inline_Func(void);
// 禁止内联
__attribute__((noinline)) void NoInline_Func(void);
5.3 库函数裁剪
通过重定向库函数到空实现:
c复制// 在gd32f10x_conf.h中添加
#define ADC_CUTOFF
#ifdef ADC_CUTOFF
#define adc_init() ((void)0)
#define adc_enable() ((void)0)
// 其他ADC函数...
#endif
5.4 启动文件优化
修改startup_gd32f10x.s文件:
- 移除未使用的中断向量
- 精简初始化流程
- 禁用不必要的时钟使能
6. 工程管理建议
6.1 版本控制配置
在.gitignore中添加:
code复制# Keil临时文件
*.uvoptx
*.uvguix
*.axf
*.crf
*.d
*.o
*.lst
6.2 模块化工程结构示例
推荐的项目目录结构:
code复制Project/
├── Core/
│ ├── Inc/ # 核心头文件
│ └── Src/ # 核心源文件
├── Drivers/
│ ├── GD32F10x/ # 标准外设库
│ └── BSP/ # 板级支持包
├── Middlewares/ # 中间件
├── Output/ # 生成文件
├── Projects/ # Keil工程文件
└── User/
├── config/ # 配置文件
└── application/ # 应用代码
6.3 自动化构建配置
使用批处理文件实现一键优化编译:
bat复制@echo off
set UV4="C:\Keil_v5\UV4\uv4.exe"
set PROJECT="Project\Projects\GD32.uvprojx"
%UV4% -j0 -b %PROJECT% -o BUILD_OUTPUT=optimized
在开发GD32项目时,存储空间优化是个持续的过程。每次添加新功能后,建议:
- 重新分析.map文件
- 检查未使用的外设驱动
- 更新scatter文件配置
- 验证功能完整性
通过这种系统化的优化方法,我在最近一个工业控制器项目中,成功将固件体积从56KB压缩到38KB,为后续功能升级预留了充足空间。记住,嵌入式开发中的每个字节都值得争取!