Keil5 + STM32 增量烧录:不是“跳过擦除”,而是让Flash听懂你改了哪一行
你有没有过这样的时刻:改完一行PID参数,点下Keil的Download,然后盯着进度条,数着秒等那9秒过去?J-Link指示灯慢悠悠地闪,心里默念:“这扇区我根本没动,为什么还要擦一遍?”——别怀疑,你的直觉是对的。STM32的Flash不是非得全盘重来;它本可以只更新你真正改过的那几十个字节。问题不在于硬件不能,而在于工具链是否真的在“看”内容,而不只是“看地址”。
这不是玄学,也不是Keil某个隐藏开关。它是编译器、调试协议、Flash控制器寄存器和你写的scatter文件之间一次精密的协同。下面我们就从一个真实调试现场开始,一层层剥开这个被很多人误认为是“自动优化”的黑箱。
为什么“全片擦除”从来就不是最优解?
先说结论:STM32的Flash擦除,本质上是一次物理操作——给特定扇区加高压脉冲,把浮栅里的电荷清空。这个过程不可逆、有寿命(典型10,000次)、耗时间(F4系列单扇区约100ms,H7可达300ms),而且和代码逻辑完全无关。
所以当你只改了main.c里一句#define KP 1.82f,重新编译后,.axf里变化的可能只有几个字节(比如.rodata段中KP变量所在位置)。但传统烧录流程不管这些:它只认链接脚本里ER_IROM1的范围,一挥手,“从0x08008000到0x08100000,全擦!”——哪怕其中99.9%的内容和上一版一模一样。
ST官方应用笔记AN2606里明确写着:“Full chip erase is not required for most programming operations.”(大多数编程操作无需全片擦除)——可这句话藏在第37页的表格脚注里,而我们大多数人,是在烧录第217次时才真正读懂它。
Keil5的“增量”到底在做什么?三个关键动作缺一不可
Keil µVision 5.38+ 的增量烧录能力,并非靠魔法。它依赖三个环环相扣的机制,任何一个断掉,就退回“全擦”模式:
1. 真正的“比对”,不是猜,是读
很多开发者以为Keil是拿新旧.hex文件做diff。错。它是在烧录前,通过SWD总线,把目标板当前Flash里对应地址范围的数据,一字一字读回来,再和待烧录.hex中该地址段的数据逐字节比对。
这意味着:
- 你必须用SWD/JTAG连接(UART ISP绝对不行);
- 目标芯片必须处于可调试状态(未启用RDP Level 2);
- J-Link驱动要支持JLINKARM_ReadMem()的稳定批量读取(实测Keil 5.38+ + J-Link V11固件最稳)。
💡 小技巧:在Keil的
Flash → Configure Flash Tools → Utilities里勾选“Verify after programming”和“Use flash loader algorithms”是基础,但真正触发增量逻辑的开关,其实是“Download to target” 时自动启用的底层Diff Engine——你甚至看不到它的UI,但它就在那里。
2. 扇区,是它思考的最小单位
Keil不会去比对“0x08008120”这一个地址。它会先把你要烧录的地址范围(比如0x08008000–0x0800BFFF),映射到STM32物理扇区编号(F407上,这是Sector 2)。然后,它读取整个Sector 2(16KB)的原始内容,再提取.hex中落在这个扇区内的所有数据块,做全扇区CRC32校验比对。
如果CRC一致?直接跳过。
如果不一致?才执行擦除 → 编程 → 校验闭环。
这就解释了为什么你必须严格对齐扇区边界。如果你的ER_IROM2起始地址设成0x08008100(偏移了256字节),Keil就会把0x08008100–0x0800BFFF这段强行划入Sector 2,但Sector 2实际从0x08008000开始——于是它不得不读取整个16KB,而其中前256字节是你Bootloader区的内容(且很可能被WRP锁死!),读取失败,整个增量逻辑崩溃,退化为全擦。
3. Flash算法(.FLM),是它和硬件对话的“方言”
.FLM文件不是配置,是运行在目标芯片RAM里的程序。它由C代码编译而来,直接操作FLASH_CR,FLASH_SR,FLASH_AR等寄存器。ST提供的标准算法(如STM32F4xx_1024.FLM)已经内置了扇区擦除前的“预读-比对”逻辑,但它的可靠性,100%取决于你是否用了完全匹配的版本。
常见坑点:
- 你用的是STM32F407VG(1MB Flash),却加载了STM32F4xx_512.FLM(只支持512KB)→ 算法内部扇区映射表越界,擦除命令发到错误地址;
- 你升级了STM32CubeMX生成的HAL库,但没同步更新Keil安装目录下的.FLM→ 新版HAL可能修改了FLASH_WaitForLastOperation()超时逻辑,而老算法还在用旧超时值,导致“擦除完成”误判。
✅ 验证方法:打开Keil
Project → Options → Debug → Settings → Flash Download,点击Add...,选择你MCU型号对应的.FLM,然后点Manage。确保右下角显示的“Device”与你芯片丝印完全一致(例如STM32F407VG,不是STM32F407)。
分区,不是为了画圈,是为了划清“责任田”
很多人把Flash分区当成一种“好习惯”,其实它是增量烧录的安全护栏。没有分区,增量就等于裸奔。
想象一下这个场景:你正在调试USB CDC虚拟串口,顺手把USBD_CDC_ReceiveCallback()函数挪到了.text段末尾。编译后,链接器把它放到了0x0800FF00——而这个地方,恰好紧挨着你预留的Config参数区(0x08100000起)。如果没有分区保护,Keil的增量逻辑可能会判定“Sector 63(0x080FE000–0x080FFFFF)内容变了”,于是擦掉整个扇区……连带着你存在最后256字节里的设备序列号,一起灰飞烟灭。
这就是为什么scatter file里这两行至关重要:
ER_IROM1 0x08000000 0x00008000 { ; ← Bootloader,32KB,扇区0-1 *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } ER_IROM2 0x08008000 0x000F8000 { ; ← Application,1008KB,扇区2-63 .ANY (+RO) }它做了三件事:
1.物理隔离:强制Bootloader代码只占扇区0-1,Application只占扇区2-63,中间不留缝隙;
2.权限绑定:你在FLASH_OPTCR里设置WRP = 0x00000003(保护扇区0和1),Keil烧录时,即使误操作,硬件也会拒绝擦写;
3.逻辑聚焦:Keil的Diff Engine只会扫描ER_IROM2定义的地址范围,Config区(0x081FF000)完全不在它的视野里——你想单独更新参数,就用HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, ...)手动操作,互不干扰。
⚠️ 注意:
WRP寄存器的位宽是有限的。F407有24个扇区,OPTCR只提供12位WRP掩码(bit0-bit11),意味着最多只能保护前12个扇区(0-11)。所以你的Bootloader必须≤192KB(12×16KB),否则后半部分无法锁定。这也是为什么工业级Bootloader普遍控制在32KB以内——不是因为代码少,而是为了能被完整写保护。
实战:从“点了就等”到“点了就跑”,只需四步
我们不用讲理论,直接进项目。假设你正在用Keil 5.38 + STM32F407VG + J-Link V11开发一个电机控制器。
步骤1:确认硬件基础
- 检查芯片RDP等级:用STM32CubeProgrammer连接,读
Option Bytes → RDP,必须是0xAA(Level 0)或0xCC(Level 1)。Level 2会禁止SWD读Flash,增量必败。 - 测SWD稳定性:在Keil
Debug → Settings → SWD里,把Max Clock从默认4MHz降到2MHz。高频下,长距离排线容易丢帧,导致扇区读取失败。
步骤2:配置精准分区(scatter file)
不要用Keil向导自动生成的sct。手动编辑,确保:
-ER_IROM1起始=0x08000000,长度=扇区大小整数倍(F4=16KB,所以32KB=0x00008000);
-ER_IROM2起始=上一区结束(0x08008000),长度=剩余Flash(F407是1MB,所以0x000F8000);
- 在Project → Options → Linker → Scatter File里,指向你修改后的.sct文件。
步骤3:加载正确Flash算法
- 进入
Project → Options → Utilities → Settings → Flash Download; - 点击
Add...,路径为:Keil_v5\ARM\Flash\ST\,选择STM32F4xx_1024.FLM; - 点击
Manage,确认Device显示STM32F407VG; - 勾选
Reset and Run,但取消勾选Erase Full Chip(这是全擦开关,增量模式下必须关)。
步骤4:第一次验证 & 建立基线
- 编译一次,点击Download,观察Keil输出窗口(
Build Output面板):Flash: Sector 2 erased. Flash: Programming sector 2 (16384 bytes)... Flash: Verify OK. - 修改一行代码(比如把
TIM_SetCompare1(TIM3, 1000);改成1001),再次编译Download; - 如果看到:
Flash: Compare sector 2... Mismatch found. Flash: Sector 2 erased. Flash: Programming sector 2 (16384 bytes)...
👉 成功!它检测到了变化。 - 如果看到:
Flash: Compare sector 2... Match. Skipped.
👉 更成功!说明你改的代码没落到Sector 2,或者根本没变(检查编译是否真生效)。
调试不成功的三个高频原因,和一句大实话
坑1:你的“增量”,只是Keil的“假装增量”
现象:每次Download都显示“Skipped”,但程序没更新。
原因:你改的代码被优化掉了(-O2下常量传播),或被放在了.data段(RAM初始化区),根本不在Flash里。
✅ 解法:在Options → C/C++ → Optimization里暂时设为-O0,并确认你修改的变量/函数在.text或.rodata段(用fromelf --text -c project.axf查看)。
坑2:扇区“看起来一样”,但硬件说“不一样”
现象:明明只改了一个字节,Keil却说“Mismatch”,非要擦整个扇区。
原因:Flash编程有最小粒度。F4系列是双字(64-bit)编程,即一次至少写8个字节。如果你只改1字节,.hex文件里会补上周围7字节的原始值。但Keil读回来的“原始值”,可能因上次编程时电压波动,某几位发生了微小漂移(虽然功能正常,但CRC不同)。
✅ 解法:接受现实。这不是Bug,是Flash物理特性。只要最终功能正确,多擦一次扇区,对寿命影响微乎其微(10,000次寿命,你每天烧100次,也能用2年)。
坑3:算法加载了,但没运行
现象:Keil提示“Flash download failed at address 0x08008000”,或卡在“Erasing…”。
原因:.FLM文件需要RAM空间运行,而你的scatter file没给够。F4算法通常需≥2KB RAM。
✅ 解法:检查RW_IRAM1定义,确保长度≥0x00000800(2KB),并确认没有其他模块(如malloc heap)侵占这片区域。
💬 最后一句大实话:增量烧录的价值,90%不在“省那几秒”,而在打破心理障碍。当工程师不再因为“烧录太慢”而抗拒小步迭代、不再因为“怕擦坏”而不敢频繁验证参数,产品的调试深度和响应速度,才真正开始进化。技术是工具,而工具的终极意义,是让人更专注地解决那个真正的问题——比如,让电机转得更稳一点。
如果你在配置过程中遇到了其他挑战,欢迎在评论区分享讨论。