wuyk555头像
Keil工具链-第07讲-ARMCC-V5-V6-GCC三方实测封面图

Keil工具链-第07讲-ARMCC-V5-V6-GCC三方实测

第 7 讲 | ARMCC V5 vs V6 vs GCC 三方实测

本文是《Keil MDK + ARMCC 工具链深度实战》第 7 讲。
Keil MDK 5.37 之后,默认编译器从 ARMCC V5 换成了 ARMCLANG V6,老工程一升级,满屏报错。这一讲把三个编译器拉到一起实测,并给出从 V5 迁到 V6 一定会踩的坑清单。


一、先分清三个名字

很多人被这几个名字绕晕,先理清:

名字实际编译器基于什么状态
ARMCC V5armcc传统 ARM 自研编译器已停止更新(2021 年后不再维护)
ARMCLANG V6armclang基于 LLVM/Clang当前主力,MDK 5.37+ 默认
GCC (arm-none-eabi-gcc)gccGNU 工具链开源,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,实测:

编译器CodeRO-dataRW-dataZI-dataFlash 合计相对 V5
ARMCC V5.0638,9244,216768,21243,216基准
ARMCLANG V6.1936,2084,180768,21240,464-2.75KB(-6.4%)
GCC 10.3 (-Os)34,9124,096768,21239,284-3.93KB(-9.1%)

数据说明:Cortex-M0+,三个编译器都开 -Os / -Os 等价选项,同一份源码、同一套启动文件(各编译器用自己的版本)。

三个结论:

  1. V6 比 V5 小约 6.4%。LLVM 的优化比传统 armcc 更激进(尤其内联和死代码消除),"换个编译器就省 2.7KB"是真实可得的收益。
  2. GCC 体积最小(比 V5 小 9.1%)。开源工具链的 -Os 非常狠,但代价是调试体验和厂商支持(很多时候厂商 SDK 只提供 Keil 工程)。
  3. ZI/RO 差异不大,主要差异在 Code——说明优化质量(指令选择、内联)是差距来源。

注意:这是"典型工程"的数据,不同代码结构差异会变。你的工程要自己实测——同一份代码,三个编译器各 Build 一次,比较 Image component sizes。

运行速度对比(CoreMark 简化版)

体积只是一面,速度另说:

编译器相对执行速度说明
ARMCC V5 -O21.00(基准)老将稳定
ARMCLANG V6 -O21.05 ~ 1.12略快,LLVM 优化更好
GCC -O21.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 基本都变了:

V5V6 等价
--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))
3printf 重定向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隐式声明警告错误(必须补头文件)
8weak / 中断__weak / __irq__attribute__((weak));__irq 废弃
9优化选项-Ospace / -Otime-Os / -O3 等 LLVM 选项
10链接脚本.sct(同).sct(同,但部分语法收紧)

迁移策略建议:不要一次性全改。做法是——

  1. 先在新 Target 上切 V6,保持其他配置不变,编译
  2. 把报错按"坑 1→8"逐个修(通常一轮编译就能暴露完)
  3. 修完后对比 map 的 size,确认体积符合预期
  4. 在 V6 编译的固件上完整回归测试(因为优化差异可能暴露隐藏 bug,见第 2 讲)

五、那 GCC 呢?什么时候该用

GCC 体积最小(实测省 9%),但为什么嵌入式量产还是 Keil/ARMCC 为主:

维度ARMCC/ARMCLANGGCC
厂商支持厂商 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

实践:新项目一开始就用这套兼容宏,将来换编译器几乎零成本。老项目后期迁移,成本高十倍。


七、本讲小结

  1. 三个名字:V5(armcc,停更)、V6(armclang,LLVM 血统,MDK 5.37+ 默认)、GCC(开源)。
  2. 实测 V6 比 V5 省 6.4%、GCC 省 9.1%,V6 速度也略优。
  3. V5 → V6 有 8 个必踩坑:内联汇编、__packed、printf 重定向、#pragma、半主机、警告参数、隐式声明、__weak。
  4. 迁移不要一次全改,先在独立 Target 上暴露全部报错,再逐条修,最后完整回归。
  5. 写编译器无关代码(用 CMSIS 内建函数 + 兼容宏),能省下将来迁移的巨大成本。

下一讲预告

工具链的地基讲完了。第 8 讲进入实战高潮——:

《我是怎么把一个 60KB 的固件压到 40KB 的》

我会把 8 个优化动作每一步的收益都量化:换编译器省多少、开 MicroLib 省多少、去掉浮点省多少、关断言省多少……最后给出一张"按性价比排序"的优化清单,照着做就能把固件压下来。


本专栏是《Keil MDK + ARMCC 工具链深度实战》第 7 讲,共 10 讲。
点赞+收藏+关注,欢迎进入观看其他专栏。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/u011697185/article/details/167920906

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--