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切换代码优化掉了,导致跳转失败,排查了两天才发现是编译器搞的鬼。

警告:双Bank切换相关的函数,建议加上 __attribute__((used)) 或者 #pragma O0,防止被编译器优化掉。

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的定义。

小技巧:IAR的 .icf 文件支持 define symbol 和 define region 指令。你可以先复制一份默认的 .icf,然后在此基础上修改,这样不容易出错。

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

核心要点:双Bank切换的本质,就是让链接器把代码分配到两个不同的Flash区域,然后通过软件控制Bank的激活状态。

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)。

提示:不同MCU的Bank状态寄存器地址不一样。STM32G4系列是0x40022000的bit4,但L4系列可能是其他位。一定要查芯片参考手册,别想当然。

3.6 常见问题与避坑指南

最后,分享几个我实际项目中遇到的坑:

  • 链接脚本写错地址:Bank0和Bank1的地址范围不能重叠,也不能留空洞。我曾经把Bank1的起始地址写成了0x08040001,结果程序死活跑不起来。
  • 中断向量表没对齐:ARM要求向量表必须256字节对齐。如果你把Bank1的起始地址设成0x08040100,那向量表就废了。
  • 忘记处理Bank切换后的时钟:有些MCU在Bank切换后需要重新配置Flash等待周期,否则读Flash会出错。

嗯,环境搭建这部分就讲到这里。下一章我们会真正开始写双Bank切换的核心代码——那个才是重头戏。