
搞嵌入式这几年我遇到最多的一种“假成功”就是编译零错误、烧录提示成功、复位之后板子却一点反应都没有。尤其是当你刚刚把工程的 Flash 起始地址从 0x08000000 改到 0x08008000准备给 Bootloader 腾地方、做 OTA 升级的时候这种“静默翻车”几乎成了固定节目。这篇文章就专门解决这个问题围绕 STM32F103 CubeMX CLion 这套组合把 Flash 起始地址调整这件事从原理到落地完整拆开包括 CubeMX 端怎么配合、链接脚本怎么改、中断向量表 VTOR 怎么设最后给一份可以直接抄的 CMake 构建配置并附上我在实际项目中踩过的下载失败、程序跑飞等典型坑位排查记录。如果你正在做 Bootloader App 的双区固件方案或者想在 Flash 最前面预留一块参数存储区又或者你刚从 Keil 迁移到 CLion、对 CMake 接管嵌入式工程还不太熟这篇文章应该能帮你少走不少弯路。1. 为什么要调整 Flash 起始地址1.1 什么场景下必须动 Flash 起始地址STM32F103 的片内 Flash 默认映射地址是 0x08000000芯片上电复位后CPU 会第一时间跑到这个地址读取栈顶指针和复位向量然后开始执行用户代码。如果不做任何特殊处理你编译出来的整个程序都会被链接器安排在 0x08000000 开始的连续空间里。但有三种场景你必须让 App 的代码挪个窝Bootloader App 架构这是最常见的需求。Bootloader 放在 Flash 的最前面比如 0x08000000 ~ 0x08007FFFApp 从 0x08008000 开始。这样 Bootloader 可以先跑起来检查升级标志、接收固件包然后跳转到 App 执行。如果 App 还是从 0x08000000 开始Bootloader 和 App 就会互相覆盖整个方案直接崩塌。Flash 头部预留数据区比如产品序列号、校准参数、升级标志位你想固定存放在 Flash 最前面的几页里。这些数据不希望被代码覆盖也不希望每次擦写代码时连数据一起擦掉。把 App 整体后移预留区就稳了。开发阶段隔离调试如果你同时在调试 Bootloader 和 App 两个工程两者都用默认地址会导致反复擦写冲突。给 App 单独设定偏移地址后可以做到互不干扰烧录不踩雷。另外需要提醒一句STM32F103 的 Flash 是有页面概念的中容量系列每页 1KB大容量系列每页 2KB。你选择的 App 偏移地址最好按页对齐否则擦写时容易误伤其他区域的数据。1.2 调整的本质链接、向量表、下载三层配合很多人改 Flash 起始地址失败是因为只盯着一个地方改。实际上这个操作需要三层同时配合第一层是链接层。你要告诉链接器代码从新地址开始排布这个动作靠修改链接脚本里的 FLASH ORIGIN 完成。链接器会据此把所有函数、常量、中断向量表安排到新地址区间。第二层是运行层。芯片上电后 CPU 并不会自动跳到你设置的 0x08008000它依然从 0x08000000 取向量。如果 0x08000000 是 BootloaderBootloader 需要在跳转前把栈指针、程序计数器设到 App 入口如果 0x08000000 没有代码App 被直接烧录后还需要靠中断向量表偏移寄存器 VTOR 把异常向量表重定位到新地址。否则一旦发生中断CPU 去 0x08000000 查中断函数指针拿到的要么是空指针要么是跳到错误地址的野指针程序必挂。第三层是烧录层。下载器要把固件写到正确位置。这里有个容易被忽略的细节大多数下载工具不是按固定地址烧录的而是读取 ELF 或 HEX 文件里记录的地址信息。也就是说如果你的链接脚本把代码定位到了 0x08008000烧录器就会自动把固件写到 0x08008000并不会破坏 Bootloader 区域。这个特性做得好能省掉很多手动配置地址的麻烦前提是你得确保链接脚本确实生效了。用一个不太准确但很好理解的比方Flash 起始地址相当于房子的门牌号。链接脚本负责把“家具”摆进新房子里VTOR 负责告诉 CPU “进了门之后应该先看哪块地砖”而下载器负责把“家具”实际搬进新房子里。三层都对上程序才能跑得起来。2. 环境准备CubeMX CLion 交叉工具链2.1 工具链清单与最小系统硬件在正式开始调整 Flash 地址之前先把整套环境捋一遍。我假设你用的是最常见的 F103C8T6 最小系统板以及 ST-Link/V2 或 DAP 下载器。用 J-Link 的读者也不用慌后面我会单独提 J-Link 的配置区别。软件层面需要准备这四样STM32CubeMX用来生成初始化代码。推荐 6.x 以上版本新版本对生成 CMake 工程的支持更友好。CLionJetBrains 的 IDE对 CMake 原生支持嵌入式开发体验在 2024 年之后改善非常明显。CLion 不是免费软件但学生、教师、开源项目维护者可以通过官方渠道申请免费授权大家按自己的情况选择正规途径。arm-none-eabi-gcc 交叉编译工具链这是实际干活的编译器。安装完成后记得把 bin 目录加入系统 PATH并在终端里执行arm-none-eabi-gcc --version确认能正常打印版本号。OpenOCD 或 J-Link 驱动负责烧录和调试。如果你用 ST-Link装 OpenOCD 最省心如果用 J-Link安装 J-Link Software Pack 即可。硬件上F103C8T6 最小系统板、ST-Link、几根杜邦线就够了。接线就三个关键信号SWDIO、SWCLK、GND有条件的话再补一根 3.3V 给板子供电。很多下载失败的玄学问题到最后查来查去就是杜邦线接触不良。2.2 CubeMX 工程生成方式的选型CubeMX 在生成工程时会让你选 Toolchain/IDE常见选项有 STM32CubeIDE、MDK-ARM、Makefile新版还有 CMake。这里我给一个明确建议如果你要在 CLion 里做开发优先选 Makefile 生成器然后手动在工程根目录补一份 CMakeLists.txt让 CLion 以 CMake 项目模式加载。为什么不直接选 CubeMX 自带的 CMake 生成器因为新版 CubeMX 生成的 CMakeLists 虽然能加载但自由度有限你想追加链接选项、自定义生成 hex/bin、控制链接脚本路径都还要绕一圈。而选 Makefile 生成器之后CubeMX 会老老实实把外设初始化代码和 GCC 链接脚本.ld 文件都生成好我们在此基础上自己写 CMakeLists既灵活又可控。CLion 识别到 CMakeLists.txt 后会自动以 CMake 项目模式打开配合配置好的交叉工具链编译、烧录、调试一步到位。需要留意的是CubeMX 生成的 GCC 链接脚本文件名一般长这样STM32F103C8Tx_FLASH.ld。不同芯片、不同容量文件名有差异后面改脚本时千万不要找错文件。3. CubeMX 端配置与链接脚本修改3.1 Flash 偏移量怎么选Bootloader 大小与 App 空间你可能有疑问地址偏移到多少才合适这完全取决于你给 Bootloader 分配多大的空间。以下以 F103C8T6官方标注 Flash 64KBRAM 20KB为例Bootloader 大小Bootloader 地址区间App 起始地址App 可用 Flash 空间16KB0x08000000 ~ 0x08003FFF0x0800400048KB32KB0x08000000 ~ 0x08007FFF0x0800800032KB48KB0x08000000 ~ 0x0800BFFF0x0800C00016KB很多 Bootloader App 方案喜欢把偏移设为 0x08008000也就是 Bootloader 占 32KB。这样 Bootloader 能放下完整的升级协议、Flash 驱动和通信缓冲。但也要现实一点C8T6 的 App 空间只剩 32KB如果你的业务逻辑复杂、调试打印又多很容易把 Flash 塞爆。编译时报region FLASH overflowed by xxx bytes时不要慌先出优化再考虑是不是把偏移缩小到 16KB。选地址还有一个硬约束STM32F103 的中断向量表偏移要求按字对齐实际项目里更稳妥的做法是按 256 字节对齐。0x08004000、0x08008000 这类地址本身就是 0x100 的整数倍天然满足要求。怕就怕你拍脑袋设成 0x08004100 这种半吊子地址既浪费空间还可能触发对齐问题。3.2 修改链接脚本 FLASH ORIGIN核心动手步骤CubeMX 生成的.ld文件里最核心的就是MEMORY块。默认内容大致如下/* Entry Point */ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _estack 0x20004FFF; /* end of 20K RAM */ MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K }注意FLASH这一行的两个参数ORIGIN是起始地址LENGTH是可用长度。把 App 的起始地址挪到 0x08008000同时 Bootloader 占掉前 32KB那就改成这样MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K FLASH (rx) : ORIGIN 0x08008000, LENGTH 32K }这里有个新手容易踩的坑只改ORIGIN不改LENGTH。如果保持LENGTH 64K链接器会认为从 0x08008000 到 0x08017FFF 都是你的可用空间但 F103C8T6 的 Flash 物理上只到 0x0800FFFF。编译可能不报错但烧录后访问到不存在的地址程序通常在初始化阶段就跑飞。正确做法是LENGTH 物理 Flash 容量 - 偏移量。64KB 减 32KB就是 32KB。修改完.ld文件后建议顺手确认一下 RAM 区域不用动。我们这里只做代码段偏移RAM 地址保持 0x20000000 不变除非你有复杂的 RAM 分区需求否则改 RAM 就是给自己找麻烦。3.3 中断向量表重定位VTOR 设置链接脚本改完之后下一个关键点是 VTOR。F103 是 Cortex-M3 内核VTOR 寄存器地址是 0xE000ED08。在 App 的main函数最前面建议加这样一行/* 定义在公共头文件里便于 Bootloader 和 App 共用 */ #define APP_FLASH_BASE 0x08008000 int main(void) { /* 重定位中断向量表到 App 起始地址 */ SCB-VTOR APP_FLASH_BASE; /* 其余的 HAL_Init、时钟配置等照常 */ HAL_Init(); SystemClock_Config(); // ... }为什么要放在最前面因为一旦使能中断CPU 就会从 VTOR 指向的地址查向量表。如果中断来得比 VTOR 设置还早CPU 仍会去 0x08000000 查中断入口在 App 场景下这就是灾难。如果你不是裸机直接从 0x08000000 启动而是从 Bootloader 跳转到 App那么跳转前的准备工作更关键。典型的跳转代码是typedef void (*pFunction)(void); static void jump_to_app(uint32_t app_addr) { uint32_t app_sp *(volatile uint32_t *)app_addr; pFunction app_reset (pFunction)(*(volatile uint32_t *)(app_addr 4)); /* 进入 App 前必须关中断 */ __disable_irq(); /* 重定位向量表 */ SCB-VTOR app_addr; /* 设置 App 的栈指针 */ __set_MSP(app_sp); /* 跳转到 App 的 Reset_Handler */ app_reset(); }跳转地址app_addr就传 0x08008000。App 的 Reset_Handler 执行后会重新初始化栈和中断最终进入 main。这一段代码我先放这儿后面的常见问题里还会提到跳转失败的具体表现。4. CLion 下 CMake 完整配置与实操4.1 CMakeLists.txt 完整示例与逐行解析在 CLion 里CMakeLists.txt 相当于传统 IDE 里的工程文件。下面这份配置我实际跑过F103C8T6 最小系统板配 ST-LinkOpenOCD 下载稳定可用。你可以直接复制再按自己的目录结构微调。cmake_minimum_required(VERSION 3.16) project(stm32f103_app C ASM) # 目标平台信息 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) # 全局编译选项 set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} -mcpucortex-m3 -mthumb -ffunction-sections -fdata-sections -Wall -g -gdwarf-2) set(CMAKE_EXE_LINKER_FLAGS ${CMAKE_EXE_LINKER_FLAGS} -mcpucortex-m3 -mthumb -Wl,--gc-sections -Wl,-Map${PROJECT_NAME}.map) # 收集源文件按需增删路径 file(GLOB_RECURSE SOURCES Core/Src/*.c Core/Src/*.s Drivers/STM32F1xx_HAL_Driver/Src/*.c ) # 头文件路径 target_include_directories(${PROJECT_NAME}.elf PRIVATE Core/Inc Drivers/STM32F1xx_HAL_Driver/Inc Drivers/STM32F1xx_HAL_Driver/Inc/Legacy Drivers/CMSIS/Device/ST/STM32F1xx/Include Drivers/CMSIS/Include ) # 宏定义具体按 CubeMX 生成的配置来 add_definitions(-DSTM32F103xB -DUSE_HAL_DRIVER -DDEBUG) # 生成目标 add_executable(${PROJECT_NAME}.elf ${SOURCES}) # 指定链接脚本 target_link_options(${PROJECT_NAME}.elf PRIVATE -T ${CMAKE_SOURCE_DIR}/STM32F103C8Tx_FLASH.ld) # 生成 hex 和 bin 文件 add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND arm-none-eabi-objcopy -O ihex ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex COMMAND arm-none-eabi-objcopy -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin )几个关键点说一下-mcpucortex-m3 -mthumb是 F103 必须的Cortex-M3 内核 Thumb 指令集缺一个都会编译出问题。-ffunction-sections -fdata-sections配合-Wl,--gc-sections能裁掉没使用的函数和数据对 Flash 空间紧张的 C8T6 很有用。-T ${CMAKE_SOURCE_DIR}/STM32F103C8Tx_FLASH.ld是指定链接脚本的关键路径里如果包含空格或反斜杠很容易在 Windows 环境出问题建议把工程放在纯英文无空格的目录下。另外眼尖的读者会注意到这里没有单独设置CMAKE_OBJCOPY实际用arm-none-eabi-objcopy时我写的是完整命令行路径。原因很简单我们只设置了 C 和 ASM 编译器CLion 不一定把工具链目录自动导入 CMake 环境变量直接写完整命令最稳。如果你想做得更规范可以在前面补一句set(CMAKE_OBJCOPY arm-none-eabi-objcopy)然后 POST_BUILD 里用${CMAKE_OBJCOPY}。两种写法都行看个人习惯。4.2 下载器与调试配置OpenOCD / J-LinkCLion 下载和调试走的是 OpenOCD 或 J-Link 插件。以 ST-Link OpenOCD 为例你需要准备一个 OpenOCD 配置文件通常命名为stm32f103.cfg内容只要两行source [find interface/stlink.cfg] source [find target/stm32f1x.cfg]然后在 CLion 的设置里导航到 Build, Execution, Deployment - Embedded Development选择 OpenOCD 作为调试器并把配置文件路径指到刚才的stm32f103.cfg。CLion 会自动用 OpenOCD 完成擦除、烧录和调试。这里我要特别强调一个原理OpenOCD 烧录时是直接解析 ELF 文件里的 LOAD 段地址的。你的链接脚本把代码定位到 0x08008000OpenOCD 就会把代码写入 0x08008000完全不会碰 0x08000000 开始的 Bootloader。这比 Keil 里手动设置 IROM 地址再单独配下载算法要直观得多也是 CLion CMake 组合的一条隐藏优势。如果你用 J-Link配置方式类似只是 OpenOCD 换成 J-Link 插件在 Embedded Development 里选择 J-Link并填上设备型号STM32F103C8。要注意 J-Link 连接 F103 时如果目标板供电不稳或者 SWD 速度太高会出现握手失败后面常见问题里我会重点讲。4.3 从生成工程到烧录验证的完整流程我把完整操作串一遍方便你对着做。第一步在 CubeMX 里选好芯片比如 STM32F103C8Tx配置好时钟、串口、GPIOProject Manager 里 Toolchain 选 Makefile然后生成工程。第二步修改链接脚本里的 FLASH ORIGIN 和 LENGTH改成 0x08008000 和 32K保存。第三步在 main.c 开头加上SCB-VTOR APP_FLASH_BASE;并定义APP_FLASH_BASE宏。此时可以顺手在 main 里加一行调试输出方便验证printf(VTOR 0x%08lx\n, (unsigned long)SCB-VTOR); printf(main %p\n, (void *)main);如果串口打印出来main的地址落在 0x08008000 之后说明链接已经生效VTOR 打印出来是 0x08008000说明向量表重定位也成功了。这一步验证花不了 2 分钟但能帮你把前面所有环节一次性串起来。第四步在工程根目录新建 CMakeLists.txt把上面 4.1 的配置复制进去按需调整源文件路径。然后用 CLion 打开这个目录它会自动识别 CMake 工程。第五步配置 OpenOCD编译下载。如果一切顺利你打开串口助手就能看到上面那两行调试输出。实测下来这套流程从头到尾大概半小时。如果中途报错大概率就落在两个地方链接脚本路径没写对或者 CMake 缓存没刷新。5. 常见问题与排查技巧实录5.1 flash download failed 系列报错不管你是用 OpenOCD、J-Link 还是 DAP下载失败永远是最打击士气的事情。报错里最常见的就是Error: flash download failed - target dll has been cancelled。这个提示虽然看着像软件崩溃但绝大多数时候是硬件或接线问题而不是工具坏了。我的排查顺序基本固定查接线SWDIO、SWCLK、GND 三根线必须可靠连接杜邦线松动是头号元凶。下载时用手压一压线如果报错消失那就是接触不良。查供电很多最小系统板用 USB 供电如果下载器还要从目标板取电压降很容易导致握手失败。极端情况下把目标板改成外部独立供电只接 GND 和信号线。查 BOOT 引脚状态F103 的 BOOT0 和 BOOT1 决定启动模式。如果 BOOT0 被拉高芯片上电后进入串口下载模式或 SRAM 启动模式此时 SWD 连接可能一切正常但烧录 Flash 会异常。一般开发阶段 BOOT0 接 10K 下拉电阻保持在 0 状态。降速再试在 OpenOCD 或 J-Link 配置里把 SWD 时钟降到 100kHz 或更低尤其当杜邦线超过 10cm 时高速 SWD 很容易受干扰。手动复位时序在 OpenOCD 里启用 reset 配置或者在下载软件里勾选“下载前复位”。某些代码把 PA13/PA14SWD 引脚复用成了普通 GPIO调试器会直接失联这种情况下必须让目标板在下电瞬间保持复位状态才能重新连接。如果你用 ST-Link 出现target dll has been cancelled可以先在终端里跑一次openocd -f stm32f103.cfg -c program build/stm32f103_app.elf verify reset exitOpenOCD 会把具体失败环节打印得更详细比 IDE 里的报错信息有用得多。5.2 改完地址程序跑飞的排查顺序烧录成功后板子没反应这比下载失败更让人头疼。我的排查顺序是第一看链接脚本是否真的生效。我在实际项目里遇到过改了.ld文件但 CLion 没有刷新 CMake 缓存编译时用的还是旧链接脚本代码仍然落在 0x08000000。检查方法很简单编译后看一眼生成的.map文件搜.text段的起始地址。如果地址是 0x08000000说明链接脚本没生效删掉 build 目录重新 configure。第二看 VTOR 有没有设置。很多人在 main 里加了一行SCB-VTOR 0x08008000但位置放错了放在了某个外设初始化之后。第一次使能中断时向量表还没重定位CPU 直接跑飞。硬核一点的排查方法是在 main 第一行打断点单步执行到 VTOR 设置语句之后用调试器的寄存器面板读 0xE000ED08 的值确认是不是 0x08008000。第三看 Bootloader 跳转逻辑是否正确。如果你有 Bootloader跳转前必须确保全局中断已关闭、VTOR 已设置、MSP 已更新。这三个条件缺一个App 的表现就是“烧进去了但按复位没反应”。跳转代码建议参考我在 3.3 节给出的模板不要自己发明。第四检查 stack size 和 heap size。CubeMX 生成的启动文件里默认堆栈是 0x4001KB如果 App 用了 printf、浮点运算、RTOS栈很容易爆。栈溢出的话程序会在不明所以的指令位置跑飞和 Flash 地址问题表现很像。把启动文件里的Stack_Size调大到 0x800 或 0x1000能排除一大部分玄学问题。第五Boot0 引脚状态反复确认。做过一次下载失败排障之后我就长记性了如果板子上有 BOOT0 跳线帽永远先确认它是不是被插到了 1 的位置。这个跳线帽害我排查过整整一个下午。5.3 用 map 文件验证 Flash 起始地址map 文件是编译链接后生成的符号地址清单也是排查地址类问题最有力的工具。在 CMake 配置里我加了-Wl,-Map${PROJECT_NAME}.map编译完成后工程目录下会自动生成.map文件。打开 map 文件找到类似这样的片段.text 0x08008000 0x1234如果.text段的起始地址不是 0x08008000而是 0x08000000说明链接脚本没有生效赶紧回去查 CMake 缓存。如果地址正确再往下翻找到__initial_sp和Reset_Handler这两个符号它们的地址也应该在 0x08008000 之后。这几个地址全对链接层的任务才算完成。很多时候map 文件里藏着的不是“有没有改对”的问题而是“链接器认为哪里是起点”的问题。你可能觉得自己改的是STM32F103C8Tx_FLASH.ld但工程里其实同时存在多个.ld文件CMake 通过-T选项指定的是另一个。用 map 文件一验立刻现形。结尾两个小建议折腾完这一整套我心里最大的体会是调整 Flash 起始地址这事80% 的坑都不在技术难度而在“三层各改各的、最后没有对齐”。链接脚本改了、VTOR 忘了设或者 CMake 缓存没刷新、还在用旧脚本这些我都一一踩过。建议你新建一个专门的app_config.h把APP_FLASH_BASE这类地址统一管理起来Bootloader 跳转、App 的 VTOR 设置、链接脚本里的宏注释都引用同一套数字至少能少一多半的冤枉问题。最后再分享一个小技巧第一次调试 App 时不要直接点击 Run先用 Debug 模式跑到 main然后看 PC 指针是不是停在 0x08008000 附近的地址。PC 能进去地址就成功了一大半PC 跑飞优先检查 VTOR 和跳转前的 MSP。改地址这件事无非就是把烧录、链接、运行三件事想清楚然后把细节落实到位。