news 2026/9/28 1:56:53

新版Keil烧录成功但程序不运行?AC6编译器优化是罪魁祸首

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
新版Keil烧录成功但程序不运行?AC6编译器优化是罪魁祸首

1. 现象复盘:下载成功,板子却像没烧一样

1.1 客户描述里的关键线索

前两天帮客户维护一块老板子,主控是 STM32F103C8T6,功能简单得很:上电点两盏 LED,跑一个串口上报逻辑。代码是两年前的旧工程,之前一直跑得好好的。结果客户换了台新电脑,重新装上最新版 Keil,编译、下载都提示成功,可一断电重启,板子就愣在那里,一点反应没有。

这个现象最迷惑人的地方在于:烧录工具明确显示 Flash Download Successful,点调试也能连上芯片,但程序就是不跑。如果你也遇到过类似的情况,大概率第一反应是怀疑硬件坏了、晶振没起振、BOOT0 配置不对,甚至怀疑芯片被锁死。我当时也把这一整套全查了一遍,最后才发现问题根本不是硬件,而是出在 Keil 新版本的工具链上。

这篇文章我会把完整的排查过程写出来,重点讲一个特别隐蔽、但近几年越来越常见的坑——新版 Keil 的默认编译器从 AC5 换成了 AC6,旧代码在 AC6 的优化下会出现"编译能过、运行就跑飞"的情况。同时也会给出详细的解决方法和一份"烧录后不运行"的通用排查清单,适合正在被 Keil 升级折腾的工程师,也适合刚入门 STM32 的开发者提前避坑。

1.2 烧录阶段的检查清单:先排除掉低级错误

在怀疑编译器之前,我还是按老规矩把基础问题先过了一遍。这个步骤很重要,不要跳过,因为"烧录后不运行"有相当一部分是低级错误导致的,排查顺序错了会浪费时间。

  • 供电检查:万用表量 VCC 和 GND,3.3V 正常,芯片表面没有异常发热。
  • 复位电路:NRST 引脚电压 3.3V,没有一直被拉低;手动按复位键,现象依旧。
  • BOOT0/BOOT1:BOOT0 为低电平,确认是从主 Flash 启动,不是进了系统存储器 Bootloader。
  • 晶振:示波器看 OSC_IN 引脚,8MHz 晶振有波形,说明时钟没出问题。

硬件这条路走完,我再回到烧录环节。Keil 下载后提示成功,不代表你烧进去的就是你以为的那个程序。这句话看起来是废话,但实际排查里很关键。保险起见,我用 ST-Link Utility 把芯片 Flash 内容整个读出来,和本地 hex 文件做了一次逐字节对比,完全一致。这说明程序确实烧进去了,而且没有校验错误。

到这里基本可以下结论:硬件没有问题,烧录没有问题,问题出在程序本身在板子上的运行行为变了。但是代码一个字没改,为什么换个 Keil 版本行为就变了?这就引出真正的核心矛盾——编译器换了。

2. 旧代码"带病运行"被 AC6 优化放大

2.1 新版本 Keil 默认编译器换成了什么

Keil MDK 从 5.14 版本开始就集成了 ARM Compiler 6(简称 AC6),但很长一段时间里默认编译器还是 ARM Compiler 5(简称 AC5)。直到 MDK 5.37 左右,新装的 Keil 默认选项变成了 AC6。很多老工程师升级 Keil 后并不会注意到这个变化,因为新建工程或者打开旧工程时,界面长得几乎一模一样,编译下载的按钮也在同一个位置。

但两者底层差异非常大。AC5 是 ARM 自家的 armcc 编译器,AC6 基于 LLVM/Clang 架构,从编译策略、优化逻辑到代码生成方式完全是两套体系。AC6 的编译速度快、代码密度高,这是它的优势;但它对未定义行为的处理方式比 AC5"激进"得多,这一点恰恰是旧工程出问题的根源。

有个很形象的说法:AC5 像一位经验丰富但性格保守的老会计,账面上有模糊不清的地方,它宁可先按最保守的方式圈出来,不敢乱动;AC6 像一位效率极高的新人会计,遇到模糊不清的地方,会直接按自己认为最优的规则处理,如果账本身有歧义,它就会算出一本和你想的不太一样的账。

2.2 容易暴雷的三类老代码写法

结合我这几年帮别人排查的经验,从 AC5 切到 AC6 后频繁翻车的代码主要有三类。

第一类:未初始化就直接使用的局部变量。很多老工程师习惯在函数里先声明一个局部数组或变量,不初始化就直接用,因为 AC5 编译后栈上的残留数据往往恰好是 0,程序能凑合跑。AC6 对栈空间的复用更积极,局部变量可能落在之前被其他函数污染过的内存区域,导致随机值、越界、死循环甚至 HardFault。这类 bug 最坑的地方在于:同一个 hex,这次烧进去能跑,下次重新编译可能又不行,时好时坏。

第二类:中断服务函数和主循环共享的变量没加 volatile。这是嵌入式开发的老生常谈,但很多老工程确实没有做。在 AC5 的低优化等级下,编译器每次读变量都老老实实从内存取,碰巧掩盖了问题。AC6 在 -O1 以上优化等级下,如果编译器发现"这个变量在主循环里没有被写",它会把变量值直接缓存到寄存器里,这样中断里更新了变量,主循环读到的还是旧值。表现出来就是:程序没有死,但业务逻辑卡住了,板子看起来像完全没反应。

第三类:依赖未定义行为的写法。典型的有几种:有符号整型溢出、数组下标越界后碰巧访问到相邻合法地址、字符串结尾没补 '\0' 就调用 strlen、函数里用到了隐式类型转换。这些写法在 AC5 下大概率能跑出"正确"结果,但在 AC6 下编译器的优化会改变这些场景的最终效果。

2.3 为什么编译不报错,运行却出问题

这一点是很多人想不通的:代码有问题,为什么编译器不报错?

因为 C 标准把上面这些情况归为未定义行为(Undefined Behavior)。对于未定义行为,编译器不需要报错,也不保证任何行为。AC5 只是"碰巧"生成了一种符合你预期的机器码,AC6 按照自己的优化规则生成了另一种机器码。两种机器码都是合法的,但运行结果一个正常、一个不正常。

所以,当"烧录后不运行"发生在升级工具链之后,别急着怀疑硬件,先想想你的编译器版本是不是变了。我遇到过一个 LED 闪烁工程,代码里用了一个未初始化的局部变量做软件延时计数,而不是用for空循环。AC5 下编译运行正常,AC6 下变量初始值变成了一个很大的栈残留值,延时时间从几十毫秒变成了几秒钟,LED 很久才闪一下,客户以为程序没跑。

3. 从 HardFault 到源代码:我的定位过程

3.1 第一步:让调试器告诉你程序到底在哪

排查"烧录后不运行"最忌讳的就是对着代码瞎猜。正确的做法是接上 ST-Link 或 J-Link,让芯片跑起来,然后在调试模式下暂停,看一眼程序计数器 PC 停在哪里。

我当时的操作是:Keil 里进入 Debug 模式,全速运行一会儿后点击暂停。发现 PC 停在HardFault_Handler里,这说明程序其实已经跑起来了,但很快就进了硬件错误中断。代码里没有任何业务逻辑能跳到这里,显然是因为某条指令触发了总线错误、未对齐访问或非法指令。

下一步是查看 Fault Report。Keil 在 Debug 模式下可以通过 Peripherals > Core Peripherals > Fault Reports 打开故障报告窗口,重点看 CFSR(可配置故障状态寄存器)里的三个子段:MMFSR(存储管理故障)、BFSR(总线故障)、UFSR(用法故障)。我这个板子报的是 BFSR 里的 PRECISERR 位置位,说明发生了一次精确的 bus fault,能直接定位到出错地址和指令。

3.2 第二步:从反汇编和调用栈回溯源头

Keil 的 Fault Report 会给出出错地址 BFAR,这个地址指向访问失败的内存位置。但更有效的是看崩溃点对应的源代码。

打开 Disassembly 窗口,结合寄存器窗口里 LR(链接寄存器)的值,就能找到是在哪个函数、哪条指令上跑飞了。我当时看到的崩溃指令是在一个字符串解析函数的strlen调用附近,看起来像是因为局部缓冲区越界把后续数据覆盖了。

为了确认调用关系,我又在HardFault_Handler入口设置了一个断点,然后查看 Call Stack + Locals 窗口。这个方法非常实用:在 HardFault_Handler 里打断点,命中断点之后,调用栈窗口通常会保留进入 HardFault 之前已经在栈上的函数调用链。从调用链上能看出来是哪个业务函数在什么上下文中触发了问题。

3.3 第三步:用优化等级做 A/B 验证

到这一步,我已经能隐约感觉到是编译器优化的问题了,但还差一个决定性证据。最直接的方法就是切换优化等级做 A/B 测试。

打开 Options for Target > C/C++ (AC6) 选项卡,把 Optimization 从默认的-O1改成-O0,重新编译烧录,LED 立刻恢复正常闪烁。然后把优化等级调回-O1,问题复现。如此反复两次,基本可以锁定:这个"不运行"不是硬件问题,而是 AC6 优化导致的代码行为改变。

这里要补充一个操作细节:切换优化等级后,一定要用 Rebuild all targets 全部重新编译,不能只点 Build。因为 AC5 和 AC6、不同优化等级生成的中间文件格式不一样,Keil 有时不会自动清理旧的 .o 文件,混合编译会产生更诡异的问题。清一次工程中间文件再全量编译,是最稳妥的。

4. 收拾烂摊子:三种解决方案怎么选

4.1 方法一:切回 AC5,成本最低但要注意版本

如果你的项目是老代码、老逻辑,而且没有立刻迁移到 AC6 的计划,那切回 AC5 是性价比最高的方案。

在 Keil 里打开 Options for Target > Target 选项卡,找到 ARM Compiler 下拉框,选择 "Use default compiler version 5",然后 Rebuild all targets。老版本 Keil(MDK 5.36 及更早)默认内置了 AC5,切换很简单。

但有一点要提醒:MDK 5.37 之后的某些版本,默认安装包里已经不再自带 ARM Compiler 5,你打开下拉列表可能只看到 "Use default compiler version 6" 和一个灰色的 V5 选项。这种情况下需要单独下载安装 ARM Compiler 5.06 Update 7(build 960)的离线补丁包。装完后回到 Keil 里刷新一下,下拉框就会出现 V5 的选项。

Ulink 和 ST-Link 的调试器设置不需要动,切换的是编译器本身,对下载算法没有影响。不过切换后同样建议全量 Rebuild 一次。

4.2 方法二:用 AC6 修正代码,长期更健康

如果你的工程还需要长期维护,或者已经开始用 HAL 库、新中间件,那直接切换到 AC6 并把代码里的隐患清掉,才是更健康的方向。

需要做的关键修正包括:

  • 所有中断和主循环共享的标志位、状态变量,一律加上volatile修饰。
  • 局部变量声明时就初始化,尤其是数组、结构体、指针。
  • 用memset或显式赋值把缓冲区清空,字符串操作前保证'\0'结束符存在。
  • 关闭编译器对严格别名的默认假设,在 Options > C/C++ (AC6) > Misc Controls 里加上-fno-strict-aliasing。这个选项能降低部分指针相关优化带来的风险。
  • 开发阶段可以把 Optimization 临时设为-O0或-Og,保证调试体验,发布时再调回-O1、-O2验证一轮。

改代码的过程中建议按模块推进,不要一次性大改。比如先修中断相关的 volatile,编译烧录确认没问题,再修其他部分,这样如果引入了新问题,能快速定位到刚改的那一块。

4.3 方法三:单个函数做局部降级

如果你的代码很小,只有个别函数在 AC6 高优化下行为异常,又暂时不想动代码逻辑,可以用编译器属性给单个函数关优化。

在函数定义前加上:

__attribute__((optimize("O0"))) void some_problem_function(void) { // 原有逻辑 }

这样只有这个函数以 O0 优化等级编译,其他文件保持全局优化等级。这个方法适合用来临时止血,但不建议大量使用,因为 Keil 对 optimize 属性的支持不如 GCC 那么完善,而且治标不治本。长期来说,把代码本身修正确才是正路。

5. 还有哪些"烧录后不运行"的经典元凶

编译器优化是升级之后的重点怀疑对象,但不是唯一原因。为了让你排查时能一口气走完,我把其他几种常见原因也整理成一份快速对照表。

原因典型表现确认方式处理办法
Reset and Run 未勾选下载后程序停在复位状态,手动按复位才运行Options > Utilities > Settings > Flash Download 页面勾选 "Reset and Run"
Flash 编程算法选错下载提示成功但程序完全不运行,或运行到一半丢失读回 Flash 内容与 hex 对比在 Flash Download 中选择匹配芯片型号的 FLM 算法
BOOT0 引脚电平不对上电后程序区完全不执行,但调试能连上万用表测量 BOOT0 电压确保 BOOT0 拉低,从主 Flash 启动
ST-Link 固件被新版自动升级升级后下载异常或下载后不复位在 ST-Link Utility 中查看固件版本更新 Keil 或回退 ST-Link 固件
分散加载文件地址错乱工程带 Bootloader 时 APP 段地址和链接地址不一致查看 Build Output 里的 sct 文件内容在 Target 选项卡里正确设置 IROM1 起始地址和大小
复位电路异常上电瞬间 NRST 毛刺导致芯片复位挂在循环里示波器抓上电时 NRST 波形检查复位电容电阻参数,必要时换复位芯片

这里特别说一下第一项,因为很多新手会卡在这上面。Keil 的 Flash Download 设置里有一个 "Reset and Run" 选项,如果没勾选,程序下载完会停留在复位状态,需要手动按一下复位键才运行。对于量产和现场维护场景,这几乎是灾难性的体验。升级 Keil 后这个选项有时会恢复到默认状态,一定要检查。

另外,验证 Flash 内容不一定非要用 ST-Link Utility。你手头有 J-Flash 就用 J-Flash,有 OpenOCD 就用 OpenOCD,甚至 STM32CubeProgrammer 也行。工具虽然不同,但核心理念一样:程序编译下载成功后,从芯片里读出来的内容必须和 hex 文件一致。如果一致,问题在运行阶段;如果不一致,问题在烧录阶段。这一条判断直接决定了你在哪半边排查。

6. 写在最后:一次升级引发的经验沉淀

这次排查大概花了我半天时间,其中一小半浪费在怀疑硬件上。回过头看,如果我第一时间就检查 Keil 的编译器版本和优化等级,十分钟就能定位问题。

经历过这次之后,我自己形成了一个习惯:任何工具链升级,都不要直接拿正在交付的工程开刀。先装好新版本,新建一个最简单的 LED 闪烁工程编译烧录一遍,确认环境没问题后,再打开旧工程查看编译器的默认配置。如果编译器默认选项变了,先在工程里显式锁定原有的编译器版本和优化等级,再做后续操作。版本管理工具在这种时候尤其重要,升级前一个 commit,出了问题随时能回退。

还有一个小技巧:如果遇到"烧录后不运行",按"硬件 → 烧录 → 运行"三个步骤走。硬件看供电、复位、BOOT0,烧录看算法、校验、Flash 内容,运行看 PC 指针、Fault 寄存器、调用栈。绝大多数问题都能在这三步里现出原形。

这套排查方法我后来又用过两次,一次是一块 GD32 板子,一次是一块 AT32 板子,症状一模一样——换新版本 Keil 后烧录不跑,最后都指向 AC6 优化。如果你也换了新版本之后遇到类似的诡异现象,别急着换板子,先从编译器版本和优化等级查起,大概率能帮你省下半天时间。

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

基于Python与YOLOv5的路面桥梁裂缝检测识别实战

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

作者头像 李华
网站建设 2026/9/28 1:56:35

Servlet+JSP+JDBC房屋租赁管理系统开发实战:从设计到部署

简介:这是一套基于ServletJspJDBC的房屋租赁管理系统完整毕业设计资料包,面向计算机相关专业正在准备毕设的学生,以及需要JavaWeb实战练习的开发者。系统功能覆盖前台会员登录、免费注册、忘记密码、信息检索、留言板和公告浏览,以…

作者头像 李华
网站建设 2026/9/28 1:55:40

BMS中AFE同步采样与分片上报的工程本质

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

作者头像 李华
网站建设 2026/9/28 1:53:21

CODESYS安装与汉化新手指南:适配国产PLC的工业自动化入门路径

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

作者头像 李华
网站建设 2026/9/28 1:53:01

高速避障无人机配置指南:Livox MID360与FAST-LIO2实战

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

作者头像 李华
网站建设 2026/9/28 1:52:25

Windows下RP2040 PICO SDK开发环境搭建与VSCode配置指南

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

作者头像 李华