ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Keil MDK 迁移 Arm Compiler 6.20.1 实战:从 ArmCC 5 到 ArmClang 6 的避坑指南

Keil MDK 迁移 Arm Compiler 6.20.1 实战:从 ArmCC 5 到 ArmClang 6 的避坑指南

简介:这份资源是面向嵌入式开发者的 Arm Compiler for Embedded 6.20.1 独立安装包,主要服务于使用 Keil MDK 进行 STM32 等 ARM 芯片开发的工程师与学习者。当 Keil 自带编译器版本不满足项目需求,或需要特定版本编译器完成工程构建时,可通过该安装包在 Keil 环境中补充安装 Arm Compiler 6.20.1,解决编译工具链版本不匹配的问题。压缩包共 7 个文件,以 5 个 txt 许可与说明文档、1 个 msi 安装程序及 1 个 html 发布说明为主,整体约 303.83MB,安装程序用于部署编译器本体,文本与网页文件则提供版本发布信息、许可条款及第三方组件说明,便于安装前核对环境与授权要求。目前已有 405 人学习下载,适合需要为 Keil 配置 Arm Compiler 6.20.1 的开发者参考使用,可帮助快速获取官方安装介质与配套说明,减少版本查找与兼容性确认的时间成本。

1. 从一次编译报错说起:Arm Compiler 6.20.1 在 Keil 里到底换了什么

如果你最近把 Keil MDK 升到了带 Arm Compiler for Embedded 6.20.1 的版本,然后打开一个跑了三五年的老工程直接点 Build,大概率会收获一屏红色报错。这不是你代码写坏了,而是编译器内核从 ArmCC 5 换成了基于 LLVM/Clang 的 ArmClang 6,两代工具链在语法容忍度、链接脚本格式、启动文件写法上几乎是两套体系。Arm Compiler for Embedded 6.20.1 是 Arm 官方面向嵌入式 Cortex-M 系列的编译器发行版,集成进 Keil MDK 后,你可以在 Manage Project Items 或 Options for Target 里把它设为默认工具链。它带来的好处很实在:更小的代码体积、更严格的 C 标准符合性、对 Cortex-M55/M85 等新核的支持,以及更清晰的诊断信息。代价是迁移成本——老工程的汇编启动文件、分散加载文件、内联汇编、__attribute__用法都得过一遍。这篇笔记面向正在做或准备做这次迁移的嵌入式工程师,把选型理由、迁移步骤、参数配置和踩坑记录一次讲透。

2. 为什么值得从 ArmCC 5 迁到 ArmClang 6:选型与代价

2.1 两代编译器在内核与优化上的本质差异

ArmCC 5 是 Arm 自研的闭源编译器,前端语法宽松,很多不符合标准的写法它都默默接受。ArmClang 6 基于 LLVM/Clang,前端严格按 C99/C11/C17 标准解析,后端用 LLVM 的优化管线。这个差异直接导致几个后果:一是隐式函数声明、隐式类型转换这类老代码里的“灰色写法”会直接报错或警告升级;二是优化策略不同,同样的-O2,ArmClang 可能做出和 ArmCC 完全不同的内联与循环展开决策,导致某些依赖时序的裸机代码行为变化;三是 ArmClang 对volatile、内存屏障的处理更严格,这对驱动和外设寄存器操作是好事,但会暴露老代码里本来就存在的同步缺陷。

从选型角度看,如果你的芯片是 Cortex-M0/M3/M4,ArmCC 5 还能用,但 Arm 已经停止对它做新特性支持。如果你的目标是 Cortex-M7 以上的核,或者要用 Helium(MVE)指令、TrustZone-M,那 ArmClang 6 基本是唯一选择。6.20.1 这个版本在诊断信息可读性和 LTO(链接时优化)稳定性上比早期 6.x 有明显改善,这是我建议至少升到这个版本而不是停在 6.14 之类的原因。

2.2 迁移前必须盘点的四类工程资产

动手之前先做一次资产盘点,能省掉后面大量返工。第一类是启动文件,startup_xxx.s如果是 ArmCC 5 格式的汇编,几乎一定要换成 ArmClang 格式的版本,两者伪指令和段声明语法不同。第二类是分散加载文件.sct,ArmClang 6 支持新的.scf语法,虽然兼容旧.sct,但某些+RO、+ZI的写法在新链接器下语义有细微差别。第三类是内联汇编,ArmCC 5 用__asm { }块,ArmClang 6 用__asm volatile("...")的 GCC 风格,这是迁移里最费时的一块。第四类是各种#pragma,比如#pragma arm section、#pragma import,这些在 ArmClang 下要么换写法要么直接不支持。

盘点完这四类,你就能估算工作量。一个中等规模、外设驱动自己写的工程,通常需要一到三天完成迁移和验证;如果工程里大量用了第三方库且只有 ArmCC 5 的.lib,那要么找源码重编,要么放弃迁移。

2.3 在 Keil 里切换工具链的最小操作

在 Keil MDK 里切换工具链本身很简单,但顺序不能错。先确认你安装的 MDK 版本已经带了 Arm Compiler 6.20.1,可以在 Keil 安装目录下的ARM/ARMCLANG/bin里看到armclang.exe。然后在 Options for Target 的 Target 页,把 ARM Compiler 下拉框从 “Use default compiler version 5” 改成 “Use default compiler version 6” 或指定 6.20.1。

# 在 Keil 安装目录确认 ArmClang 6.20.1 是否存在 ls ARM/ARMCLANG/bin/armclang.exe # 查看版本,确认是 6.20.1 ARM/ARMCLANG/bin/armclang --version

第一条命令确认可执行文件存在,第二条打印版本号。如果--version输出里显示的是 6.20.1,说明工具链就位。注意不要手动去改工程文件里的工具链路径,Keil 的工程管理会自己处理,手动改容易导致下次打开工程时配置错乱。切换后先别急着全量编译,先编译一个最小的、只含 main 和启动文件的工程,确认工具链本身能跑通,再逐步把外设驱动加回来。

3. 把老工程迁到 ArmClang 6.20.1:启动文件、分散加载与内联汇编

3.1 启动文件从 ArmCC 5 格式换成 ArmClang 格式

ArmCC 5 的启动文件里常见AREA RESET, DATA, READONLY、EXPORT __Vectors、DCD这些写法,ArmClang 6 的启动文件改用.section、.global、.word这类 GNU 风格伪指令。最省事的做法是直接用芯片厂商提供的、面向 ArmClang 的启动文件模板,而不是手工逐行改。以 Cortex-M4 为例,一个 ArmClang 风格的启动文件片段如下:

.section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler ; ... 其余中断向量

.section .isr_vector,"a",%progbits声明一个可分配、可执行的向量表段,%progbits表示段内包含数据。.word等价于 ArmCC 5 的DCD,每个中断向量占一个字。_estack是链接器符号,指向栈顶,必须在分散加载文件里定义。这里的关键是段名.isr_vector要和分散加载文件里的向量表放置位置对应,否则链接器会把向量表放到错误地址,芯片上电就跑飞。

3.2 分散加载文件从 .sct 到 .scf 的写法对照

ArmClang 6.20.1 的链接器仍然能读旧.sct,但新工程建议用.scf。两者核心概念一样:定义加载域和运行域,把代码、只读数据、读写数据、零初始化数据分配到不同内存区域。下面是一个典型的.scf片段:

LR_IROM1 0x08000000 0x00100000 { ; 加载域,起始 0x08000000,大小 1MB ER_IROM1 0x08000000 0x00100000 { ; 执行域,代码和只读数据 *.o (RESET, +First) ; 向量表放最前 *(InRoot$$Sections) ; 库的根段 .ANY (+RO) ; 其余只读内容 } RW_IRAM1 0x20000000 0x00020000 { ; 读写数据域,128KB SRAM .ANY (+RW +ZI) ; 读写数据和零初始化 } }

LR_IROM1是加载域,描述烧录到 Flash 的镜像布局。ER_IROM1是执行域,*.o (RESET, +First)强制把含 RESET 段的向量表放在最前,这是上电取向量的前提。.ANY (+RO)让链接器自动摆放其余只读内容。RW_IRAM1里的.ANY (+RW +ZI)把已初始化变量和零初始化变量都放进 SRAM。迁移时最容易翻车的是+First漏写,导致向量表不在 0x08000000,芯片上电直接进 HardFault。

3.3 内联汇编从 __asm 块改写成 GCC 风格

ArmCC 5 的内联汇编是__asm { MOV R0, R1 }这种块写法,ArmClang 6 只认__asm volatile("mov r0, r1")的字符串形式,而且寄存器约束要用 GCC 的扩展语法。下面是一个读 PRIMASK 寄存器的例子:

// ArmClang 6 风格:读取 PRIMASK 到变量 static inline uint32_t get_primask(void) { uint32_t result; __asm volatile ("mrs %0, primask" : "=r" (result)); return result; }

__asm volatile告诉编译器这段汇编有副作用、不能随意移动或删除。"mrs %0, primask"是汇编模板,%0是占位符,对应输出操作数。"=r" (result)表示用一个通用寄存器承载输出,结果写入result变量。迁移时要注意:ArmCC 5 里直接写寄存器名的写法在 ArmClang 下可能因为寄存器分配冲突而报错,必须用约束让编译器自己选寄存器。如果一段内联汇编改起来太麻烦,可以考虑用单独的.s文件写汇编函数,通过 AAPCS 调用约定和 C 交互,这样更清晰也更好维护。

4. 编译参数怎么设:优化等级、C 标准与诊断开关

4.1 优化等级的选择与裸机时序代码的注意点

ArmClang 6.20.1 支持-O0到-O3、-Os、-Oz。-O0不优化,调试友好但代码大;-O1做基本优化;-O2是发布常用档;-O3激进优化,可能大幅展开循环;-Os优先体积;-Oz比-Os更激进地压体积。对裸机工程,我的经验是:调试阶段用-O0或-O1,发布用-Os,只有在确实需要吞吐且验证充分时才上-O2/-O3。

要特别注意的是,-O2以上时,编译器可能把一段用于延时的空循环优化掉,或者把对同一外设寄存器的连续写合并。如果你的代码依赖指令周期做精确延时,要么用__NOP()配合volatile,要么把延时函数放到单独的、用-O0编译的源文件里。在 Keil 里可以针对单个文件设置不同优化等级,右键文件 → Options for File,在 C/C++ 页单独指定。

4.2 C 标准与语言扩展的取舍

ArmClang 6.20.1 默认按-std=gnu11解析,也就是 C11 加 GNU 扩展。老工程如果用了 ArmCC 5 的私有扩展,比如__packed、__weak、__irq,在 ArmClang 下部分还能用(ArmClang 为兼容提供了这些关键字),但__irq这类已经不建议用,中断函数应该用普通的 C 函数加正确的向量表登记。如果你想把代码写得更可移植,可以显式指定-std=c11,但这样会关掉 GNU 扩展,某些依赖typeof、语句表达式的代码会编译失败。

在 Keil 的 C/C++ 页,Misc Controls 里可以填-std=c11或-std=gnu11。我一般建议新代码用-std=c11,老代码迁移时先用默认的gnu11跑通,再逐步清理对 GNU 扩展的依赖。另外-fno-common在 ArmClang 6 里是默认行为,这意味着多个源文件里同名的未初始化全局变量会报重复定义,而 ArmCC 5 会默默合并。迁移时如果遇到multiply defined symbol,先检查是不是有全局变量定义在头文件里。

4.3 诊断开关与把警告当错误

ArmClang 6.20.1 的诊断信息比 ArmCC 5 清晰很多,每个警告都有编号,比如-Wunused-variable、-Wimplicit-function-declaration。迁移初期建议把警告都打开,用-Wall -Wextra,先看一遍有多少问题。如果工程太大,可以先用-Wno-xxx关掉某一类,集中处理另一类。

# 在 Keil Misc Controls 里可填的诊断相关参数示例 -Wall -Wextra -Wno-unused-parameter # 把隐式函数声明从警告升级为错误,强制修复 -Werror=implicit-function-declaration

-Wall -Wextra打开常用警告集,-Wno-unused-parameter关掉“未使用参数”这类噪音。-Werror=implicit-function-declaration把隐式函数声明直接变成错误,这在迁移时很有用,因为隐式声明往往意味着漏了头文件,运行时会因为参数类型不匹配出玄学问题。注意不要一上来就-Werror全开,否则老工程可能几千个错误,根本没法推进。分批次、按类别处理才是可持续的做法。

5. 迁移中最容易翻车的几个点:避坑与排查

5.1 现象:上电直接进 HardFault,向量表地址不对

原因通常是启动文件的段名和分散加载文件不匹配,或者.scf里漏了+First,导致向量表没有被放在 Flash 起始地址。解决方法是先用调试器看SCB->VTOR寄存器的值,确认它指向的地址和实际向量表位置一致。然后在.scf里显式写*.o (RESET, +First),并确认启动文件里向量表所在段名就是RESET。如果用的是厂商模板,检查模板里的段名和你的.scf是否一致,两边对不上是常见错误。

5.2 现象:链接报 undefined symbol,但函数明明定义了

这多半是 C/C++ 混编时的名字修饰问题。ArmClang 6 对 C++ 符号的修饰规则和 ArmCC 5 不同,如果头文件里没有用extern "C"包住 C 函数声明,C++ 文件调用 C 函数时链接器找不到符号。解决办法是在所有给 C++ 用的 C 头文件里加:

#ifdef __cplusplus extern "C" { #endif // 函数声明 #ifdef __cplusplus } #endif

__cplusplus宏只在 C++ 编译时定义,这样 C 编译时看到的是普通声明,C++ 编译时看到的是extern "C"声明,两边符号名一致。迁移时如果遇到一堆undefined symbol且名字看起来被“改写”过,先查这个。

5.3 现象:中断进不去或进去一次就死

ArmCC 5 时代,中断函数常用__irq关键字,编译器会自动生成保存/恢复现场和返回指令。ArmClang 6 不支持__irq,中断函数就是普通 C 函数,现场保存由硬件自动完成(Cortex-M 的异常机制),返回用普通的函数返回即可。如果你把老代码里的__irq直接删掉但没检查函数体,可能残留了手动清中断标志的错误顺序。正确做法是:中断函数用普通 C 函数定义,函数名和启动文件向量表里的名字一致,函数体内先清中断标志再处理数据。如果进不去,检查向量表里的函数名拼写;如果进去一次就死,检查是不是忘了清标志导致中断反复触发。

5.4 现象:代码体积突然变大或变小很多

从 ArmCC 5 换到 ArmClang 6,代码体积变化是正常的,但如果变化超过 20%,要查原因。变大常见于:-O0没改、调试信息没关、某些库函数被换成了更完整的实现。变小常见于:链接器做了更激进的死代码消除,或者 LTO 把一些你以为会保留的函数删了。如果用了 LTO(-flto),要确认所有参与链接的目标文件都支持 LTO,否则链接器会回退到非 LTO 模式并给警告。体积异常时,用fromelf --text -c -o output.txt your.axf生成反汇编,对比关键函数的实现,看是不是被优化掉了。

5.5 现象:浮点运算结果和以前不一致

ArmClang 6 默认遵循 IEEE 754,但优化等级高时可能启用快速数学(-ffast-math),这会改变浮点结合律和精度。如果你的工程依赖确定的浮点结果,检查 Misc Controls 里有没有被加上-ffast-math,有的话去掉。另外 Cortex-M4F/M7 的 FPU 有单精度和双精度之分,如果代码里用了double但芯片只有单精度 FPU,会走软件浮点,速度和精度都和硬件浮点不同。迁移时确认__FPU_PRESENT和__FPU_USED宏定义正确,并在启动代码里使能 FPU。

6. 用 fromelf 和 map 文件做迁移后的体积与性能验证

迁移完成后不能只看“能编译能跑”,得做量化验证。ArmClang 6.20.1 配套的fromelf工具可以分析 axf 文件的段大小和反汇编,链接器生成的.map文件能告诉你每个目标文件占了多少 Flash 和 RAM。我的习惯是迁移前后各存一份 map 文件,逐段对比。

# 生成段大小统计 fromelf --text -z -o size_report.txt your_project.axf # 生成反汇编,用于对比关键函数 fromelf --text -c -o disasm.txt your_project.axf

--text -z输出各段的压缩大小统计,-o指定输出文件。--text -c生成带 C 源码交织的反汇编(如果编译时带了-g)。对比时重点看三处:一是.text段总大小,确认没有异常膨胀;二是.data和.bss段,确认 RAM 占用没超;三是关键中断函数和延时函数的反汇编,确认指令数和预期一致。如果发现某个函数被内联导致体积暴涨,可以在该函数定义前加__attribute__((noinline))阻止内联。

性能验证方面,如果有条件,用 DWT 的 CYCCNT 计数器测关键路径的周期数,迁移前后各测一遍。没有硬件计数器的话,至少用 GPIO 翻转加示波器看执行时间。我一般会在迁移后跑一遍完整的功能测试用例,特别是通信协议、电机控制、ADC 采样这些对时序敏感的模块,确认行为一致。如果某个模块行为变了,先查优化等级,再查volatile是否漏加,最后查内联汇编改写是否正确。

迁移这件事,我的血泪经验是:不要试图一次性把所有警告都清零再编译,那样你会卡在几千个错误里出不来。正确节奏是先让工程能编译通过、能跑起来,再按模块逐个清理警告和验证行为。每改一个模块就提交一次,出问题能快速回退。另外,ArmClang 6.20.1 的文档里对每个诊断编号都有说明,遇到不认识的警告先去查编号,比盲目搜索高效得多。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进