简介:ARM Compiler Version 5(ARMCC)是ARM公司面向ARM架构处理器推出的高效编译工具链,适用于嵌入式开发、物联网、汽车电子与工业控制等场景,面向需要编写、编译和优化ARM平台软件的开发者。资源包共650个文件,约56.83MB,以rar压缩包形式提供,内含242个b文件与242个l文件(库与目标文件)、70个h头文件、24个cc源文件,以及exe可执行工具、s汇编文件、map与elmap映射文件,并附带C++标准库头文件如vector、iostream、algorithm、memory等,构成完整的编译、链接与调试环境。该工具链支持Cortex-A、Cortex-R、Cortex-M系列核心,提供O0至O3多级优化、ELF链接、调试信息生成、硬件浮点与软浮点支持、代码大小压缩及CMSIS标准接口,并具备线程安全函数库与实时中断处理能力。目前已有4270人学习下载,适合嵌入式开发者深入理解ARMCC的编译流程、优化策略与标准库组织方式,快速搭建本地开发与验证环境。
1. ARMCC 在 ARM Compiler 5 里的真实定位:为什么老项目还在用它
如果你手里有一份十年前的 MCU 工程,打开 Keil MDK 的 Options for Target,发现编译器路径指向ARMCC而不是AC6,那大概率就是 ARM Compiler 5 的现场。ARMCC 是 ARM Compiler 5 工具链里的 C/C++ 编译器可执行文件,配套的还有armasm、armlink、fromelf这几个老搭档。它和后来的 ARM Compiler 6(基于 Clang/LLVM)在命令行参数、内联汇编语法、链接脚本处理上都有明显差异,所以很多量产项目宁可锁死旧工具链也不愿意迁移。
这份资源能解决的核心问题很具体:当你需要维护、复现或者二次开发一个基于 ARM Compiler 5 的工程时,你得知道 ARMCC 怎么调、参数怎么给、和 AC6 的坑在哪。适合谁?一是接手了老代码库的嵌入式工程师,二是要在 CI 里跑 legacy 构建的 DevOps,三是想搞清楚--c99、-Ospace、--split_sections这些参数到底干了什么的人。下面按“先搞懂工具链结构,再动手编译,最后排坑”的顺序拆。
2. ARMCC 工具链拆解:从 armcc 到 armlink 的调用链
2.1 ARMCC 在工具链里的角色与文件构成
ARM Compiler 5 不是单个 exe,而是一组可执行文件的集合。核心成员包括:
| 可执行文件 | 作用 | 典型输入 | 典型输出 |
|---|---|---|---|
armcc | C/C++ 编译 | .c/.cpp | .o对象文件 |
armasm | 汇编编译 | .s | .o对象文件 |
armlink | 链接 | .o/.lib | .axf/.elf |
fromelf | 格式转换 | .axf | .bin/.hex/ 反汇编 |
armar | 静态库打包 | .o | .lib |
我一般会先确认工具链根目录,常见路径是C:\Keil_v5\ARM\ARMCC\bin或者独立安装的DS-5安装目录下的sw/ARMCompiler5。判断版本用:
armcc --version # 输出示例:ARM Compiler 5.06 update 7 (build 960)这里有个容易翻车的点:armcc --version和armcc -v输出不一样,前者给完整版本号,后者只给简短信息。写 CI 脚本做版本校验时用--version,别用-v,否则解析出来的字符串对不上。
工具链的调用链是:armcc把每个源文件编译成.o,armlink把所有.o和库按 scatter file 描述的地址布局链接成.axf,最后fromelf把.axf转成烧录用的.bin或.hex。理解这条链,后面调参数才知道改哪一环。
2.2 手动编译一个最小工程:命令行逐条拆解
假设有一个最小工程,文件结构如下:
project/ ├── main.c ├── startup.s └── scatter.sctmain.c内容:
#include <stdio.h> volatile int counter = 0; int main(void) { counter = 42; while (1) { counter++; } return 0; }第一步,编译 C 文件。ARMCC 的常用参数组合:
armcc --c99 -c --cpu Cortex-M4.fp \ -Ospace -g -W \ --split_sections \ -I./inc \ -o main.o main.c逐条说明:--c99启用 C99 标准,老工程里很多用了//注释和变长数组的代码必须加这个;-c表示只编译不链接;--cpu Cortex-M4.fp指定目标核和浮点单元,.fp后缀表示带 FPU,写错会导致浮点指令生成异常;-Ospace是优化等级,ARMCC 的优化选项和 GCC 不一样,-O0到-O3之外还有-Ospace(优先省空间)和-Otime(优先省时间),嵌入式里-Ospace用得最多;-g生成调试信息;-W打开警告;--split_sections让每个函数单独成段,配合链接器的--remove可以剔除未调用函数,这个参数在量产固件里几乎是必加项。
第二步,编译汇编启动文件:
armasm --cpu Cortex-M4.fp \ --pd "__MICROLIB_SET__ SETA 1" \ -o startup.o startup.s--pd是预定义宏,等价于在汇编里写GBLA声明。启动文件里经常用条件汇编区分不同库,这里就是入口。
第三步,链接:
armlink --cpu Cortex-M4.fp \ --scatter scatter.sct \ --strict \ --info sizes,totals \ --map --list=project.map \ -o project.axf \ main.o startup.o--scatter指定分散加载文件,决定代码和数据放在 Flash 还是 RAM 的哪个地址;--strict把警告当错误,CI 里建议开;--info sizes,totals输出各段大小,方便看 Flash/RAM 占用;--map --list=project.map生成 map 文件,排查内存溢出时全靠它。
第四步,转二进制:
fromelf --bin --output=project.bin project.axf fromelf --text -c --output=project.dis project.axf第一条生成烧录用的.bin,第二条生成反汇编文本,调试 HardFault 时对着.dis看 PC 指针落在哪一行,比盲猜快得多。
提示:
--cpu参数在 armcc、armasm、armlink 三处必须一致,任何一处写错都会在链接阶段报L6242E之类的架构不匹配错误,这是新手最常踩的坑之一。
3. 参数调优与构建脚本:把 ARMCC 塞进自动化流程
3.1 优化等级与代码体积的取舍
ARMCC 的优化选项比 GCC 细,常用的几档:
| 参数 | 含义 | 适用场景 |
|---|---|---|
-O0 | 不优化 | 单步调试,变量不被重排 |
-O1 | 基础优化 | 调试与体积折中 |
-O2 | 较高优化 | 量产固件常用 |
-O3 | 最高优化 | 计算密集型,慎用 |
-Ospace | 优先减小体积 | Flash 紧张时 |
-Otime | 优先提升速度 | 实时性要求高时 |
我一般会这样组合:调试阶段用-O0 -g,量产阶段用-O2 -Ospace。注意-Ospace和-Otime不能同时给,给了后者会覆盖前者。还有一个玄学参数--loop_optimization_level,在 5.06 版本里对循环展开有额外控制,但文档写得含糊,我一般不动它,除非 profiling 明确指向某个循环。
代码体积对比可以用fromelf --text -z看各段大小:
fromelf --text -z project.axf # 输出各段的 Code / RO-data / RW-data / ZI-data 大小如果-Ospace之后 Code 段反而变大,通常是内联函数被过度展开,这时候加--no_inline或者对特定函数加__attribute__((noinline))更有效。
3.2 用 Makefile 封装 ARMCC 构建
手工敲命令只适合验证,真正落地要写成 Makefile。下面是一个可复用的模板:
# 工具链路径,按实际安装位置改 ARMCC = armcc ARMASM = armasm ARMLINK = armlink FROMELF = fromelf # 目标核与优化 CPU = Cortex-M4.fp OPT = -O2 -Ospace CFLAGS = --c99 -c --cpu $(CPU) $(OPT) -g -W --split_sections ASFLAGS = --cpu $(CPU) LFLAGS = --cpu $(CPU) --scatter scatter.sct --strict --info sizes,totals # 源文件 C_SRCS = main.c driver.c ASM_SRCS = startup.s C_OBJS = $(C_SRCS:.c=.o) ASM_OBJS = $(ASM_SRCS:.s=.o) OBJS = $(C_OBJS) $(ASM_OBJS) # 默认目标 all: project.bin %.o: %.c $(ARMCC) $(CFLAGS) -o $@ $< %.o: %.s $(ARMASM) $(ASFLAGS) -o $@ $< project.axf: $(OBJS) $(ARMLINK) $(LFLAGS) -o $@ $(OBJS) project.bin: project.axf $(FROMELF) --bin --output=$@ $< $(FROMELF) --text -c --output=project.dis $< clean: rm -f *.o *.axf *.bin *.dis *.map关键点说明:%.o: %.c这条模式规则里$<是第一个依赖,$@是目标名,写错顺序会导致编译空文件;--split_sections配合armlink的--remove才能剔除死代码,但--remove默认是开的,不用显式加;clean里把.map也删掉,避免旧 map 干扰排查。
注意:ARMCC 在 Windows 下路径分隔符用反斜杠,但 Makefile 里统一用正斜杠更稳,
armcc自己能识别。如果遇到cannot open source file但文件明明存在,先检查路径里有没有空格,ARMCC 对带空格的路径处理不如 AC6 友好。
3.3 与 ARM Compiler 6 的共存策略
很多团队同时维护新旧两套工程,机器上装了 AC5 和 AC6。切换时最容易出问题的是内联汇编语法:AC5 用__asm { ... },AC6 用__asm volatile("...")。如果一份代码要两边都能编,常见做法是用宏隔离:
#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION < 6000000) __asm { NOP } #else __asm volatile("nop"); #endif__ARMCC_VERSION是编译器内置宏,AC5 的值在 5000000 到 5999999 之间,AC6 从 6000000 起。用这个宏做条件编译,比手动改代码可靠。另外 AC5 的--c99在 AC6 里变成了-std=c99,构建脚本里也要对应改。
4. 避坑与排查:ARMCC 使用中的五类高频问题
4.1 链接报 L6218E:未定义符号但函数明明写了
现象:armlink报L6218E: Undefined symbol xxx,但xxx在某个.c里确实定义了。
原因:最常见的是该.c文件没被加入编译列表,或者函数被static修饰导致链接器看不到。另一种情况是 C++ 文件里的函数没加extern "C",名字被 mangle 了。
解决:先看 map 文件里有没有这个符号的.o来源;没有就检查 Makefile 的源文件列表。C++ 场景在头文件里加:
#ifdef __cplusplus extern "C" { #endif void my_func(void); #ifdef __cplusplus } #endif4.2 浮点运算结果异常:--cpu 没带 .fp 后缀
现象:代码里用了float,编译通过,但运行时结果全是 0 或者 HardFault。
原因:--cpu Cortex-M4不带.fp时,编译器按软件浮点处理,但启动文件里可能开了 FPU,两边不一致。
解决:确认armcc、armasm、armlink三处的--cpu都带.fp,并且启动文件里SCB->CPACR的 FPU 使能位已置位。用fromelf --text -c反汇编看有没有VLDR/VSTR指令,没有就说明浮点没启用。
4.3 优化后变量被“优化没了”:volatile 漏加
现象:-O2下某个全局变量在调试器里看不到变化,或者循环被整个删掉。
原因:编译器认为该变量没有被“有效使用”,直接优化掉。中断里修改的变量、硬件寄存器映射的变量,都必须加volatile。
解决:给所有可能被中断或硬件修改的变量加volatile。如果加了还不行,检查是不是被static限制了作用域导致链接器剔除,用__attribute__((used))强制保留。
4.4 scatter file 地址冲突:RW 段和 ZI 段重叠
现象:链接时报L6221E: Execution region ER_RW overlaps with Execution region ER_ZI。
原因:分散加载文件里两个区域的起始地址和大小算错了,RW 数据段结束地址超过了 ZI 段起始地址。
解决:打开 map 文件看Execution Region的实际地址范围,重新核算。常见错误是 RAM 总大小没减去堆栈预留,导致 ZI 段越界。我一般会在 scatter 里显式留出ARM_LIB_STACK和ARM_LIB_HEAP,不依赖默认值。
4.5 fromelf 生成的 bin 文件大小不对
现象:fromelf --bin出来的.bin比预期大很多,或者开头一堆 0xFF。
原因:.axf里包含了调试段和未初始化段,--bin默认把所有段都转出来。如果 scatter 里地址不连续,中间的空洞会被填充。
解决:用--bin --output时加--only指定段,或者先用fromelf --text -z看段布局,确认只转ER_IROM区域。另一个办法是链接时加--remove剔除无用段,从源头减小体积。
5. 进阶技巧:用 map 文件和反汇编定位 HardFault
HardFault 是 ARMCC 老工程里最让人头疼的问题之一,因为默认的 HardFault_Handler 就是个死循环,什么信息都不给。我的习惯是结合 map 文件和反汇编来定位。
先看 map 文件里的符号表:
# 在 project.map 里搜索函数地址 grep -n "main" project.map # 输出示例:0x080001a5 Thumb Code 42 main main.o(.text.main)拿到地址后,在调试器里看 HardFault 发生时的 PC 值,比如0x080001b2,落在main的范围内,说明问题在 main 里。再用反汇编精确定位:
fromelf --text -c --output=project.dis project.axf # 在 project.dis 里找 0x080001b2 对应的指令.dis文件里每条指令前面都有地址,找到0x080001b2那一行,看它访问了哪个寄存器或地址。常见原因是空指针解引用或者数组越界写坏了栈。
更进一步,可以在 HardFault_Handler 里用汇编把LR、PC、PSR压栈后传给 C 函数打印:
__asm void HardFault_Handler(void) { IMPORT hardfault_report TST LR, #4 ITE EQ MRSEQ R0, MSP MRSNE R0, PSP B hardfault_report } void hardfault_report(unsigned int *stack) { volatile unsigned int pc = stack[6]; volatile unsigned int lr = stack[5]; (void)pc; (void)lr; while (1); }这段代码的关键是TST LR, #4判断异常发生前用的是 MSP 还是 PSP,然后把对应的栈指针传给 C 函数。stack[6]是压栈的 PC,stack[5]是 LR。拿到 PC 后回到 map 文件查符号,基本能锁定到函数级别。
提示:这段汇编在 AC5 和 AC6 下语法不同,AC6 要用
__asm volatile加纯汇编字符串,迁移时注意改写。
从那以后我每次接手 ARMCC 老工程,第一件事就是把 map 文件和.dis反汇编在构建脚本里强制生成,不管当前用不用得上。HardFault 来的时候没有后悔药,只有提前留好的黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取