【说在前面的话】
时间大约在2015年,Arm第一次在
MDK 5.20
中引入了
Arm Compiler 6
(那时候的版本是 6.9),正式拉开了Arm官方编译器从第五版(armcc)到第六版(armclang)升级替换的序幕……
嵌入式行业的长尾效应是及其突出的,且不说都2022年了还有很多人在坚持 MDK4,即便是从“
Arm在2017年对外宣布停止维护 Arm Compiler 5
”算起,如今5年过去了,坚持使用 armcc 的用户仍然不在少数。
Arm Compiler 5
,也就是大家口中的
armcc
,它很弱么?相对免费的工具链
arm gcc
来说,它还是强很明显;但你要说它非常能打么?作为一个“理论上”收费的编译器,它甚至已经全方位落后于最新发布的“免费开源”编译器
LLVM Embedded ToolChain For Arm 14.0.0
(clang),更不用说现在的当红贵人
Arm Compiler 6(armclang)
了。
如果非要我给出一份“不负责任”的编译器性能对比的话,这是独属于我的答案:
arm gcc < armcc < clang < IAR <= armclang
别问我为什么,问就是谁用谁知道。
如果不是因为产品存在 Golden Code(屎山),只要你选定了Arm Compiler 而不是IAR,既然横竖要使用付费编译器,为什么不用Arm例行维护(几乎每半年不到就发布一个新版本)的Arm Compiler 6,而继续死守Arm Compiler 5呢?
有的人说“Arm Compiler 6不如Arm Compiler 5”稳定
。这里给出我的几个反驳的几个理由,
但我不指望能说服那些抱有这类想法的人
:
-
Arm Compiler 5已经停止维护,Arm Compiler 6还在持续更新
。没有bug的编译器是不存在的,一个生命周期已经结束的编译器就几乎不在存在修复已有bug和未发现bug的可能性;而一个积极维护的编译器则可以及时的将发现的问题进行修复; -
Arm Compiler 5过去只有Arm维护,
而 Arm Compiler 6是基于LLVM(clang)的商业化改进版,这里LLVM是一个开源项目,由众多的个人和商业组织共同维护,
参考过去gcc的成功——
这么多“大聪明”在盯着的项目,即便发现错误,估计也是“分分钟”就被拿去“邀功请赏”了吧?
-
虽然我在实际使用中抓到(报告并得到修复)的Arm Compiler 6 bug的数量超过在座99%的人,但正因如此,我知道要遇到一个Arm Compiler 6的bug有多难——更多时候,其实是我们自己对编译器理解不深刻,甚至是基于自己对C语法的错误认知导致的“乌龙”。在我看来,
与其怀疑Arm Compiler 6不稳定,不如怀疑下自己对C语言的理解
。 -
不要屈服于由“未知带来的恐惧”,不要拿“污名化”当做掩盖自己“偷懒和无知”的遮羞布
(对这句话感到愤怒的人,我送你一句:爱听不听,欢迎取关,谢谢)。
看到这里,如果你决定继续往下阅读,我就
假设你已经有兴趣去尝试使用Arm Compiler 6 来逐步取代已有的 Arm Compiler 5了
。基于这一前提,我们将用随后的一系列文章来介绍:
-
短期内:
MDK 5.37 抛弃 armcc 的补救措施
-
中期:从 armcc 向 armclang 进行
过渡时期的一些快速应对的方法
-
面对一些
armcc 独有的编译器特性的应对方法
吧
【临时补救】
虽然最新的 MDK 抛弃了Arm Compiler 5,但它仍然允许我们通过手动添加的方法将其请回来,具体方法我在《
惊爆内幕:老MDK也可以使用新编译器
》文章中已经详细介绍过,这里就不再赘述,值得补充说明的是:
1、
新MDK也可以手工添加老版本的编译器
,不要被文章的标题限制住了思路
2、Arm Compiler 5的下载链接如下:
https://developer.arm.com/downloads/-/legacy-compilers
【几颗定心丸】
1、“接头霸王”
我们知道MDK是一个集成开发环境(
Integrated Development Environment
),它默认原生支持
Arm Compiler 5(armcc)
、
Arm Compiler 6(armclang)
和
arm gcc
。虽然这三个编译器都是由Arm所维护和提供的,但前两者算是彼此兼容的编译器:
-
使用共同的 armlink
-
使用相同的方式来描述地址空间布局
(分散加载脚本 scatter script) -
从
Arm Compiler 6.14
开始,
armclang
甚至开始支持
armasm
的汇编语法了
实际上可以认为,
armcc
和
armclang
是一对连体兄弟,身子是
armlink
,而两个脑袋分别是
armcc
和
armclang
。大约是这种感觉,你体会下。
作为定心丸的结论是:
-
原来
Arm Compiler 5
项目下的所有库
(*.lib)
都可以在
Arm Compiler 6
下直接使用 -
原来由
Arm Compiler 5
生成的对象文件
(*.o)
都可以在
Arm Compiler 6
下直接使用 -
原来
Arm Compiler 5
下所用到的“
几乎所有
” armlink 相关的特性都可以在
Arm Compiler 6
直接使用(因为基本就是同一个armlink,所以几乎不存在“移植”的说法)
当然,还是有一些特例的,比如
__attribute__((at(地址)))
语法,这个我们将出一个专题来介绍应对方式。
2、“偷懒是第一生产力”
由于
Arm Compiler 6
脱胎于
LLVM
,因此在汇编语法上它也继承了
clang
的特性——使用
GNU Assembly Syntax
,而非 Arm 此前一直尝试推广的
Unified Assembly Language(UAL)
汇编语法。
由于 Arm Compiler 5 一直使用的是 UAL 汇编语法,广大用户长时间来积累了大量使用该语法编写的 .s 文件。
汇编原本就是个头疼的东西——不到万不得已谁写汇编啊?对很多项目来说,且不说汇编原本就是少数大牛才敢碰的东西——几乎就是“Golden Code(屎山)”的代名词,实际上,这些“历史尘埃”的作者可能早就已经离职了——就算你把本人找回来,恐怕很多时候连当事人自己也是狗咬刺猬无法下嘴了。
尽管 Arm 专门写了一个名为《
Migrating from armasm to the armclang Integrated Assembler
》的文档来“教大家做事”,但社区的反馈可想而知……
文章链接如下:
https://developer.arm.com/documentation/100068/0618/Migrating-from-armasm-to-the-armclang-Integrated-Assembler?lang=en
在众多“我不想,你求我啊……”的声音中,Arm Compiler 6从 6.14版本开始,重新把 UAL 的支持加了回来,并在 MDK 中引入了这样一个选项:
这里几个选项的意义如下:
-
armclang(Auto Select)
:使用 armclang 来编译汇编源代码(对应命令行选项
-masm=auto
),然后armclang会根据语法风格自动决定是当做 GNU Assembly Syntax 来处理,还是使用 UAL 语法来解析。
我吐血推荐使用这个选项
。 -
armclang (GNU Syntax)
:使用
armclang
来编译汇编源代码(对应命令行选项
-masm=gnu
),然后强制使用 GNU 汇编语法风格。 -
armclang (Arm Syntax)
:使用
armclang
来编译汇编源代码(对应命令行选项
-masm=armasm
),然后强制使用 UAL 汇编语法风格。
其实,这里
armclang
也是个二道贩子——它也是调用
armasm
来完成编译的,只不过在这之前,它会默认用C预编译器对汇编源代码进行预处理,换句话说,折磨
armasm
很多年的“如何在汇编代码中使用C语言宏和预处理”的问题,得到了根治——你可以大大方方的在汇编代码里用 #include、各类宏定义和 #if 了。
-
armasm(Arm Syntax)
:直接使用
armasm
来编译汇编源代码。该选项对 老的 UAL 源代码文件兼容性最好。如果使用
armclang
(Arm Syntax)遇到问题,不妨用这个选项来试一下——一般都可以顺利解决问题。
怎么样,不用修改屎山了,是不是如释重负?
3、在线汇编(Inline Assembly)和嵌入C代码的汇编(Embedded Assembly)
无论你是否了解
Arm Compiler 5
所支持的这两种在C语言中使用汇编的方法,也不用关心它们的区别,结论是——任何
Arm Compiler 5
下的C代码只要使用了上述两种方法之一,基本上就是“需要手工干预”的。
这里我给出一个万能药方:
对这部分C源文件,请使用 armcc 编译,生成 .o 后扔到 Arm Compiler 6里直接参与链接即可
。
当然,如果你有兴趣依照前面文档里的介绍进行改写,我祝你好胃口。
至于如何让改写后的C代码同时兼容
Arm Compiler 5
和
Arm Compiler 6
,就离不开下面的内容了——
它也是我们后续一系列差异化改造的基础
。
【如何检测编译器】
一般来说,当我们要对某一部分代码进行跨编译器移植的时候,当然可以按照新语法一改了之,但对很多人来说,老的编译器总是会让大家萌生一种说不上来的留念之情,
继而抱有:
“我要让修改后的代码仍然兼容过去老编译器”;
或是:
“老代码删除太可惜了,我要留下来,以后万一有用呢?”
这样的想法。我也是这么想的。
要做到这一点,就绕不开一个核心问题:
如何可靠的检测出当前编译器版本呢?
一般来说,编译器的宏检测有两个思路:
-
借助某一编译器独有的特征宏来判断编译器
-
借助多个编译器共有但值不同的宏来判断
对于第一种思路,有两个比较有名的宏:
__GNUC__
和
__clang__
。过去,很多人喜欢用下面的代码来判断编译环境是否是
GCC
或者
CLANG
:
#if defined(__GNUC__)
/* 我觉得编译器gcc */
#endif
#if defined(__clang__)
/* 我觉得编译器是 clang */
#endif
然而,遗憾的是,由于很多编译器都在某种程度上对
GCC
扩展提供支持,因而也会定义宏
__GNUC__
,比如
armcc
、
armclang
、
clang
、
IAR
都定义了该宏……因此,它几乎失去了GCC特征宏的价值,退化为“当前编译器支持
GCC
扩展(但具体哪些
GCC
扩展,这就看我心情了)”的标志。其实
__clang__
宏也是类似的情况,因为
armclang
也会定义该宏,毕竟
Arm Compiler 6
是从LLVM中派生而出的。
当然,更为常见和有用的编译器特征宏是 __IAR_SYSTEMS_ICC__ ,借助它的帮助,我们可以判断当前开发环境是否为 IAR:
//! \note for IAR
#undef __IS_COMPILER_IAR__
#if defined(__IAR_SYSTEMS_ICC__)
# define __IS_COMPILER_IAR__ 1
#endif
Arm Compiler 5
和
Arm Compiler 6
都是
Arm Compiler
,区别它们二者有很多方法,但官方推荐的方法是判断宏
__ARMCC_VERSION
的值。从名字上就可以看出,这是一个自
armcc
以来一直延续到
armclang
的共有宏,它保存了编译器的版本,因此我们很容易编写出如下的宏:
//! \note for arm compiler 5
#undef __IS_COMPILER_ARM_COMPILER_5__
#if ((__ARMCC_VERSION >= 5000000) && (__ARMCC_VERSION < 6000000))
# define __IS_COMPILER_ARM_COMPILER_5__ 1
#endif
//! @}
//! \note for arm compiler 6
#undef __IS_COMPILER_ARM_COMPILER_6__
#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)
# define __IS_COMPILER_ARM_COMPILER_6__ 1
#endif
#undef __IS_COMPILER_ARM_COMPILER__
#if defined(__IS_COMPILER_ARM_COMPILER_5__) && __IS_COMPILER_ARM_COMPILER_5__ \
|| defined(__IS_COMPILER_ARM_COMPILER_6__) && __IS_COMPILER_ARM_COMPILER_6__
# define __IS_COMPILER_ARM_COMPILER__ 1
#endif
借助它们的帮助,我们可以很容易的通过判断
__IS_COMPILER_ARM_COMPILER_5__
和
__IS_COMPILER_ARM_COMPILER_6__
的值是否为“1”来确定当前的编译器版本。在只关心当前编译器是否为Arm Compiler,而不在乎它具体是哪个版本时,可以借助
__IS_COMPILER_ARM_COMPILER__
来进行判断。
假设我们的代码只考虑支持
gcc
、
clang
、
iar
、
armcc
和
armclang
,那么利用排除法,我们就可以轻松的判断当前编译环境是否是
GCC
或
LLVM
了:
#undef __IS_COMPILER_LLVM__
#if defined(__clang__) && !__IS_COMPILER_ARM_COMPILER_6__
# define __IS_COMPILER_LLVM__ 1
#else
//! \note for gcc
# undef __IS_COMPILER_GCC__
# if defined(__GNUC__) && !( defined(__IS_COMPILER_ARM_COMPILER__) \
|| defined(__IS_COMPILER_LLVM__) \
|| defined(__IS_COMPILER_IAR__))
# define __IS_COMPILER_GCC__ 1
# endif
//! @}
#endif
简单说一下这里的思路:
1、在排除了
Arm Compiler 6
的前提下,根据
__clang__
来判断当前编译器是否为
LLVM
(即:
__IS_COMPILER_LLVM__
);
2、在排除了
LLVM
、
Arm Compiler
和
IAR
的前提下,根据
__GNUC__
来判断当前编译器是否为
GCC
为了方便大家理解,下面介绍几个上述宏的应用场景:
-
如何在 Arm Compiler 6 下告知编译器 main() 函数不带输入参数
默认情况下(使用默认的 libc),
Arm Compiler
6
会认为
main()
函数是带有标准的输入参数的:
int main (int argc, char *argv[]);
哪怕你强行把 main() 函数写成无需输入参数的情况,编译器也还是会准备好参数——而准备参数的过程很有可能会导致
hardfault
(这里会涉及到
semihosting
的问题,比较头疼,暂时不表)。为了解决这一问题,我们一般这么做:
#if __IS_COMPILER_ARM_COMPILER_6__
__asm(".global __ARM_use_no_argv\n\t");
#endif
又因为
MicroLib
不存在该问题,因为我们可以根据(MDK会追加的一个宏)
__MICROLIB
,来做一个小小的区分:
#if __IS_COMPILER_ARM_COMPILER_6__
# ifndef __MICROLIB
__asm(".global __ARM_use_no_argv\n\t");
# endif
#endif
也就是当且仅当我们使用
Arm Compiler 6
,且不使用
MicroLib
的时候,通过专门的语法结构来告诉编译器:
main()
函数没有传入参数。
-
如何关闭 Semihosting
你有没有遇到过这样神奇的情景:在调试模式下,程序可以正常运行;一旦退出调试模式,系统就死机了,重新进入调试模式后,发现系统进入了Hardfault。恭喜你,这很可能就是(默认开启的)
semihosting
在作怪。关于Semihosting的内容,篇幅过大,不在本文讨论之列。今天我们只介绍一下如何关闭它。
Arm Compiler 5
和
Arm Compiler 6
关闭
Semihosting
的方法是不同的:
#if __IS_COMPILER_ARM_COMPILER_6__
__asm(".global __use_no_semihosting");
#elif __IS_COMPILER_ARM_COMPILER_5__
#pragma import(__use_no_semihosting)
#endif
一旦关闭了
Semihosting
,
Arm Compiler 6
就可能会报告类似如下的错误:
Error: L6915E: Library reports error: __use_no_semihosting was requested, but _sys_exit was referenced
简单解释下原因:
Arm Compiler 6
依赖的一个函数
_sys_exit()
原本是用
Semihosting
方式默认提供的,现在你把
Semihosting
关闭了,所以你要负责到底。知道了原因,解决方法也很简单——缺这个函数,我们提供一个就行:
#if __IS_COMPILER_ARM_COMPILER_6__
void _sys_exit(int ret)
{
(void)ret;
while(1) {}
}
#endif
类似的情况还会发生在一个叫
_ttywrch()
的函数上,我们可以如法炮制:
/* 为 arm compiler 5 和 arm compiler 6 都添加这个空函数 */
#if __IS_COMPILER_ARM_COMPILER__
void _ttywrch(int ch)
{
ARM_2D_UNUSED(ch);
}
#endif
-
如何解决使用
assert.h
引发的问题
很多代码都有使用
assert()
来截获错误的习惯,当我们使用
Arm Compiler 6
且开启
MicroLib
的时候,由于
MicroLib
并不提供对
assert()
底层函数的具体实现,
当我们没有定义 NDEBUG 来关闭 assert() 时
,会在链接阶段看到如下的编译错误:
Error: L6218E: Undefined symbol __aeabi_assert (referred from main.o).
知道原因后,解决也很简单:既然
MicroLib
没提供实现,我们就自己提供一个好了:
#if __IS_COMPILER_ARM_COMPILER_6__ && defined(__MICROLIB)
void __aeabi_assert(const char *chCond, const char *chLine, int wErrCode)
{
(void)chCond;
(void)chLine;
(void)wErrCode;
while(1) {
__NOP();
}
}
#endif
既然上述这套
__IS_COMPILER_xxxx__
这么好用,我们可以从哪里获得呢?
目前已知的获取渠道
包括但不限于
:
-
从本文抄下来
-
包含获取
perf_counter
并包含
perf_counter.h
-
在存在
arm-2d
的情况下,直接包含
arm_2d.h
或者
arm_2d_utils.h
……
【说在后面的话】
我承认
Arm Compiler 5
迁移到
Arm Compiler 6
不是一个轻松的过程,但也绝非大家想象的那样痛苦,很多时候,也许只是在
MDK
中更换一个选项那么简单:
不试一试怎么知道呢?
对主流芯片大厂,比如 ST和NXP来说,它们的库早就完成了对 Arm Compiler 6的支持,
可以说如果你遇到编译器兼容问题,应该首先
考虑下载最新版本的驱动库
。
本文介绍的方法,基本上可以应对常见的从
Arm Compiler 5
到
Arm Compiler 6
可能遇到的问题。这当然不是一份万能的解药,对于一些特殊的情况,我们将在后续文章中进行专题讨论。
最后附上各类编译器的下载链接:
Arm Compiler 5:
https://developer.arm.com/downloads/-/legacy-compilers
Arm Compiler 6:
https://developer.arm.com/downloads/-/arm-compiler-for-embedded
Arm GCC:
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/downloads
LLVM Embedded ToolChain for Arm 14.0.0
https://github.com/ARM-software/LLVM-embedded-toolchain-for-Arm/releases
也可以在关注公众号【裸机思维】后,发送消息LLVM获得