简介:ARM Compiler Version 5(ARMCC)是一款面向ARM嵌入式开发的编译器工具链,专为Cortex-A/R/M系列处理器设计,覆盖代码编译、链接、汇编与调试全流程。压缩包共650个文件,体积约56.83MB,核心文件以b、l、h、cc等格式存储,包含编译二进制、库文件、头文件与示例源码,并有exe可执行程序和map链接映射文件,便于环境验证与结果对照。资源已有4260人学习下载。该版本强调O0-O3分级优化、ELF链接编辑、调试信息生成、CMSIS标准接口支持以及代码压缩能力,可帮助开发者深入理解ARMCC的使用逻辑,并在裸机程序、RTOS移植或低功耗物联网项目中快速落地。同时,它还内置浮点运算选择、多线程安全函数库和全面的错误警告机制,便于适配不同硬件场景并提升代码质量。从文件构成看,目录相对完整,适合嵌入式初学者建立工具链概念,也适合有经验者排查编译与链接问题。
1. ARMCC 退役但不淘汰:存量项目里绕不开的编译器
Cortex-M4 上跑了两年的裸机工程,某天把编译器从 ARMCC 换成 armclang,编译输出一屏警告,链接后 Flash 直接超了 8%;退回 ARMCC 重新编译,一切照旧。这不是个例——ARMCC 是 ARM Compiler 5 的 C/C++ 编译器,产物是 ELF/DWARF 格式,曾作为 Keil MDK 的默认工具链长期存在,覆盖 Cortex-A/R/M 全系列,在裸机、RTOS、低功耗场景里产出稳定,缺点是停止更新、C++ 标准停留在十年前。很多开发者纠结要不要迁移,又怕迁移过程中把线上固件改坏。这套资源把 ARMCC 的选型边界、核心编译选项、分散加载和五个高频坑一次说清,帮你在“坚守老工具”和“迁移新工具链”之间做出有依据的判断。适合维护存量工程、做裸机开发、评估工具链切换的开发者和团队。
2. ARMCC与AC6、GCC的三条产品线:选型前先分清“保守稳定、LLVM新代码、免费通用”
很多人把 ARMCC 当成“ARM 的编译器”统称,实际上 ARM 官方工具链分两个代际,再加上开源 GCC,三条产品线并存。选错工具链是后面所有编译、链接问题的总根源,所以先花一章把产品线、语法差异和适用边界理清。这一章的信息量比较大,但都是可以直接对照自己工程判断的硬标准。
2.1 三者关系:编译器前端、工具链代际与产物差异
ARMCC 是 ARM Compiler 5 的核心编译器,前端采用自家维护的 C/C++ 解析实现(基于 EDG 授权),输出标准 ELF/DWARF 目标文件,在 Cortex-M 系列上打磨多年,代码质量和调试体验经过了大量量产项目验证。armclang 则是 ARM Compiler 6 的编译器,底层是 LLVM/Clang 架构,从 ARM Compiler 5.06 开始与 ARMCC 以双编译器形式共存于同一个工具链目录下。arm-none-eabi-gcc 是第三条独立分支,由开源社区维护,免费、更新频繁,被大量 Linux 生态和第三方 IDE 采用。
三者最终都生成 ELF 文件,但目标文件里的 section 命名、调试信息布局、内置宏定义并不互通。最直接的差异体现在语法层面:GCC 和 AC6 里写__attribute__((aligned(8))),ARMCC 里写__align(8);GCC 的__attribute__((packed)),ARMCC 里写__packed。代码在三个工具链之间拷贝,前几十行就会开始改写法。ARMCC 还提供了一批私人前缀关键字,包括__irq、__svc、__weak、__used、__inline,这些在 AC6 里大多变成__attribute__((...))风格,在 GCC 里写法又有差异。内联汇编的差异更明显,这个放到第 4 章单独展开。
选型的核心判断不是“哪个新用哪个”,而是“你的工程代码长在哪个语法生态里”。一个从 RVDS 时代迁移过来的老工程,代码里全是__packed和__align,强行换到 AC6 会经历一场大规模语法改写,测试回归成本远高于工具链本身带来的收益。
2.2 ARMCC的适用边界:Cortex-M裸机与MDK存量工程
判断自己的工程是否适合继续用 ARMCC,我一般看三条硬条件。第一,目标核是不是 Cortex-M 或 Cortex-R 系列,ARMCC 对这两个系列的代码生成最成熟,对 Cortex-A 的支持从 ARMv7-A 开始就明显跟不上 Linux 生态需求;第二,工程是不是从 Keil MDK 或旧版 RVDS 迁移来的,这类工程通常已经继承了大量 ARMCC 风格语法;第三,代码里有没有大量__packed、__align、嵌入式汇编这类 ARMCC 特色写法,出现频率高就意味着迁移成本高。
三条里占两条以上,建议至少在当前维护周期内继续使用 ARMCC,不要赶时髦换编译器。反过来,以下情况不适合继续用 ARMCC:目标是 Cortex-M23/M33 这类带 TrustZone 的 ARMv8-M 新核,ARMCC 5 能识别芯片型号,但针对新架构的代码生成和调试体验明显不如 AC6;要编译 C++17 或要开 LTO(链接时代码优化),ARMCC 的 C++ 前端主体停留在 C++98/03,只支持部分 C++11 特性,LTO 支持也很有限;工程需要长期跟随开源生态更新,越早切到 GCC 或 AC6 越好。
ARMCC 到今天仍值得下载,不是因为新,而是因为行为可预期。它在 Cortex-M 上的优化策略经过十几年磨合,几乎没有“编译器自作主张改掉你代码语义”的意外。对医疗、工控、车载这类要做长期稳定维护的产品,一个能预测行为的旧编译器,比一个每天都在进步的新编译器更让人放心。
2.3 从ARMCC切到AC6的成本评估:先量化再动手
如果最终决定迁移到 AC6,不要拍脑袋直接打开 MDK 的编译器下拉框切换。我建议先做一次量化评估,把迁移工作量拆成可统计的项,再决定一次切还是分步切。
下表列出我对存量工程做迁移评估时必查的五项,你可以直接拿这张表过一遍自己代码:
| 检查项 | ARMCC 风格写法 | AC6 常见写法 | 风险等级 |
|---|---|---|---|
| 内联汇编 | __asm { MRS r0, PRIMASK } | __asm volatile("MRS %0, PRIMASK" : "=r"(x)) | 高 |
| 结构体对齐 | __align(8) struct xxx | struct __attribute__((aligned(8))) xxx | 中 |
| 压缩结构体 | __packed struct xxx | struct __attribute__((packed)) xxx | 中 |
| 变量绝对定位 | __attribute__((at(0x20000000))) | __attribute__((section(".ARM.__at_0x20000000"))) | 中 |
| 分散加载文件 | LOAD_REGION ... +FIRST | 仍是 scatter 语法,但+Last改.ANY | 低 |
量化评估方法很简单:先切到 AC6 做一次全量编译,把错误数量、警告数量、每个文件的报错行数导出来,按上表分类统计。如果报错集中在某几个文件,说明迁移影响面可控,可以分文件逐步改;如果报错遍布整个工程,建议推迟迁移,或者干脆维持 ARMCC。
分步迁移的常见做法是保持 ARMCC 编译整个工程,把需要新开发或大改的模块单独用 AC6 编译成静态库,再通过链接器混用。注意两个工具链的目标文件都是 ELF 格式,混合链接通常没问题,但调试信息格式和宏定义不同,跨模块调试时可能看到不一致的变量布局,这点要在迁移计划里提前告知团队。
3. ARMCC编译选项与分散加载:六个必须理解的关键参数
ARMCC 的命令行参数风格和 GCC 不太一样,用的是双短横线加小写单词,比如--cpu、--apcs、--split_sections。很多人拿 GCC 的参数习惯硬套,结果选项被静默忽略或者生效了一部分,编译成功了但行为不是自己想要的。这章把最常配错的六个参数拆开讲:前置的--cpu和--apcs,影响代码尺寸的--split_sections,优化等级与浮点模式--fpmode,以及链接阶段的分散加载配置。
3.1 --cpu、--apcs与--split_sections:三条最常配错的选项
--cpu指定目标内核,这个参数直接决定编译器生成哪套指令集。Cortex-M4 要区分有没有 FPU:带硬件浮点写--cpu Cortex-M4.fp,不带 FPU 写--cpu Cortex-M4.no_fp,不写后缀时 ARMCC 默认按有 FPU 处理。CPU 型号选错,编译器可能生成目标芯片不支持的指令,程序一运行到对应代码就触发 UsageFault。选型依据不是看 MDK 界面里的芯片型号,而是看芯片数据手册里 Core 段落写的处理器型号,对照填写。
命令行编译时参数写法如下:
armcc --cpu Cortex-M4.fp --apcs=interwork --split_sections -O2 \ --fpmode=fast -I./include -c main.c -o main.o--apcs=interwork是告诉编译器代码里可能出现 ARM 和 Thumb 指令混合调用。对纯 Cortex-M 系列来说,所有代码都是 Thumb/Thumb-2,这个选项不影响最终行为,但从旧 ARM 平台迁移过来的代码不要省,因为老代码里可能残留了__irq中断入口函数,没有 interwork 标志时链接器可能生成错误的调用指令。--apcs=ropi和--apcs=rwpi是位置无关代码选项,分别对应只读数据和读写数据位置无关,加了之后生成的代码可在不同加载地址驻留,适合做 Bootloader 跳转和 OTA 双区升级,但要确认配套的分散加载文件正确设置了执行区基地址,否则跳转后照样跑飞。
--split_sections把每个函数放进独立的 ELF section,这样链接器可以回收没有被任何代码引用的函数,对减少最终固件尺寸非常有帮助。但它不是免费的,具体代价放在第 4 章第一个坑里细说,这里先记住:默认不开启,只有当你确定工程里有大量未被调用的函数且需要压缩 Flash 占用时才打开。
3.2 优化等级与--fpmode:代码尺寸和浮点一致性怎么平衡
优化等级六个档位,-O0生成最直白的代码,局部变量尽量保留在内存里,方便调试器观察;-O1做基础优化,调试体验仍可接受;-O2是多数量产裸机工程的首选,平衡代码尺寸和执行速度;-O3偏向速度,会做更多循环展开和自动内联,代码尺寸明显增大;-Os以尺寸为首要目标,在-O2基础上关闭部分膨胀优化;-Oz更激进,进一步挤压代码体积,但执行速度可能下降。-Ofast允许超出 C 标准约束的浮点变换,不要给需要确定性行为的控制类固件用。
浮点相关的一套--fpmode参数经常被忽略。Cortex-M4 带 FPU 时,ARMCC 默认浮点行为是--fpmode=ieee,即严格按 IEEE 754 规则做中间结果舍入,程序行为确定但性能略低;--fpmode=fast会允许编译器和硬件跳过部分舍入和异常检查,运算吞吐更高。如果你的工程做电机控制、传感器融合这类对实时性敏感但对最后一位精度不敏感的算法,--fpmode=fast是值得开的;如果做计量类产品,涉及小数比较和常量表匹配,老老实实用默认模式,否则不同编译参数下同样的输入可能产出不同结果,这个坑非常隐蔽。
完整编译命令可以组合以上参数:
armcc --cpu Cortex-M4.no_fp --apcs=interwork --split_sections \ -Os --fpmode=fast -DUSE_ARMCC -I./src -I./libs \ main.c uart.c timer.c -o app.o参数说明:-DUSE_ARMCC在代码里可配合#ifdef USE_ARMCC做平台差异处理;-I指定头文件搜索路径;-o app.o说明这里只是单次编译,实际工程建议每个 C 文件分别编译产出独立的目标文件,再统一交给链接器,这样修改单个文件时只需重编该文件,能明显加快迭代速度。
3.3 分散加载文件与MDK窗口:链接参数最终要落到这两种形式里
ARMCC 的链接阶段靠分散加载文件(scatter file)描述代码和数据在 Flash 与 RAM 中的排布。一个典型的 ARMCC scatter 文件长这样:
LOAD_REGION 0x08000000 0x00020000 { EXEC_REGION 0x08000000 0x00020000 { startup.o (RESET, +FIRST) * (+RO) } RAM_REGION 0x20000000 0x00010000 { * (+RW, +ZI) } }字段说明:LOAD_REGION定义加载区,即固件烧录到 Flash 里的起始地址和最大长度,这里是0x08000000共 128KB;EXEC_REGION定义执行区,即程序实际运行时的地址范围,裸机场景下加载区就是执行区,两者地址相同;startup.o (RESET, +FIRST)表示启动文件里的 RESET 段必须放在整个执行区的最前头,因为 Cortex-M 上电后从0x08000000地址读初始堆栈指针,再从偏移 4 处读复位向量,顺序错一个字节芯片都起不来;* (+RO)把所有剩余只读段(代码和常量)按顺序排列;RAM 区里(+RW, +ZI)覆盖所有可读写数据和需要清零的变量段。
在 MDK 图形界面下,大多数情况下你不会直接编辑这个文件。MDK 的“Options for Target → Linker”窗口里,如果勾选了“Use Memory Layout from Target Dialog”,IDE 会依据 Target 标签页里填的 ROM/RAM 起始地址和大小自动生成一份 scatter。很多人在这里翻车:改了 Target 窗口里的 Flash 地址,但 scatter 用的是 IDE 自动生成,两个地址不一致时链接产物地址和烧录地址对不上,程序下载后直接跑飞。所以改地址时要么全部用图形窗口,要么全部手编 scatter 文件,两条路二选一,不要混用。
需要自定义链接布局时,最常见做法是取消“Use Memory Layout”勾选,指定一个自己维护的 scatter 文件。比如要把校验码放在 Flash 末尾,就在加载区末尾追加一个只读区域并绝对定位:
FIXED_CRC 0x0801FFC0 0x40 { crc_data.o (+RO) }这个区域固定放在 128KB Flash 的最后 64 字节,配合crc_data.o里预先算好的校验数组,实现启动时校验固件完整性。注意这里的地址要和 Flash 大小严格对应,写错一个字节就可能覆盖掉应用程序代码。
4. ARMCC避坑记录:五个藏得深的编译链接问题
ARMCC 用得多的人,多少都经历过“编译通过、链接通过、上电跑飞”的玄学时刻。这五个问题是我在多个工程里反复踩过、也帮别人排过的高频坑,按“现象 → 原因 → 解决”的顺序写清楚,方便你直接对照排查。
4.1 坑一:开 --split_sections 后代码反而膨胀
现象:在 Cortex-M0 工程上为了减小固件尺寸开启--split_sections,编译后发现 Code 段不仅没变小,反而从 32KB 涨到 37KB,Flash 差点放不下。
原因:--split_sections把每个函数拆成独立 ELF section,链接器回收未引用函数的前提是拆得够细,但它也会阻止一些跨函数优化。Cortex-M0 的跳转距离有限,函数被拆散后,调用和被调用函数距离可能超出直接跳转范围,链接器被迫插入跳板指令;同时每个独立 section 都要维护自己的字面量池(常量数据),原来多个函数共享一份常量表的场景消失了,Flash 占用自然上升。ARMCC 的链接器在这类场景下不会像 GCC 的-ffunction-sections一样做二次重排,膨胀就出现了。
解决:先别全局开,改成对个别文件控制。ARMCC 支持--split_sections按编译单元分别启用,把包含大量死代码的文件单独加参数,其余文件保持默认;编译完成后打开链接器生成的 map 文件,搜索“Removing unused input section”,如果几乎搜不到删除记录,说明工程里本就没有多少死代码,这个参数对你的工程完全无效,直接关掉。
4.2 坑二:内联汇编语法与 GCC 不互通,移植必翻车
现象:从 GCC 或 AC6 工程拷贝一段内联汇编代码,例如asm volatile("MRS %0, PRIMASK" : "=r"(x));,粘到 ARMCC 工程里编译报错,提示汇编表达式语法错误或操作数不匹配。
原因:ARMCC 的嵌入式汇编器走的是自家语法,不支持 AT&T 或 LLVM 风格的操作数约束。它要求用__asm关键字,汇编语句按 ARM 汇编器的写法直接写寄存器名,不搞%0、=r这种占位符。两套语法表面上都在写汇编,底层规则完全不通,很多人在这上面耗掉一整天。
解决:ARMCC 下的标准写法是这样的:
__asm { MRS r0, PRIMASK MOV result, r0 }其中result是 C 变量,ARMCC 会自动完成寄存器到变量的搬运。如果只是读取 PRIMASK、BASEPRI 这类特殊寄存器,更省事的做法是直接用 ARMCC 提供的 intrinsic 函数,比如__disable_irq()、__get_PRIMASK(),这些函数在core_cm4.h这类 CMSIS 头文件里已定义,编译器会生成最精简的指令,完全没有必要自己写内联汇编。工程里已经有大量内联汇编的话,迁移到 AC6 前先统计所有__asm出现位置,每处都要手工改写,这个工作量要提前算进迁移预算。
4.3 坑三:C++ 异常默认关闭,throw 直接 HardFault
现象:用 ARMCC 编译一个 C++ 工程,代码里有throw语句,编译和链接都正常通过,程序运行到throw执行处直接进 HardFault,调试器停在异常向量里。
原因:ARMCC 的 C++ 编译器默认不启用异常处理,throw被编入代码但对应的异常展开表(unwind table)没有生成。运行时遇到throw调用,库函数尝试查展开表得到空指针,随后访问非法地址触发 HardFault。这不是代码逻辑问题,是工具链默认配置问题。ARMCC 对 C++ 的异常支持需要显式加--exceptions编译选项,同时链接时要有配套的运行库。
解决:首先要决策——裸机固件里到底要不要用异常。我的建议是不要用,嵌入式环境里错误处理用返回错误码加断言,控制流更清晰,代码尺寸也更小。如果历史代码已经用了,至少在 ARMCC 命令里补上:
armcc --cpu Cortex-M4.fp --exceptions -O2 -I./src main.cpp -o main.o加了--exceptions后,Flash 占用会明显增加,因为异常展开表会占据额外空间。在 MDK 里这个参数填在“Options for Target → C/C++ → Misc Controls”输入框内,然后重新编译全部文件。改完后再跟踪一下有没有抛异常但没被捕获的路径,异常一旦逃逸出main边界,结果同样是 HardFault。
4.4 坑四:__align 与 8 字节对齐:结构体被压栈时“假对齐”
现象:结构体里有uint64_t成员,变量声明处用了__align(8),单步调试时访问成员正常,但把结构体指针传给 DMA 缓冲区时,偶发 HardFault。进一步跟踪发现,某些函数调用链上编译器生成的ldrd(双字加载)指令要求 8 字节对齐地址,实际地址是 4 字节对齐的。
原因:ARMCC 对结构体对齐遵循自身 ABI 规则,__align(8)修饰能保证该变量定义处按 8 字节对齐,但无法保证所有访问路径都保持对齐状态。当结构体作为函数参数传递时,ARMCC 按 4 字节对齐压栈,栈上的新副本可能落在 4 字节对齐而非 8 字节对齐的位置;函数内再把栈地址指给 DMA 外设,硬件访问时按 8 字节读就会出错。
解决:不要让对齐只落在声明处。结构体定义时用#pragma pack(push, 8)和#pragma pack(pop)包起来,强制该类型的每个实例、每次拷贝都遵循 8 字节对齐;函数参数改成指针传递,避免结构体整体压栈;涉及 DMA 的缓冲区额外用__align(8)数组:
__align(8) uint8_t dma_buf[256];这个 256 字节数组按 8 字节对齐分配,传给 DMA 控制器时地址低三位恒为零。ARMCC 对普通变量的对齐处理在绝大多数场景下没问题,问题只出现在“定义处对齐了、使用处对齐信息丢失”这种组合里,排查时优先看指针传参和函数压栈路径。
4.5 坑五:microlib 和标准库混用,链接器报“__use_no_semihosting”重定义
现象:工程里某个模块用了标准 C 库的printf,另一部分代码引用了 microlib 的特性,链接时反复报__use_no_semihosting符号重定义错误,或者程序里用了fopen后运行到库函数内部就跳进 HardFault。
原因:microlib 是 ARMCC 自带的轻量 C 运行库,面向裸机环境设计,不对主机做任何假设;标准 C 库则是面向有操作系统的场景。两个库同时被拉进链接流程,会产生重复的底层符号定义,链接器无法决定用哪一套。printf这类函数在标准库实现里依赖半主机(semihosting)机制,没有调试器连接时它尝试访问调试通道,访问失败就触发 HardFault。microlib 之所以“装上就能用”,就是因为它把输出函数重定向到用户实现的fputc、sendchar这类函数上。
解决:明确整个工程只选一条路。如果用 microlib,在 MDK 的“Options for Target → Target → Code Generation”窗口勾选“Use MicroLIB”,编译产物里的库全部来自 microlib;如果坚持用标准库,就必须自己提供底层支撑,典型代码是:
#include <rt_sys.h> void _sys_exit(int return_code) { while (1); }这个空实现告诉标准库“程序不要退出”,同时你还需要实现fputc等底层字符输出函数,才能让printf不依赖半主机通道。两条路不能并行,工程里出现混合引用时优先查工程配置里的 MicroLIB 勾选项和是否有自行添加的启动文件覆盖了_sys_exit符号。
5. 用 ARMCC 的 #pragma Ospace/Otime 做函数级尺寸验证:一个 15 分钟自查流程
调整编译参数时最怕遇到黑匣子——改了某个选项,全局 Code 段变小了,但不知道是哪些函数发生变化,更不知道有没有函数反而膨胀。ARMCC 比 GCC 讨喜的一点是它提供了函数级优化控制方式,也就是#pragma Ospace和#pragma Otime,可以直接针对单个函数指定尺寸优先或时间优先的编译策略,而不必对整个文件或整个工程统一调整优化等级。
先要读懂 ARMCC 的 map 文件。编译链接完成后,打开.map文件找到 “Image component sizes” 段,里面是一张表,列出每个源文件在最终镜像里占用的 Code、RO Data、RW Data、ZI Data 大小。我习惯的做法是先把优化前的表格复制到一个文本文件,记为“基准尺寸”,然后做任何参数调整后,再次编译并对比这张表。只对比总 Code 大小是不够的,要按文件名逐行对比,才能定位出“这个参数让哪个文件变胖了、哪个文件变瘦了”。
#pragma Ospace的用法非常直接:
#pragma Ospace int clamp_value(int v, int min, int max) { if (v < min) return min; if (v > max) return max; return v; } #pragma Otime在函数定义前加#pragma Ospace,告诉 ARMCC 对这个函数按代码尺寸最小化编译;函数结束后立即接#pragma Otime,把后续代码恢复到默认的时间优先策略。对比验证流程:先用默认-O2编译一次,记下该函数的汇编长度;加上#pragma Ospace后重新编译,从 map 文件或反汇编里看同一个函数的字节数变化。我实际测过的事例里,一个包含多个边界判断的数值计算函数,在-O2下编译出 92 字节,加了 Ospace 后降到 68 字节,性能损失几乎可以忽略,因为它本来就不在热循环里。
验证更精细的可以这样操作:把函数单独放进一个 C 文件,用--split_sections编译,然后执行fromelf --text -z查看该函数对应 section 的大小,得到的是纯函数体长度,排除了其他代码的干扰。常用命令是:
fromelf -z --text app.axf对比优化前后的 section 大小,差异就是#pragma Ospace带来的真实收益。从那以后,我每次调整编译参数都强制走一遍这套流程:先记录基准尺寸,再改参数,重新编译,逐文件对比 map 表,最后才敢合入代码。哪怕只改了优化等级一位,也走这个流程,因为 ARMCC 的优化在不同函数间分配并不均匀,全局涨跌不代表每个文件都在涨。这套方法也帮我积累了一批按函数区分的优化记录,对日后评估迁移到 AC6 时哪些函数可能变差、哪些会变好很有参考价值,希望帮到你。
本文还有配套的精品资源,点击获取