news 2026/10/2 1:27:22

STM32F103 IAP升级踩坑:Flash起始地址调整与三地址一致律实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 IAP升级踩坑:Flash起始地址调整与三地址一致律实战

做STM32F103的IAP在线升级时,我踩过的坑里有一半都跟Flash起始地址有关。默认情况下,芯片的Flash从0x08000000开始,链接脚本默认写死FLASH ORIGIN = 0x08000000,一切都好说;可一旦App要避开Bootloader挪到后面的地址,事情就变成了三个层面的联动修改:编译链接时的地址、内核向量表的运行时偏移、以及烧录器实际写入的物理地址。任何一个环节没改对,程序的表现都相当迷惑——要么启动后凡是中断一触发就HardFault,要么调试器下载完但PC停在一个诡异的地址,要么干脆把辛辛苦苦写好的Bootloader给覆盖掉了。

这篇文章我以STM32F103C8T6 + CubeMX + CLion这套组合为例,把“调整Flash起始地址”这件事从原理到落地讲透。你如果能完整跟着做一遍,会明白链接脚本、向量表偏移、OpenOCD烧录这些平时不太关注的东西是怎么咬合在一起的。适合正在做IAP升级、Bootloader与App分离、Flash多分区方案的开发者参考。

1. 什么情况下必须动Flash起始地址

1.1 IAP在线升级是最典型的需求

IAP(In-Application Programming,应用内编程)的经典架构是:Flash前段固化一个Bootloader,负责开机时检查新固件、接收升级包写入Flash后段,然后跳转到App执行。这个架构决定了App的起始地址不能是0x08000000,必须往后挪。

第一次做IAP的人容易有个误区:以为只要在Bootloader里实现了“串口接收+擦写Flash+跳转”就等于完工。实际情况是,等把App烧进去、按下复位、看到Bootloader成功跳转,才发现App跑不起来。一个很典型的现象是:App的main函数被执行了,主循环里的LED也在闪(因为主循环不靠中断),但一按按键或者一开定时器,程序直接HardFault。这就是Flash起始地址只改了一半的典型症状:编译地址被链接脚本改到了偏移位置,但中断向量表还指向原来的0x08000000。

所以,做IAP的人迟早会遇到“调整Flash起始地址”这个操作,它不是可选项,而是App工程编译前的必做步骤。

1.2 除了Bootloader,还有这些场景需要动地址

  • 双镜像OTA(A/B分区):0x08000000放Bootloader,0x08008000放App A,0x08010000放App B。App A升级失败时,Bootloader回滚到App B。这种场景下的每个App分区起始地址都要单独规划。
  • Flash末尾存储参数:如果不想用外部EEPROM,可以把用户参数放在Flash最后几页。这种场景一般不改ORIGIN,但要改LENGTH以缩小可用Flash范围,防止链接器把数据段塞进参数区。
  • 多固件共存:Bootloader + 主应用 + 恢复固件(Recovery)这种三区甚至四区方案,每一段代码都要精确占据自己的地址区间。

这些场景本质上是同一件事:你要告诉编译器和运行时环境,“我的程序不在0x08000000住,而在XX地址住”。理解了这个本质,后面所有修改步骤都会变得顺理成章。

1.3 动手改之前,必须分清三个“地址”

为了让后面的实操不蒙圈,先建立一个核心认知框架:Flash起始地址相关的“地址”其实有三个,它们各管一段,缺一不可:

概念是什么在哪里配置改错了会怎样
链接地址(运行地址)编译时,代码段、数据段被约束到哪个起始地址运行链接脚本 MEMORY 段的 FLASH ORIGIN / LENGTH程序烧进去后跳转混乱,跑飞
向量表地址CPU取中断向量表(异常入口地址数组)的基址SCB->VTOR 寄存器,或 system_stm32f1xx.c 中的 VECT_TAB_OFFSET中断触发后取到Bootloader或空白的向量,直接HardFault
烧录地址下载器/JTAG/OpenOCD实际往Flash里写入的物理地址下载器配置或命令行参数把App烧错位置,覆盖Bootloader或烧到空洞区,结果不可预测

这三个地址必须一致,至少在“App工作期间”要保持协调。链接地址告诉编译器,向量表地址告诉CPU,烧录地址告诉下载器——只有三者指向同一片区域,整个App才能真正跑起来。我习惯把这叫“三地址一致律”,后面所有实操都围绕它展开。

2. STM32F103的启动与向量表机制:为什么扯一发动全身

2.1 存储映射:别被“FLASH ORIGIN=0x08000000”骗了

STM32F103的Flash映射到地址0x08000000,而CPU在复位后从0x00000000取第一条指令。这两个地址看起来不同,但实际上在F103的启动模式下,0x00000000是0x08000000的一个别名区——你在0x08000000放的东西,CPU从0x00000000也能读到。默认场景下完全不用关心这个别名机制,链接脚本直接写0x08000000,一切正常。

但对做Bootloader+App的人,这个机制就成了陷阱。如果把App链接到0x08008000,CPU在App被Bootloader跳转后,取指令的PC是从0x08008000开始的,这没问题;但中断向量表默认在0x00000000读取(也就是0x08000000的别名),于是CPU在触发任何中断时,都会去读Bootloader区域的数据当作中断向量,结果自然是HardFault。

除非你显式告诉CPU“我的向量表不在这里,在偏移0x8000处”,也就是设置SCB->VTOR。所以改ld之后,必须同步改向量表偏移——这不是惯例,而是Cortex-M3的硬件机制决定的。

2.2 向量表偏移(VTOR)的运作细节

Cortex-M3内核里有个SCB->VTOR寄存器(地址0xE000ED08),它定义了异常向量表的起始地址。复位后VTOR默认值是0x00000000。当程序运行在0x08008000,我们要把它改成0x08008000。

修改方式有两种。第一种,在系统初始化代码里统一处理:修改system_stm32f1xx.c中的VECT_TAB_OFFSET宏,从0x00改成0x8000,这样SystemInit会在main之前执行时设置VTOR。第二种,在main函数开头手动执行SCB->VTOR = 0x08008000;,适合不用CubeMX或者需要动态修改的场景。

第二种方式看起来方便,但有个隐藏问题:如果在SystemInit到main之间有任何中断发生,或者你依赖某个驱动的初始化在main之前就用了中断,那“手动设置”就太晚了。最稳妥的做法还是直接修改VECT_TAB_OFFSET宏,让SystemInit在最早的启动阶段把VTOR办妥。

2.3 Bootloader跳转到App时发生了什么

跳转流程说白了就四步:

  1. 关闭全局中断(__disable_irq)。
  2. 把App区域第4字节(复位向量地址)取出来,作为跳转目标。
  3. 把App区域首字节(栈顶地址)写入MSP。
  4. 跳转到Reset_Handler。

跳转代码里有一个非常关键的步骤:设置SCB->VTOR。这一步在Bootloader里做也行,在App的SystemInit里做也行。我的建议是两边都做——Bootloader设置了之后,App启动初期(SystemInit完成前)如果触发中断,向量表地址已经是对的;App里再设一次,是为了应对Bootloader可能没设置的情况。

跳转前的栈指针校验也很重要:取出来的“栈顶地址”必须在SRAM范围(0x20000000~0x20004FFF),否则说明App区域的Flash不是有效固件,直接跳会发生灾难。这个校验虽然只有两行代码,但关键时刻能救你一命——程序错乱时至少不会跑到天荒地老。

3. CubeMX工程生成与Flash起始地址修改实操

3.1 CubeMX生成可以被CLion直接使用的CMake工程

先交代我的环境:STM32CubeMX 6.12 + CLion 2024.1 + arm-none-eabi-gcc 13.2 + OpenOCD 0.12。

CubeMX 6.10之后的版本,Project Manager工具链下拉框里有一个“CMake”选项。选择它并生成代码,CubeMX会生成一个完整的CMake工程结构,包括CMakeLists.txt、Core目录、Drivers目录,CLion打开后能直接识别。

关键配置项:

  • 芯片选STM32F103C8T6,封装LQFP48。
  • SYS -> Debug: Serial Wire(打开SWD下载口)。
  • RCC -> HSE: Crystal/Ceramic Resonator(如果板上有8M晶振)。
  • 时钟树配到72MHz:HCLK 72MHz,APB1 36MHz,APB2 72MHz。
  • Project Manager -> Project -> Toolchain/IDE: CMake。
  • Project Manager -> Code Generator: 勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”,便于管理。

生成后的目录大致是这样:

my_project/ ├── CMakeLists.txt ├── Core/ │ ├── Inc/ │ └── Src/ │ ├── main.c │ ├── system_stm32f1xx.c │ └── ... ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── startup_stm32f103xb.s └── STM32F103C8Tx_FLASH.ld

CLion直接Open这个CMakeLists.txt即可。建议先在CLion里配好Toolchain——装好arm-none-eabi-gcc之后,在Settings -> Build, Execution, Deployment -> Toolchains里,把C Compiler和ASM Compiler都指向arm-none-eabi-gcc。CLion会自动识别,不需要额外装插件。

3.2 链接脚本(.ld)的修改:改哪里、为什么这样改

链接脚本是决定“程序认为自己住在哪”的核心文件。CubeMX生成的STM32F103C8Tx_FLASH.ld里,MEMORY段是重点关注对象。

默认内容:

MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K }

我的分区规划是Bootloader占16KB,App从0x08004000开始。修改为:

MEMORY { FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 48K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K }

如果你计划Bootloader占32KB,App从0x08008000开始,则改为:

MEMORY { FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 32K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K }

这里有一个非常容易犯的错:只改ORIGIN,不改LENGTH。默认LENGTH=64K,如果你保留它,意味着链接器认为从0x08004000往后有64K可用空间,但实际上0x08010000就是Flash终点,超出部分根本不存在。链接器不会报错(它不知道芯片物理极限),只会把代码放到不存在的地址上,烧录后几乎必炸。

正确的计算方式:

Flash总数 = 0x10000(64KB,C8T6) App起始 = 0x4000(16KB处) App可用长度 = 0x10000 - 0x4000 = 0xC000 = 48KB

如果你是CBT6(128KB Flash),Bootloader占32KB,App从0x08008000开始,LENGTH = 128K - 32K = 96K。务必先确认自己芯片的Flash总容量再算。

另外,我习惯在ld文件ORIGIN修改处的注释里写清楚Bootloader占多大、App可用多大、日期和原因。这样过半年回来看不会一脸懵。改完ld后,如果用的是CubeMX默认链接脚本名,CMakeLists.txt里自动引用的就是这个文件,无需再动CMake。但后面我会讲一种更稳的规避手段。

3.3 system_stm32f1xx.c中VECT_TAB_OFFSET的修改

找到Core/Src/system_stm32f1xx.c,搜索VECT_TAB_OFFSET,默认是:

#define VECT_TAB_OFFSET 0x00

改成:

#define VECT_TAB_OFFSET 0x4000

如果你用的是0x08008000方案,这里就改成0x8000。这个宏最终在SystemInit函数里被使用。F103的SystemInit里有这样一段条件编译:

#if defined(SCB_VTOR_TBLOFF) && (SCB_VTOR_TBLOFF != 0) SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; #endif

把偏移填进去后,每次上电执行SystemInit,就会把向量表指针指到对应的偏移地址。这样即使Bootloader忘记设置VTOR,App自己也能在启动的极早阶段搞定。

这里有同学会问:F103的VTOR是不是都可用?STM32F1家族里,部分早期Cortex-M3的VTOR是只读的,写了没反应。这个在F103中高密度型号上基本不会遇到,CMSIS头文件也明确定义了SCB_VTOR_TBLOFF,SystemInit的这段代码就会生效。万一在某些老F1上改了VECT_TAB_OFFSET没效果,只能用“中断向量表重映射到SRAM”这类方案,不过这是极小众情况,不用过于担心。

3.4 CubeMX重新生成代码的“覆盖”坑与规避办法

这句话请记住:CubeMX每次Generate Code,都会用默认模板重新生成ld文件,把你手动改的ORIGIN/LENGTH冲掉。这不是“用户代码保留区”能保护的,因为ld文件的生成逻辑根本不看USER CODE注释。

我踩过这个坑。一开始图省事直接改默认ld,改完编译烧录全通。后来因为多勾了一个外设重新Generate Code,再编译就发现程序怎么烧都不对。花了二十分钟才意识到是ld被重置回0x08000000了。

规避办法:

  1. 最省事:每次生成代码后,重新检查ld文件,把改动再应用一遍。缺点是容易忘。
  2. 相对稳:把改好的ld另存为my_linker.ld,放在工程根目录或LinkerScripts/目录下,然后在CMakeLists.txt里把LINKER_SCRIPT变量指向它。CubeMX重新生成时不会触碰这个新文件。
  3. 一劳永逸:写构建脚本,在CMake Configure阶段自动用你预先写好的ld替换默认ld。

实际开发里我推荐第2种,最简单可靠。后面第4节会给出改法。

4. CLion + CMake完整配置:编译、烧录、调试

4.1 CubeMX生成的CMakeLists.txt结构速览

CubeMX生成的CMakeLists.txt骨架大致如下(不同版本略有差异,核心点一样):

cmake_minimum_required(VERSION 3.22) project(my_project C ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) # 裸机环境 set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) add_compile_options(-mcpu=cortex-m3 -mthumb -Wall -fdata-sections -ffunction-sections) set(LINKER_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/STM32F103C8Tx_FLASH.ld) add_link_options(-T${LINKER_SCRIPT} --specs=nosys.specs -Wl,--gc-sections -Wl,-Map=output.map) file(GLOB_RECURSE SOURCES Core/Src/*.c Core/Startup/*.s Drivers/STM32F1xx_HAL_Driver/Src/*.c Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/*.c ) add_executable(${PROJECT_NAME}.elf ${SOURCES}) add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.bin COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.hex )

如果你采用了3.4节的“独立ld文件”方案,把set(LINKER_SCRIPT ...)一行改一下即可:

set(LINKER_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/my_linker.ld)

改完在CLion里点一下Reload CMake Project,新配置立即生效。

注意:add_compile_definitions里通常需要包含芯片型号宏,CubeMX默认会加上STM32F103xB USE_HAL_DRIVER之类的定义。如果你手写CMake,别漏掉这两个宏,否则HAL库头文件找不到。

4.2 链接脚本路径的指定与验证

采用独立my_linker.ld之后,还有一个细节:add_executable里最好把链接脚本也作为一个“源文件”加进去,这样CMake在ld文件变化时能自动重新链接,不然改ld后有时候不重新编译。

add_executable(${PROJECT_NAME}.elf ${SOURCES} ${LINKER_SCRIPT})

验证是否真的用了你改过的ld,有两种方法:

  1. 在CLion的CMake构建日志里搜“my_linker.ld”或“--script”,能看到-T/path/to/my_linker.ld出现在链接命令里。
  2. 打开生成的map文件。在链接选项里加-Wl,-Map=${PROJECT_NAME}.map之后,编译完用文本编辑器打开map文件,第一页会有Linker script and memory map段,里面会列出:
LOAD my_linker.ld ... .text 0x0000000008004000 0xec 0x0000000008004000 isr_vector

看到这个地址是你规划的起始地址,就说明链接地址对了。这一步验证很重要,不要跳过,因为CubeMX版本之间生成的CMakeLists可能有细微差异,肉眼看不出来的时候,map文件会告诉你真相。

4.3 OpenOCD烧录配置:地址不对全是白干

CLion里烧录最典型的配置是OpenOCD。先安装官方插件Embedded Development,然后在Run/Debug Configuration里新建一个OpenOCD Download & Run配置,核心参数:

  • OpenOCD路径:填你本机的openocd路径。
  • 目标配置文件:如果没有对应board配置,用通用组合:
    • interface/stlink.cfg
    • target/stm32f1x.cfg 多个文件用空格分隔,OpenOCD会按顺序加载。

关键点来了:OpenOCD加载elf文件烧录时,目标地址取自elf的段地址。所以只要你链接到了0x08004000,烧录时不会碰到0x08000000,Bootloader安全。这也是为什么我强烈建议:App开发尽量用elf烧录,而不是bin。

但如果不得不用bin文件烧录,OpenOCD命令必须带地址:

openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \ -c "program build/my_project.bin 0x08004000 verify reset exit"

这里的0x08004000必须与ld的ORIGIN一致。写错就是把App烧到别处或覆盖Bootloader。很多人遇到的问题“App烧进去后板子没反应,再一看Bootloader没了”,十有八九就是bin烧录忘了指定偏移地址。

用STM32CubeProgrammer CLI时也有同样问题:

STM32_Programmer_CLI -c port=SWD -d app.elf

这个直接烧elf没问题;如果烧bin,命令行里需要按工具说明指定起始地址。核心原则不变:烧录地址必须等于链接脚本ORIGIN。

4.4 调试时的地址视角:用OpenOCD + map文件验证

烧录成功后做两步验证,防止看起来“跑起来”但实际地址不对:

  1. 在OpenOCD console里敲reset halt,再敲reg pc。如果你正在调试App,PC应该停在0x08004000附近(Reset_Handler所在位置)。如果PC停在0x08000000,说明你其实在跑Bootloader,App根本没被正确启动。
  2. 在OpenOCD里执行x/32wx 0x08004000,看一下向量表内容。前4字节应该是合法的栈顶地址(形如0x20004xxx),第2个4字节应该是Reset_Handler地址(形如0x0800xxxx)。如果读到的全是0xFFFFFFFF或乱码,说明Flash没烧进去或烧错位置。

这两步操作很快,但能帮你确认“三地址一致律”里的链接地址和烧录地址到底对齐没有。CLion用户可以在Run面板切到OpenOCD的Console视图直接操作。

map文件同样有用。比如查某个函数的实际地址:

arm-none-eabi-nm -n build/my_project.elf | grep main

如果输出是08004xxx T main,说明main被链接到了偏移后的地址段。

还有一个实用技巧:在CMake里加一个自定义target,把烧录命令集成进去,这样终端里一行命令就能烧录,不必每次都依赖IDE配置。我后面给的完整CMake里就带了flash_app这个target。

5. 踩坑记录:改完Flash地址后最容易翻车的5个错

5.1 长度少算或多算,程序越界写入空洞区

前面说了LENGTH不跟着ORIGIN走就会出问题,但还有另一个极端——有人把LENGTH写成64K-16K=48K,可芯片实际型号是CBT6(128K Flash),莫名其妙白丢一大块空间。所以第一步永远是搞清楚自己芯片Flash总共多大,用“Flash总容量 - Bootloader占用”来算App的LENGTH。

C8T6是64KB,CBT6是128KB,别记混。还有个小众话题:网上说C8T6标称64K但实际刻了128K,能当128K用。我建议工程上还是按官方64K规划,因为你无法保证每颗芯片都能白嫖超出的空间,换一批料可能就翻车。

5.2 只改ld不改VTOR,主循环正常但一切中断HardFault

这是我见过最多、最容易误判的故障。表现是App的main能执行、LED能闪(因为主循环一直在跑),但只要按一下按键、开一个定时器、来一个串口中断,立刻进HardFault。很多人第一反应以为是自己中断配置写错了,排查半天。

实际上就是VTOR还指向0x08000000(Bootloader区域)。CPU一进中断去Bootloader的向量表里取Handler地址,取到的是随机数据或Bootloader的中断处理函数,总之不是App的,程序自然就炸了。

所以:改了链接脚本,第一件事就去改system_stm32f1xx.c的VECT_TAB_OFFSET,不要等HardFault出现再回头查。

5.3 烧录bin时不指定偏移,Bootloader被无情覆盖

用elf烧录不会出这个错,因为elf自带地址。用bin烧录就非常容易翻车,尤其在某些图形化烧录工具里默认从0x08000000开始烧。我不止一次看到有人把App的bin直接刷掉Bootloader,然后来问“Bootloader怎么连不上了”。

解决方案前面已经给过:OpenOCD带地址烧bin,或者干脆永远用elf烧录。如果你是通过STM32CubeProgrammer手动烧,每次都要确认界面里填的起始地址。

另外,烧录时如果遇到“flash download failed”这类报错,先查一下下载器接线和驱动,再检查烧录地址是否有效——地址超出了芯片Flash范围,下载器同样会报错。

5.4 CLion调试器加载elf后PC位置对不上

用CLion调试App时,有一种情况:程序能跑、能断点,但reset后PC停在0x08000000而不是0x08004000。这是因为OpenOCD的reset会走一次芯片复位,如果调试的App不在物理地址0x08000000处,Bootloader会接管启动。

如果你的调试目标是App本身,想从App入口单步,比较稳妥的做法是在OpenOCD配置里加:

-c "init" -c "reset halt" -c "reg pc 0x08004000"

或者更简单:在App的Reset_Handler处打一个断点,然后用OpenOCD手动reset halt,再执行reg pc 0x08004000,接着continue到断点。这样能跳过Bootloader,直接进入App的调试上下文。

如果你要做Bootloader跳转App的联合调试,那就把OpenOCD的config配置好flash bank地址,加载App符号表,确保断点落在0x08004000区间的代码上。

5.5 升了CubeMX版本或换了芯片型号,ld被重新解析

除了手动Generate Code会覆盖ld,还有一个隐性坑:在CubeMX里从C8T6换成CBT6,CubeMX会重新生成对应容量的ld文件;CubeMX大版本升级后首次生成工程,模板也可能变化。如果ld文件不是你手动维护的独立文件名,防不胜防。

所以我的最终建议是:把ld文件当成“源文件”纳入工程管理,不要让它成为CubeMX的“一时生成物”。独立命名+在CMakeLists里显式引用,就是最适合CubeMX+CLion组合的方案。

6. 可直接套用的完整CMake配置与Bootloader跳转参考代码

6.1 完整CMakeLists.txt(C8T6/CLion/OpenOCD)

这是一份经过实战验证的CMakeLists.txt,你可以直接放在工程根目录使用。它基于CubeMX生成的版本做了精简和调整,注释说清楚了每部分的作用:

cmake_minimum_required(VERSION 3.22) project(stm32f103_app C ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) # 裸机环境 set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) # 编译器由CLion Toolchain提供,这里做兜底 set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) # MCU编译选项:Cortex-M3,Thumb指令集 add_compile_options(-mcpu=cortex-m3 -mthumb -Wall -fdata-sections -ffunction-sections) add_compile_definitions(STM32F103xB USE_HAL_DRIVER) # 手动维护的链接脚本(CubeMX重新生成不会覆盖) set(LINKER_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/my_linker.ld) # 链接时生成map文件,方便核对地址 add_link_options(-T${LINKER_SCRIPT} --specs=nosys.specs -Wl,--gc-sections -Wl,-Map=${PROJECT_NAME}.map) # 收集源文件 file(GLOB_RECURSE SOURCES Core/Src/*.c Core/Startup/*.s Drivers/STM32F1xx_HAL_Driver/Src/*.c Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/*.c ) add_executable(${PROJECT_NAME}.elf ${SOURCES} ${LINKER_SCRIPT}) # 生成bin与hex add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.bin COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.hex COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${PROJECT_NAME}.elf> ) # 烧录App到0x08004000,OpenOCD命令 add_custom_target(flash_app COMMAND openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.bin 0x08004000 verify reset exit" DEPENDS ${PROJECT_NAME}.elf ) # 用elf烧录(地址来自elf自身,更安全) add_custom_target(flash_elf COMMAND openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.elf verify reset exit" DEPENDS ${PROJECT_NAME}.elf )

其中my_linker.ld的关键段(以16KB Bootloader、App从0x08004000为例):

MEMORY { FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 48K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K }

6.2 Bootloader跳转App的参考代码

假设你的Bootloader通过串口或按键判断需要启动App,跳转函数如下:

#include "stm32f1xx_hal.h" #define APP_FLASH_BASE 0x08004000u #define SRAM_BASE 0x20000000u #define SRAM_END 0x20004FFFu /* C8T6: 20KB SRAM,按实际型号调整 */ void JumpToApp(void) { /* 1. 关闭全局中断,DeInit用到的外设 */ __disable_irq(); HAL_UART_DeInit(&huart1); /* 如有其他外设,也恢复默认状态 */ /* 2. 从App向量表读出栈顶指针和复位向量 */ uint32_t app_sp = *(volatile uint32_t *)APP_FLASH_BASE; uint32_t app_pc = *(volatile uint32_t *)(APP_FLASH_BASE + 4); /* 3. 校验栈顶指针是否落在SRAM范围,防止跳到非法区域 */ if (app_sp < SRAM_BASE || app_sp > SRAM_END) { return; /* App无效,继续留在Bootloader */ } /* 4. 设置向量表偏移 */ SCB->VTOR = APP_FLASH_BASE; /* 5. 设置主栈指针并跳转 */ __set_MSP(app_sp); typedef void (*pFunction)(void); pFunction jump = (pFunction)app_pc; jump(); while (1) { /* never reach here */ } }

这段代码最重要的两点:跳转前把Bootloader用到的外设全部复位,否则App初始化时可能遇到残留状态;跳转前校验栈指针范围,防止误跳。很多IAP问题不是出在“跳过去了”,而是出在“乱跳”。

6.3 整体关系回顾

再梳理一遍完整链路:

  1. Bootloader编译链接在0x08000000,烧录到0x08000000。
  2. App编译链接在0x08004000(ld的ORIGIN),向量表偏移0x4000(VECT_TAB_OFFSET),烧录到0x08004000(OpenOCD program命令的地址或elf内嵌地址)。
  3. Bootloader上电执行,检查App有效性,跳转前设置SCB->VTOR和MSP,然后跳转。
  4. App的SystemInit根据VECT_TAB_OFFSET再次设置VTOR,保证中断向量正确。
  5. 至此,App中的所有中断、外设、主循环都在0x08004000地址段正常运行。

你只要把“三个地址”全部对齐,这套方案在任何F103工程上都成立。

写到最后,分享一个我自己的习惯。每次调整Flash起始地址,我会先在一张纸上画Flash分区图,把Bootloader、App、参数区的地址范围全部标出来,然后对照“三地址一致律”逐项检查:链接脚本ORIGIN/LENGTH、VTOR偏移、烧录命令地址,三个地方的值是否一致、长度是否算对。别小看这张图,很多时候排查半天的问题,回头看就是某个地址差了一个0。

如果你现在正卡在“App能编译能烧录但跑不起来”,建议顺着这篇文章的顺序重查一遍:先看map文件确认.text段起始地址,再看VTOR有没有设对,最后确认烧录命令的bin偏移。大概率就是这三者之一出了问题。祝一次打通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 1:26:45

GeoServer Windows安装全指南:Java环境配置与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:25:20

STM32参考设计实战指南:从原理图到代码的落地方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:25:20

吉林大学编译原理实验包:可调试可扩展的C++编译器前端工程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:25:12

APDL命令流实现混凝土与形状记忆合金高精度本构建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:24:41

数学建模AI助手MM-Agent:专为建模工作流设计的智能胶水层

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:24:02

YOLOv8-seg掩码后处理全解析:从系数到像素级分割

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华