3、开发环境搭建:Keil/IAR工程配置、链接脚本(.icf/.ld)修改、启动文件分析
好,咱们进入实战环节的第一步——搭环境。
说实话,很多同学觉得搭环境就是装个软件、点几下鼠标,没什么技术含量。但我做了这么多年嵌入式,见过太多因为环境配置翻车的案例了。双Bank切换这种操作,对链接脚本和启动文件的要求特别苛刻,稍有不慎,程序就跑飞了。
3.1 开发工具的选择:Keil还是IAR?
我个人习惯用Keil多一些,毕竟ARM生态下Keil的普及率最高。但IAR的编译器优化确实更狠,代码密度能压得更低。怎么选?
- Keil MDK:上手快,调试方便,适合中小规模项目
- IAR EWARM:优化强,链接脚本灵活,适合对Flash空间有极致要求的场景
我建议你两个都装。为什么?因为有时候客户指定了工具链,你没得选。我在项目中就遇到过甲方要求必须用IAR,结果我平时只用Keil,临时抱佛脚学了一周才搞定。
3.2 Keil工程配置要点
先说说Keil下的配置。新建一个工程后,有几个关键地方必须改:
3.2.1 Target选项卡
这里要指定芯片型号和浮点单元。双Bank切换通常用在中高端MCU上,比如STM32G4、L4系列。记得把FPU打开,不然浮点运算会走软件模拟,性能差一大截。
3.2.2 C/C++选项卡
优化等级我一般选 -O2,兼顾性能和调试。但要注意——千万别开 -O3,我曾经在某个项目里开了 -O3,结果链接器把Bank切换代码优化掉了,导致跳转失败,排查了两天才发现是编译器搞的鬼。
3.2.3 Linker选项卡
这里要指定分散加载文件(.sct)。Keil默认的分散加载文件只分配了一个Bank,我们需要手动修改,把Flash分成两个区域。
3.3 IAR工程配置要点
IAR的配置思路和Keil类似,但细节上有些差异。我个人觉得IAR的链接脚本(.icf)比Keil的分散加载文件更直观,但语法也更啰嗦。
3.3.1 通用选项设置
在Project -> Options -> General Options里,选择芯片型号。注意IAR的芯片支持包需要单独安装,不像Keil那样集成在MDK里。
3.3.2 链接器配置
IAR使用 .icf 文件来定义内存布局。默认的 .icf 文件只定义了一个Flash区域,我们需要手动添加第二个Bank的定义。
3.4 链接脚本修改:.icf 和 .ld 实战
链接脚本是整个双Bank切换的核心。说白了,就是告诉编译器:你的代码该放哪儿,数据该放哪儿。
3.4.1 IAR的 .icf 文件修改
假设我们的MCU有512KB Flash,分成两个256KB的Bank。.icf文件可以这样写:
define symbol __ICFEDIT_region_ROM_BANK0_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_BANK0_end__ = 0x0803FFFF;
define symbol __ICFEDIT_region_ROM_BANK1_start__ = 0x08040000;
define symbol __ICFEDIT_region_ROM_BANK1_end__ = 0x0807FFFF;
define region ROM_BANK0 = mem:[from __ICFEDIT_region_ROM_BANK0_start__
to __ICFEDIT_region_ROM_BANK0_end__];
define region ROM_BANK1 = mem:[from __ICFEDIT_region_ROM_BANK1_start__
to __ICFEDIT_region_ROM_BANK1_end__];
place in ROM_BANK0 { section .text_bank0 };
place in ROM_BANK1 { section .text_bank1 };
嗯,这里要注意:.text_bank0 和 .text_bank1 是自定义的段名,需要在源文件中通过 #pragma location 或者 __attribute__((section(".text_bank0"))) 来指定。
3.4.2 Keil的分散加载文件(.sct)修改
Keil的分散加载文件语法略有不同:
LR_IROM1 0x08000000 0x00040000 { ; Bank0: 256KB
ER_IROM1 0x08000000 0x00040000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
}
LR_IROM2 0x08040000 0x00040000 { ; Bank1: 256KB
ER_IROM2 0x08040000 0x00040000 {
bank1.o (+RO)
}
}
看到没?Bank1的代码被单独放在 bank1.o 这个目标文件里。这意味着你需要把Bank1相关的代码单独编译成一个源文件,比如 bank1.c。
3.5 启动文件分析
启动文件(startup_xxx.s)是MCU上电后执行的第一段代码。它负责初始化堆栈、设置中断向量表、跳转到main函数。
在双Bank场景下,启动文件需要额外处理两件事:
- 中断向量表重定位:如果程序从Bank1启动,中断向量表必须指向Bank1的起始地址
- Bank状态检查:上电后先判断当前是哪个Bank在运行,然后决定跳转逻辑
我记得有一次调试,程序在Bank0里跑得好好的,切换到Bank1就死机。查了半天,发现是启动文件里的向量表地址写死了,没有根据Bank切换动态调整。这个坑我踩过,你们别踩了。
3.5.1 典型的启动文件修改点
以STM32G4为例,启动文件里通常有这样一段:
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
我们需要在 SystemInit 之前加入Bank检测代码:
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
; 检查当前Bank状态
LDR R0, =0x40022000 ; FLASH控制寄存器地址
LDR R1, [R0]
TST R1, #0x10 ; 检查BANK位
BEQ Bank0_Active
Bank1_Active
; 如果从Bank1启动,重设向量表偏移
LDR R0, =0x08040000 ; Bank1起始地址
LDR R1, =0xE000ED08 ; VTOR寄存器
STR R0, [R1]
B Continue_Startup
Bank0_Active
; Bank0启动,使用默认向量表
LDR R0, =0x08000000
LDR R1, =0xE000ED08
STR R0, [R1]
Continue_Startup
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
这段代码的核心逻辑是:读取Flash控制寄存器,判断当前激活的是哪个Bank,然后动态设置中断向量表偏移寄存器(VTOR)。
3.6 常见问题与避坑指南
最后,分享几个我实际项目中遇到的坑:
- 链接脚本写错地址:Bank0和Bank1的地址范围不能重叠,也不能留空洞。我曾经把Bank1的起始地址写成了0x08040001,结果程序死活跑不起来。
- 中断向量表没对齐:ARM要求向量表必须256字节对齐。如果你把Bank1的起始地址设成0x08040100,那向量表就废了。
- 忘记处理Bank切换后的时钟:有些MCU在Bank切换后需要重新配置Flash等待周期,否则读Flash会出错。
嗯,环境搭建这部分就讲到这里。下一章我们会真正开始写双Bank切换的核心代码——那个才是重头戏。