ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

新版Keil烧录成功但程序不运行?AC6编译器优化是罪魁祸首 1. 现象复盘下载成功板子却像没烧一样1.1 客户描述里的关键线索前两天帮客户维护一块老板子主控是 STM32F103C8T6功能简单得很上电点两盏 LED跑一个串口上报逻辑。代码是两年前的旧工程之前一直跑得好好的。结果客户换了台新电脑重新装上最新版 Keil编译、下载都提示成功可一断电重启板子就愣在那里一点反应没有。这个现象最迷惑人的地方在于烧录工具明确显示 Flash Download Successful点调试也能连上芯片但程序就是不跑。如果你也遇到过类似的情况大概率第一反应是怀疑硬件坏了、晶振没起振、BOOT0 配置不对甚至怀疑芯片被锁死。我当时也把这一整套全查了一遍最后才发现问题根本不是硬件而是出在 Keil 新版本的工具链上。这篇文章我会把完整的排查过程写出来重点讲一个特别隐蔽、但近几年越来越常见的坑——新版 Keil 的默认编译器从 AC5 换成了 AC6旧代码在 AC6 的优化下会出现编译能过、运行就跑飞的情况。同时也会给出详细的解决方法和一份烧录后不运行的通用排查清单适合正在被 Keil 升级折腾的工程师也适合刚入门 STM32 的开发者提前避坑。1.2 烧录阶段的检查清单先排除掉低级错误在怀疑编译器之前我还是按老规矩把基础问题先过了一遍。这个步骤很重要不要跳过因为烧录后不运行有相当一部分是低级错误导致的排查顺序错了会浪费时间。供电检查万用表量 VCC 和 GND3.3V 正常芯片表面没有异常发热。复位电路NRST 引脚电压 3.3V没有一直被拉低手动按复位键现象依旧。BOOT0/BOOT1BOOT0 为低电平确认是从主 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。老版本 KeilMDK 5.36 及更早默认内置了 AC5切换很简单。但有一点要提醒MDK 5.37 之后的某些版本默认安装包里已经不再自带 ARM Compiler 5你打开下拉列表可能只看到 Use default compiler version 6 和一个灰色的 V5 选项。这种情况下需要单独下载安装 ARM Compiler 5.06 Update 7build 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 RunFlash 编程算法选错下载提示成功但程序完全不运行或运行到一半丢失读回 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 优化。如果你也换了新版本之后遇到类似的诡异现象别急着换板子先从编译器版本和优化等级查起大概率能帮你省下半天时间。
返回列表