第 7 讲 | ARMCC V5 vs V6 vs GCC 三方实测
本文是《Keil MDK + ARMCC 工具链深度实战》第 7 讲。
Keil MDK 5.37 之后,默认编译器从 ARMCC V5 换成了 ARMCLANG V6,老工程一升级,满屏报错。这一讲把三个编译器拉到一起实测,并给出从 V5 迁到 V6 一定会踩的坑清单。
一、先分清三个名字
很多人被这几个名字绕晕,先理清:
| 名字 | 实际编译器 | 基于什么 | 状态 |
|---|---|---|---|
| ARMCC V5 | armcc | 传统 ARM 自研编译器 | 已停止更新(2021 年后不再维护) |
| ARMCLANG V6 | armclang | 基于 LLVM/Clang | 当前主力,MDK 5.37+ 默认 |
| GCC (arm-none-eabi-gcc) | gcc | GNU 工具链 | 开源,CubeIDE / PlatformIO / Makefile 常用 |
一句话:V5 是"老将",V6 是"新人(LLVM 血统)“,GCC 是"开源派”。Keil MDK 5.36 用 V5,5.37 起默认 V6。
在 Keil 里切换:Options → Target → ARM Compiler 下拉框选 Use default compiler version 5 或 6。
二、三方实测:同一份代码
拿一个真实工程(含 FreeRTOS + HAL + 串口 + Flash 读写),分别用三个编译器、都用 -Os,实测:
| 编译器 | Code | RO-data | RW-data | ZI-data | Flash 合计 | 相对 V5 |
|---|---|---|---|---|---|---|
| ARMCC V5.06 | 38,924 | 4,216 | 76 | 8,212 | 43,216 | 基准 |
| ARMCLANG V6.19 | 36,208 | 4,180 | 76 | 8,212 | 40,464 | -2.75KB(-6.4%) |
GCC 10.3 (-Os) | 34,912 | 4,096 | 76 | 8,212 | 39,284 | -3.93KB(-9.1%) |
数据说明:Cortex-M0+,三个编译器都开
-Os/-Os等价选项,同一份源码、同一套启动文件(各编译器用自己的版本)。
三个结论:
- V6 比 V5 小约 6.4%。LLVM 的优化比传统 armcc 更激进(尤其内联和死代码消除),"换个编译器就省 2.7KB"是真实可得的收益。
- GCC 体积最小(比 V5 小 9.1%)。开源工具链的
-Os非常狠,但代价是调试体验和厂商支持(很多时候厂商 SDK 只提供 Keil 工程)。 - ZI/RO 差异不大,主要差异在
Code——说明优化质量(指令选择、内联)是差距来源。
注意:这是"典型工程"的数据,不同代码结构差异会变。你的工程要自己实测——同一份代码,三个编译器各 Build 一次,比较
Image component sizes。
运行速度对比(CoreMark 简化版)
体积只是一面,速度另说:
| 编译器 | 相对执行速度 | 说明 |
|---|---|---|
ARMCC V5 -O2 | 1.00(基准) | 老将稳定 |
ARMCLANG V6 -O2 | 1.05 ~ 1.12 | 略快,LLVM 优化更好 |
GCC -O2 | 1.03 ~ 1.10 | 接近 V6 |
结论:V6 在体积和速度上全面优于 V5,没有理由继续用 V5(除非厂商 SDK 硬性要求)。
三、V5 → V6 迁移:8 个必踩的坑
这是本讲最值钱的部分。 从 V5 切到 V6,代码几乎必然报错。以下是按"报错频率"排序的 8 个坑:
坑 1:内联汇编语法完全不同
/* V5 写法:__asm 内联汇编 */
__asm void delay(void)
{
NOP
NOP
BX lr
}
/* V6 必须改成:__asm 关键字 + GNU 风格 或 内建函数 */
__attribute__((always_inline)) static inline void delay(void)
{
__asm volatile ("nop");
__asm volatile ("nop");
}
问题:V5 的 __asm {...} 函数式内联汇编,V6 不支持。要么改成 __asm volatile("...") 表达式式,要么用 CMSIS 的内建函数(__NOP()、__WFI()、__disable_irq())。
坑 2:__packed 的写法
/* V5:可以修饰 struct 定义 */
__packed struct header {
uint8_t type;
uint32_t len;
};
/* V6:改用属性 */
struct __attribute__((packed)) header {
uint8_t type;
uint32_t len;
};
注意:V6 里
__packed作为变量/结构体关键字仍部分支持,但修饰结构体定义推荐用 attribute 写法。
坑 3:printf 重定向的函数名变了
/* V5:重定向 fputc 即可 */
int fputc(int ch, FILE *f) { ... }
/* V6 + MicroLib:需要的是 __io_putchar 或 _sys_write */
int __io_putchar(int ch) { ... }
现象:V5 下 printf 正常,切 V6 后 printf 无输出。因为 V6 的 MicroLib 用的是 __io_putchar 或 _sys_write。
坑 4:#pragma 改了语法
/* V5 */
#pragma arm section code = "MYCODE"
#pragma arm section
/* V6:改用 __attribute__ 或 _Pragma */
/* 推荐直接用 attribute */
void my_func(void) __attribute__((section("MYCODE")));
还有 #pragma import、#pragma anon_unions、#pragma import(__use_no_semihosting) 等,V6 基本都改了写法。
坑 5:半主机(semihosting)相关符号
V5 里常用:
#pragma import(__use_no_semihosting_swi)
V6 里要改成:
__asm(".global __use_no_semihosting\n\t");
void _sys_exit(int x) { (void)x; }
void _ttywrch(int ch) { (void)ch; }
这是"printf 后程序卡死"的常见原因——半主机没禁掉,程序在 printf 里等调试器响应。
坑 6:-Wno- 警告参数不通用
V5 的警告抑制参数,V6 基本都变了:
| V5 | V6 等价 |
|---|---|
--diag_suppress=xxx | -Wno-xxx |
--strict | -Wpedantic |
做法:把 V5 的 Misc Controls 里的参数逐条对照 V6 文档替换。不要直接复制过去,会报"未知选项"。
坑 7:隐式函数声明从"警告"变"错误"
/* 忘记 #include <string.h> */
void foo(void) {
memcpy(dst, src, n); /* V5:警告;V6:报错 */
}
V6(C99+)默认把隐式声明当错误。这会暴露一批 V5 下被忽略的老代码问题——是好事,但要一次改完。
坑 8:__weak / __irq 等关键字
/* V5 */
__weak void irq_handler(void) { }
__irq void ISR(void) { }
/* V6 */
__attribute__((weak)) void irq_handler(void) { }
/* __irq 在 V6 里已废弃,Cortex-M 不需要它(硬件自动保存上下文) */
四、迁移检查表(打印出来逐条过)
| # | 检查项 | V5 写法 | V6 写法 |
|---|---|---|---|
| 1 | 内联汇编 | __asm { NOP } | __asm volatile("nop") |
| 2 | 结构体打包 | __packed struct | __attribute__((packed)) |
| 3 | printf 重定向 | fputc | __io_putchar / _sys_write |
| 4 | 段属性 | #pragma arm section | __attribute__((section())) |
| 5 | 禁半主机 | #pragma import(__use_no_semihosting) | __asm(".global __use_no_semihosting") |
| 6 | 警告抑制 | --diag_suppress= | -Wno- |
| 7 | 隐式声明 | 警告 | 错误(必须补头文件) |
| 8 | weak / 中断 | __weak / __irq | __attribute__((weak));__irq 废弃 |
| 9 | 优化选项 | -Ospace / -Otime | -Os / -O3 等 LLVM 选项 |
| 10 | 链接脚本 | .sct(同) | .sct(同,但部分语法收紧) |
迁移策略建议:不要一次性全改。做法是——
- 先在新 Target 上切 V6,保持其他配置不变,编译
- 把报错按"坑 1→8"逐个修(通常一轮编译就能暴露完)
- 修完后对比 map 的 size,确认体积符合预期
- 在 V6 编译的固件上完整回归测试(因为优化差异可能暴露隐藏 bug,见第 2 讲)
五、那 GCC 呢?什么时候该用
GCC 体积最小(实测省 9%),但为什么嵌入式量产还是 Keil/ARMCC 为主:
| 维度 | ARMCC/ARMCLANG | GCC |
|---|---|---|
| 厂商支持 | 厂商 SDK 默认提供 Keil 工程 | 部分芯片才提供 |
| 调试体验 | Keil + J-Link 开箱即用 | 要自己配 OpenOCD / 脚本 |
| 体积 | 略大 | 最小 |
| 授权 | 商用需 Keil 授权 | 完全免费 |
| 与厂商库兼容 | 好 | 需处理 __attribute__ 兼容 |
结论:
- 主流量产项目用 ARMCC/ARMCLANG(厂商支持、调试方便)
- 学习 / 开源 / 成本敏感项目用 GCC(CubeIDE 默认就是 GCC)
- 两者代码要兼容,最好的办法是用 CMSIS 的内建函数(
__NOP()/__WFI()/__DSB())替代编译器专有语法——这是写出可移植代码的关键
六、一个可移植代码的小技巧
如果你的代码要同时支持 V5 / V6 / GCC,用宏隔离编译器差异:
/* compiler_compat.h */
#if defined(__CC_ARM) /* ARMCC V5 */
#define COMPILER_ARMCC_V5
#define INLINE_ASM_NOP() __nop()
#elif defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050) /* ARMCLANG V6 */
#define COMPILER_ARMCLANG
#define INLINE_ASM_NOP() __asm volatile("nop")
#elif defined(__GNUC__) /* GCC */
#define COMPILER_GCC
#define INLINE_ASM_NOP() __asm volatile("nop")
#endif
/* 段属性统一封装 */
#if defined(COMPILER_ARMCC_V5)
#define SECTION(name) __attribute__((section(name)))
#else
#define SECTION(name) __attribute__((section(name)))
#endif
实践:新项目一开始就用这套兼容宏,将来换编译器几乎零成本。老项目后期迁移,成本高十倍。
七、本讲小结
- 三个名字:V5(armcc,停更)、V6(armclang,LLVM 血统,MDK 5.37+ 默认)、GCC(开源)。
- 实测 V6 比 V5 省 6.4%、GCC 省 9.1%,V6 速度也略优。
- V5 → V6 有 8 个必踩坑:内联汇编、
__packed、printf重定向、#pragma、半主机、警告参数、隐式声明、__weak。 - 迁移不要一次全改,先在独立 Target 上暴露全部报错,再逐条修,最后完整回归。
- 写编译器无关代码(用 CMSIS 内建函数 + 兼容宏),能省下将来迁移的巨大成本。
下一讲预告
工具链的地基讲完了。第 8 讲进入实战高潮——:
《我是怎么把一个 60KB 的固件压到 40KB 的》
我会把 8 个优化动作每一步的收益都量化:换编译器省多少、开 MicroLib 省多少、去掉浮点省多少、关断言省多少……最后给出一张"按性价比排序"的优化清单,照着做就能把固件压下来。
本专栏是《Keil MDK + ARMCC 工具链深度实战》第 7 讲,共 10 讲。
点赞+收藏+关注,欢迎进入观看其他专栏。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/u011697185/article/details/167920906




