Keil5烧录STM32F103:Flash地址配置的实战全解析
你有没有遇到过这样的情况?代码编译通过,Keil也显示“Download Success”,但单片机一上电就卡死、进不了main函数,甚至直接HardFault?调试器连上去一看,堆栈指针MSP指向一个莫名其妙的地址——别急,这很可能不是硬件坏了,而是Flash地址没配对。
在STM32开发中,尤其是涉及Bootloader、IAP(在应用编程)或多段固件部署时,程序放在哪、从哪开始执行、中断向量表在哪,这些看似基础的问题一旦出错,就会导致系统启动失败。而这一切的核心,就是我们今天要深挖的主题:Keil5环境下,如何正确配置STM32F103的Flash地址。
本文不讲空话,只聚焦实战。我们将从芯片启动机制出发,一步步拆解Keil的链接过程、内存映射原理,并手把手教你配置多级启动系统,最终实现一个稳定可靠的跳转逻辑。无论你是刚入门的新手,还是正在调试IAP的老兵,这篇文章都能帮你绕开那些“看不见的坑”。
为什么0x08000000这么重要?
先说结论:STM32F103的用户程序必须从0x08000000开始存放,否则极大概率无法正常启动。
但这背后到底发生了什么?
启动那一刻发生了什么?
STM32上电后,CPU并不会直接执行你的main()函数。它做的第一件事是:
从地址0x0000_0000读取第一个字作为主堆栈指针(MSP),第二个字作为复位向量(Reset Handler)
听起来很简单,但关键在于:物理Flash并不在0x0000_0000。真正的Flash起始地址是0x0800_0000。
那CPU怎么找到程序的?答案是——重映射(Remap)。
通过BOOT0和BOOT1引脚的电平组合,STM32会将不同的存储区域“映射”到0x0000_0000这个逻辑地址上。最常见的配置是:
- BOOT0 = 0 → 主Flash被映射到0x0000_0000
- 此时,0x0000_0000 实际指向 0x0800_0000
所以,当你把程序烧录到0x0800_0000时,CPU上电后就能通过重映射机制,在0x0000_0000处读到正确的MSP和Reset Handler,从而顺利启动。
🔥 如果你把程序烧到了0x0800_8000,但没有做任何处理,那么0x0000_0000处仍然是空白或旧数据——结果就是堆栈指针乱飞,HardFault不可避免。
Keil5是怎么决定程序烧到哪里的?
很多人以为,只要点了“Download”按钮,Keil就会自动把程序放到正确位置。其实不然。程序烧录到哪个地址,是由链接器(linker)决定的,而链接器的行为,又由两个地方控制:
- Target选项中的IROM设置
- 分散加载文件(.sct)
方法一:图形界面配置(适合简单项目)
打开Keil5 → Project → Options for Target → Target 选项卡:
| 参数 | 典型值 | 说明 |
|---|---|---|
| IROM1 Start | 0x08000000 | 程序烧录起始地址 |
| IROM1 Size | 0x20000(128KB) | 可用Flash大小 |
| IRAM1 Start | 0x20000000 | SRAM起始地址 |
| IRAM1 Size | 0x5000(20KB) | 可用SRAM大小 |
这是最简单的配置方式,适用于标准应用程序。Keil会自动生成默认的分散加载脚本,把代码段(RO)放在这段Flash里。
但如果你要做IAP、双Bank切换或者自定义分区,就必须上手写.sct文件了。
方法二:自定义.sct文件(高级玩法必备)
假设你现在要做一个带Bootloader的系统:
- Bootloader:0x0800_0000 ~ 0x0800_7FFF (32KB)
- 用户App:从 0x0800_8000 开始
这时候,你得为App工程单独创建一个链接脚本app.sct:
LR_APP 0x08008000 { ; 加载域起始地址 ER_APP 0x08008000 FIXED { ; 执行域固定在此 *.o (RESET, +First) ; 复位向量必须放最前面 *(InRoot$$Sections) .ANY (+RO) ; 其他只读代码 } RW_RAM 0x20000000 { ; 可读写段放SRAM .ANY (+RW +ZI) } }然后在 Keil 中关闭“Use Memory Layout from Target Dialog”,并指定这个.sct文件路径。
✅ 关键点解释:
-FIXED表示不允许链接器随意移动该区域,确保地址绝对准确。
-(RESET, +First)强制将包含复位向量的目标文件放在最前面。
-.ANY (+RO)收集所有只读代码段(函数、常量等)。
这样编译出来的.hex或.bin文件,代码就会从0x08008000开始生成,不会覆盖Bootloader。
烧录时别选错了Flash算法!
你以为写了正确的.sct就万事大吉?还有一个致命陷阱:Flash Algorithm选错。
Keil在烧录时依赖一个叫“Flash Programming Algorithm”的驱动文件(.flm),它封装了针对特定MCU的擦除和写入操作。STM32F1系列根据Flash容量分为几种类型:
- Low-density: ≤ 32KB
- Medium-density: ≤ 128KB
- High-density: > 128KB(如ZET6有512KB)
如果你的芯片是STM32F103ZET6(512KB Flash),却选了“Medium-density”算法,可能只能烧前128KB,后面全写不进去!
🔧 正确做法:
Project → Options for Target → Debug → Settings → Flash Download
→ 点击“Add” → 选择匹配的算法,例如:
STM32F10x High-density Flash
Keil通常能自动识别芯片并推荐合适的算法,但如果手动改过目标型号,记得回来检查一遍。
从Bootloader跳转到App:不只是函数指针那么简单
现在App已经烧到了0x08008000,接下来怎么跳过去?
很多初学者会这么写:
((void (*)(void))(*((uint32_t*)0x08008004)))();看起来没错:取App的复位向量地址(MSP+4),强转成函数指针调用。但实际上,这样做风险极高。
跳转前必须做三件事
1. 设置主堆栈指针(MSP)
每个程序都有自己的栈空间定义。如果不先设置MSP,一旦发生中断或局部变量压栈,就会访问非法内存。
uint32_t *app_msp = (uint32_t *)0x08008000; __set_MSP(*app_msp);2. 更新中断向量表偏移(VTOR)
Cortex-M3有一个寄存器叫SCB->VTOR,用来告诉CPU:“我的中断向量表不在默认位置,而在某个偏移处”。
如果你不更新VTOR,当中断触发时,CPU还会去0x0800_0000找ISR,而不是你App里的新向量表。
SCB->VTOR = 0x08008000;别忘了加内存屏障,确保指令同步完成:
__DSB(); __ISB();3. 关闭所有中断
跳转瞬间如果来了中断,而此时中断向量还没准备好,后果不堪设想。
__disable_irq();完整跳转函数如下:
#define APP_START_ADDR 0x08008000 typedef void (*pFunc)(void); void jump_to_app(void) { pFunc app_reset = (pFunc)*(uint32_t*)(APP_START_ADDR + 4); // 复位向量 uint32_t app_stack = *(uint32_t*)APP_START_ADDR; // MSP // 停止所有外设、关闭中断 __disable_irq(); __set_MSP(app_stack); // 切换堆栈 SCB->VTOR = APP_START_ADDR;// 重定向向量表 __DSB(); __ISB(); app_reset(); // 跳! }⚠️ 注意:这段代码执行后不会再回来。相当于“重启”进入新程序。
常见问题与避坑指南
❌ 现象:程序下载成功,但运行就HardFault
原因:堆栈指针MSP无效
排查:用调试器查看_initial_sp是否落在合法SRAM范围内(0x20000000~0x20005000)
解决:确认链接脚本中SRAM范围正确,且App的startup文件未被修改
❌ 现象:中断不响应,NVIC配置都对了
原因:VTOR没更新
解决:在跳转后立即设置SCB->VTOR = APP_START_ADDR;
❌ 现象:烧录时报“Flash Timeout”
原因:Flash算法不匹配 or 供电不足
解决:
- 检查所选.flm是否对应芯片密度
- 测量VDD是否 ≥ 2.7V(Flash操作要求)
- 检查SWD接线是否松动
❌ 现象:升级后App跑不起来,但重新烧录可以
原因:跳转前未关闭定时器、串口等外设中断
解决:在跳转前禁用所有可能产生中断的模块
实战架构参考:三级启动系统设计
一个典型的可靠嵌入式系统软件架构可能是这样的:
[0x08000000] ┌─────────────────┐ │ Bootloader │ ← 出厂固化,负责基本初始化和升级判断 [0x08004000] ├─────────────────┤ │ IAP模块 │ ← 接收新固件,执行擦写,支持回滚 [0x08008000] ├─────────────────┤ │ 用户App │ ← 实现业务逻辑,可通过命令触发升级 └─────────────────┘每一段都有自己独立的.sct配置,彼此互不干扰。升级时,IAP模块将新固件写入预留区域(比如0x0801_0000),验证无误后再替换当前App。
这种设计不仅提高了系统的可维护性,也为远程OTA升级打下基础。
写在最后:地址配置的本质是信任链
Flash地址配置看似是个技术细节,实则是整个系统可信执行起点的建立过程。
从CPU上电第一条指令,到Bootloader验证固件完整性,再到跳转时正确移交控制权——每一个环节的地址都必须精确无误。任何一处偏差,都会让整个系统的稳定性崩塌。
掌握Keil5下的内存布局控制,不仅仅是学会改几个参数,更是建立起一种底层思维:你知道代码最终落在哪块硅片上,也知道处理器如何一步步走到main函数。
下次当你点击“Download”时,不妨多问一句:我写的程序,真的会被正确加载吗?
如果你在实际项目中遇到更复杂的多核、加密启动或安全固件验证场景,欢迎在评论区交流讨论。