
简介ARM编译器版本5是ARM公司面向ARM架构处理器推出的优化编译工具主要服务于嵌入式系统开发者、驱动工程师及需要跨ARM平台移植代码的技术人员。它支持C、C与汇编语言兼容C99和C03标准并提供O0至O3多级优化选项可在执行速度与代码体积之间灵活取舍针对ARM核心的指令级与循环级优化、VFP浮点运算、Thumb指令集支持以及自动栈展开等特性能有效降低函数调用开销、提升内存受限设备上的运行效率。资源包共3个文件以zip压缩包形式提供约80.94MB内含发行说明网页、Windows安装程序及MSI安装包分别用于查阅版本新特性与已知问题、完成编译器及相关组件的部署。目前已有4456人学习下载适合希望掌握编译选项配置、汇编代码调试与性能调优的开发者参考也可作为从ARM7到Cortex系列多核平台代码复用的实践依据。1. 从一次编译翻车说起ARM Compiler 5 到底还能不能碰上周帮一个做工业控制的朋友排查问题现象很典型一套维护了七八年的裸机工程在旧电脑上编译一切正常换到新装的开发环境后直接报了几十个链接错误__use_no_semihosting相关的符号全炸了。折腾半天才发现新环境默认装的是 ARM Compiler 6而这份工程从头到尾都是按 ARM Compiler 5 的语法和运行时库写的。这不是个例ARM Compiler 5简称 AC5虽然官方早已停止主版本更新但在大量存量嵌入式项目里依然是唯一能跑通的选择——尤其是那些依赖--c99模式、老版 CMSIS 库、或者特定芯片厂商 BSP 的工程。这篇文章不讨论该不该迁移到 AC6只解决一个实际问题你手上这份 ARM Compiler 5 资源包怎么装、怎么配、怎么在现有工具链里让它正常工作以及那些年我踩过的坑。2. ARM Compiler 5 的组成与安装别被目录结构绕晕2.1 工具链里到底有哪几个可执行文件很多人拿到 AC5 的安装包后第一反应是找armcc.exe这没错但只盯着一个文件会漏掉关键组件。一个完整的 AC5 工具链至少包含以下几类可执行程序它们各司其职可执行文件作用常见误用armccC/C 编译器主体直接命令行调用时忘记加--c99armasm汇编器处理.s文件与armcc的汇编预处理混淆armlink链接器生成.axf/.elf分散加载文件路径写错导致内存布局异常fromelf格式转换生成.bin/.hex忘记加--bin导致输出仍是 ELFarmar静态库打包打包顺序影响符号解析这些程序通常位于安装目录的bin子目录下。如果你用的是 Keil MDK 集成的 AC5它们藏在Keil_v5/ARM/ARMCC/bin里如果是独立安装的 DS-5 或 Arm Development Studio 附带的 AC5路径可能是DS-5/sw/ARMCompiler5.x/bin。不管哪种先把bin目录加到系统 PATH 里后面命令行调试会方便很多。2.2 安装步骤与版本选择AC5 的最后一个公开版本是 5.06 update 7build 960这个版本对 C99 的支持最完整也是大多数芯片厂商 BSP 验证过的版本。如果你手头的资源包是更早的 5.05 或 5.04遇到_Atomic或_Generic相关语法报错时不要惊讶那是编译器本身不支持。安装过程本身不复杂但有几个点容易翻车# 假设安装包解压到 /opt/ARMCompiler5 # 1. 设置环境变量注意路径不要有空格 export ARMCC5_HOME/opt/ARMCompiler5 export PATH$ARMCC5_HOME/bin:$PATH # 2. 验证版本注意输出里的 build 号 armcc --version # 期望输出类似ARM Compiler 5.06 update 7 (build 960) # 3. 检查 license独立安装版需要配置 LM_LICENSE_FILE export LM_LICENSE_FILE$ARMCC5_HOME/license.dat逻辑说明ARMCC5_HOME这个变量名不是强制的但后面写 Makefile 或 CMake 工具链文件时会反复引用统一命名能省很多事。armcc --version的输出里 build 号很关键有些第三方资源包会把 5.06 的壳套在 5.05 的核上build 号对不上就是信号。License 部分如果你用的是 Keil MDK 自带的 AC5通常不需要单独配MDK 的 license 已经覆盖了独立安装版才需要LM_LICENSE_FILE。注意AC5 在 Windows 上的安装程序会往注册表写编译器路径如果你同时装了 AC6两个版本的armcc和armclang可能互相干扰。常见做法是不要在系统 PATH 里同时放两个版本的 bin 目录而是通过工程级的工具链配置来指定。2.3 与 IDE 的集成方式如果你用 Keil MDK在Project - Manage - Project Items - Folders/Extensions里可以指定编译器版本。MDK 5.37 之后默认不再附带 AC5需要手动把 AC5 的安装目录填进去。具体路径是Keil_v5/ARM/ARMCC填完后在 Target 选项的Compiler下拉框里就能看到Use default compiler version 5。如果你用 Makefile 或 CMake集成方式更直接。以 Makefile 为例# 工具链定义 CC armcc AS armasm LD armlink FROMELF fromelf # 编译选项--c99 是 AC5 下启用 C99 的关键 CFLAGS --c99 -c --cpu Cortex-M4.fp --fpu FPv4-SP-D16 -O2 -I./inc ASFLAGS --cpu Cortex-M4.fp --fpu FPv4-SP-D16 LDFLAGS --scatter ./link.sct --info sizes --info totals # 目标文件生成 %.o: %.c $(CC) $(CFLAGS) $ -o $ %.o: %.s $(AS) $(ASFLAGS) $ -o $ # 链接与格式转换 firmware.axf: $(OBJS) $(LD) $(LDFLAGS) $(OBJS) -o $ firmware.bin: firmware.axf $(FROMELF) --bin --output $ $参数说明--cpu Cortex-M4.fp指定目标内核和浮点单元写错会导致生成的指令集不匹配。--fpu FPv4-SP-D16是单精度浮点如果你的芯片是双精度要改成FPv4-D16。--scatter指向分散加载文件这个文件决定了代码和数据的绝对地址AC5 和 AC6 的 scatter 语法有差异迁移时这里是重灾区。--info sizes会在链接结束后打印各段大小调试内存溢出时很有用。3. 编译选项与运行时库AC5 和 AC6 的真正分水岭3.1 那些 AC6 不认的 AC5 选项AC5 和 AC6 在命令行选项上的差异是导致迁移失败的首要原因。下面这几个选项在 AC5 里很常用但 AC6 的armclang完全不认--c99AC5 需要显式指定才启用 C99AC6 默认就是 C99 及以上写了反而报未知选项。--apcsinterworkAC5 的过程调用标准选项AC6 用-mabiaapcs替代。--split_sectionsAC5 下把每个函数放到独立段便于链接器剔除未使用代码。AC6 对应的是-ffunction-sections。--diag_suppressAC5 的警告抑制语法AC6 用-Wno-xxx风格。如果你在维护一份同时要兼容两个编译器的工程常见做法是用 Makefile 变量做条件分支# 根据编译器版本选择不同选项 ifeq ($(COMPILER),AC5) CFLAGS --c99 --apcsinterwork --split_sections else CFLAGS -stdc99 -mabiaapcs -ffunction-sections endif这种写法在芯片厂商的 SDK 里很常见但要注意COMPILER这个变量得自己传不能指望工具链自动识别。3.2 运行时库的差异与__use_no_semihostingAC5 和 AC6 的 C 运行时库是两套不同的实现。AC5 用的是 ARM 传统的 RVCT 运行时AC6 用的是基于 LLVM 的armclang运行时。这导致一个经典问题在 AC5 下用printf做串口输出时需要重定向fputc并且通常要加__use_no_semihosting来避免链接半主机相关代码。但在 AC6 下这个符号的处理方式变了。AC5 下的典型重定向代码#include stdio.h // 告诉链接器不要链接半主机相关代码 __asm(.global __use_no_semihosting\n\t); // 重定向 fputc 到串口 int fputc(int ch, FILE *f) { // 假设 UART0 的发送寄存器地址是 0x40000000 volatile unsigned int *uart_tx (volatile unsigned int *)0x40000000; while (*uart_tx 0x80); // 等待发送缓冲区空 *uart_tx (unsigned char)ch; return ch; } // AC5 下还需要实现 _sys_exit 等桩函数 void _sys_exit(int x) { x x; }逻辑说明__use_no_semihosting这个汇编指令是 AC5 特有的它告诉链接器不要引入半主机模式的_sys_open、_sys_write等函数。如果不加链接时会报一堆_sys_xxx未定义。fputc是标准库的弱符号重定向后printf就会走你的串口。_sys_exit是 AC5 运行时要求的桩函数不实现会报链接错误。到了 AC6__use_no_semihosting不再被识别取而代之的是在链接选项里加-nostdlib或者用__asm(.global __use_no_semihosting)的替代方案。很多从 AC5 迁移到 AC6 的工程卡在这里现象就是链接报_sys_exit、_sys_open等符号未定义。注意如果你只是想在 AC5 下用printf调试又不想折腾半主机最省事的做法是直接用--use_no_semihosting链接选项然后在代码里实现fputc和_sys_exit。但要注意一旦用了这个选项标准库里的文件操作函数全部不可用fopen、fread都会链接失败。3.3 分散加载文件在 AC5 下的写法分散加载scatter loading是 AC5 链接阶段的核心配置它决定了代码段、数据段、堆栈的绝对地址。一个典型的 Cortex-M4 分散加载文件长这样; link.sct LR_IROM1 0x08000000 0x00100000 { ; 加载区域Flash 起始 0x08000000大小 1MB ER_IROM1 0x08000000 0x00100000 { ; 执行区域代码和只读数据 *.o (RESET, First) ; 中断向量表放在最前面 *(InRoot$$Sections) ; 库函数相关段 .ANY (RO) ; 其余只读代码和数据 } RW_IRAM1 0x20000000 0x00020000 { ; 读写区域SRAM 起始 0x20000000大小 128KB .ANY (RW ZI) ; 已初始化数据和未初始化数据 } }参数说明LR_IROM1是加载区域ER_IROM1是执行区域两者地址相同表示代码在 Flash 里原地执行。RESET段必须放在最前面因为 Cortex-M 上电后从 0x08000000 取向量表的前两个字作为初始 SP 和 PC。First是 AC5 的语法AC6 的 scatter 文件里写法不同。RW_IRAM1里的RW ZI分别对应已初始化变量和零初始化变量堆栈通常也在这个区域里具体位置由__initial_sp等符号决定。如果你在 AC5 下遇到“代码放不下”的报错第一反应应该是看--info sizes的输出确认是哪个段超了。常见情况是RW_IRAM1不够这时候要么优化全局变量要么把堆栈调小要么换更大 RAM 的芯片。4. 避坑与排查AC5 下最容易翻车的五个场景4.1 链接报_sys_xxx未定义现象编译通过链接时报_sys_open、_sys_write、_sys_exit等符号未定义。原因AC5 的运行时库默认链接半主机模式这些_sys_xxx函数由半主机库提供。但如果你在代码里加了__use_no_semihosting或者链接选项里加了--use_no_semihosting半主机库就不会被链接这些符号就没人提供了。解决实现必要的桩函数。最少要实现_sys_exit和fputc如果用了printf之外的库函数可能还需要_sys_open、_sys_read等。一个偷懒的办法是直接实现一个空的_sys_exit然后把所有文件操作函数都避开不用。4.2--c99没加导致语法报错现象代码里用了for (int i 0; ...)或者变长数组编译报错说i未定义或者数组大小必须是常量。原因AC5 默认是 C90 模式C99 的语法特性需要显式开启。解决在编译选项里加--c99。注意这个选项要加在armcc上不是armlink上。如果你用 IDE在 Target 选项的 C/C 标签页里勾选 C99 模式。4.3 浮点单元配置不匹配现象程序跑起来后浮点计算结果全是 NaN或者直接进 HardFault。原因编译时指定的--fpu和芯片实际的浮点单元不一致。比如芯片是 Cortex-M4F 带单精度 FPU但编译时写了--fpu SoftVFP生成的代码用软件浮点模拟和硬件浮点寄存器不兼容。解决确认芯片的 FPU 类型Cortex-M4F 用FPv4-SP-D16Cortex-M7 单精度用FPv5-SP-D16双精度用FPv5-D16。同时检查启动文件里的FPU使能代码两者必须一致。4.4 分散加载文件路径写错现象链接报cannot open scatter file或者链接出来的程序跑飞。原因--scatter后面的路径是相对路径但 Makefile 的工作目录可能和预期不一致。或者 scatter 文件里的地址和芯片实际内存映射对不上。解决用绝对路径或者$(CURDIR)拼相对路径。地址方面对照芯片参考手册的内存映射表逐项核对特别是 Flash 和 SRAM 的起始地址与大小。4.5 优化等级导致的玄学问题现象-O0下运行正常-O2或-O3下行为异常比如某个循环被优化掉、变量读不到最新值。原因AC5 的高等级优化对volatile和内存屏障的处理比较激进如果代码里该加volatile的地方没加优化后就会出问题。解决对外设寄存器指针、中断和主循环共享的变量一律加volatile。如果还不行尝试在关键位置插入__schedule_barrier()或者降低该文件的优化等级。血泪经验是不要一上来就怀疑编译器有 bug先检查自己的volatile用对了没有。5. 进阶技巧用fromelf做镜像分析与校验5.1 从.axf提取有用信息fromelf是 AC5 工具链里被低估的工具大多数人只用它生成.bin其实它的分析功能在调试内存布局时非常有用。# 查看各段大小和地址分布 fromelf --text -s firmware.axf # 生成反汇编列表便于对照源码排查 fromelf --text -c -o firmware.dis firmware.axf # 生成 bin 文件注意 --bin 和 --output 的顺序 fromelf --bin --output firmware.bin firmware.axf # 查看符号表确认关键函数有没有被链接进来 fromelf --text -s --symbols firmware.axf | grep -i main\|isr逻辑说明--text -s输出段信息包括每个段的起始地址、大小和类型链接后第一时间跑一遍能快速发现哪个段超了。--text -c生成反汇编当程序跑飞而手头没有调试器时对照反汇编和源码能定位到具体指令。--symbols配合grep可以确认某个中断服务函数是否真的被链接进去了有时候函数写了但没在向量表里引用链接器会把它当垃圾回收掉。5.2 用--info选项做链接后检查在链接选项里加--info sizes --info totals --info unused链接结束后会打印一份详细报告。sizes显示各段大小totals显示汇总unused列出被剔除的段。我一般会在 Makefile 里把这份输出重定向到文件每次编译后扫一眼确认没有异常增长。LDFLAGS --info sizes --info totals --info unused # 链接命令后追加输出重定向 firmware.axf: $(OBJS) $(LD) $(LDFLAGS) $(OBJS) -o $ link_report.txt 215.3 版本锁定与工程归档AC5 已经停止更新这意味着你手头的版本就是最终版本。我吃过一次亏某次重装系统后从网上随便下了一个 AC5 安装包编译出来的二进制和之前的不一致排查了两天才发现那个包是 5.05 而不是 5.06。从那以后我每次归档工程时都会把 AC5 的安装目录整个打包进去同时在 README 里写清楚 build 号。具体做法是在工程根目录建一个tools文件夹把ARMCompiler5.06u7整个放进去Makefile 里的CC变量用相对路径指向它。这样无论换什么电脑只要解压工程就能编译不依赖系统环境。# 工程内自带工具链避免环境差异 TOOLCHAIN ./tools/ARMCompiler5.06u7/bin CC $(TOOLCHAIN)/armcc AS $(TOOLCHAIN)/armasm LD $(TOOLCHAIN)/armlink这个习惯帮我省了无数次“在我电脑上明明是好的”的扯皮。希望帮到你。本文还有配套的精品资源点击获取