|

tzbmf1zy4vg6401916532.png
9 b8 b' o& n6 @8 S& m
& H% X! F' m7 e- T) o# d* T【说在前面的话】
- X& H3 E7 s3 u8 T* f8 M时间大约在2015年,Arm第一次在 MDK 5.20 中引入了Arm Compiler 6(那时候的版本是 6.9),正式拉开了Arm官方编译器从第五版(armcc)到第六版(armclang)升级替换的序幕……嵌入式行业的长尾效应是及其突出的,且不说都2022年了还有很多人在坚持 MDK4,即便是从“Arm在2017年对外宣布停止维护 Arm Compiler 5”算起,如今5年过去了,坚持使用 armcc 的用户仍然不在少数。
! d7 N+ U0 N9 [Arm Compiler 5,也就是大家口中的 armcc,它很弱么?相对免费的工具链 arm gcc来说,它还是强很明显;但你要说它非常能打么?作为一个“理论上”收费的编译器,它甚至已经全方位落后于最新发布的“免费开源”编译器LLVM Embedded ToolChain For Arm 14.0.0(clang),更不用说现在的当红贵人Arm Compiler 6(armclang)了。: @; y5 E. D0 |# h: |: ]
8 }, ?: t, C& c
如果非要我给出一份“不负责任”的编译器性能对比的话,这是独属于我的答案:
1 \* J+ p! ?1 E0 P n& Karm gcc
; g- h! _/ T' e
5 A7 L# p. s. @. U+ |别问我为什么,问就是谁用谁知道。
! p+ Z+ u" t& V$ T9 k如果不是因为产品存在 Golden Code(屎山),只要你选定了Arm Compiler 而不是IAR,既然横竖要使用付费编译器,为什么不用Arm例行维护(几乎每半年不到就发布一个新版本)的Arm Compiler 6,而继续死守Arm Compiler 5呢?
. |' H) v8 V* U1 I6 y7 q D$ W3 Y有的人说“Arm Compiler 6不如Arm Compiler 5”稳定。这里给出我的几个反驳的几个理由,但我不指望能说服那些抱有这类想法的人:
* W( N1 Y$ C6 X7 I# \4 W! HArm Compiler 5已经停止维护,Arm Compiler 6还在持续更新。没有bug的编译器是不存在的,一个生命周期已经结束的编译器就几乎不在存在修复已有bug和未发现bug的可能性;而一个积极维护的编译器则可以及时的将发现的问题进行修复;Arm Compiler 5过去只有Arm维护,而 Arm Compiler 6是基于LLVM(clang)的商业化改进版,这里LLVM是一个开源项目,由众多的个人和商业组织共同维护,参考过去gcc的成功——这么多“大聪明”在盯着的项目,即便发现错误,估计也是“分分钟”就被拿去“邀功请赏”了吧?. T: P* N! h/ \2 E/ W
" S4 H9 c. V8 ?4 y% J# B
9 l6 f- N0 P. y( E+ M4 c4 u
tmcvxvucvrh6401916632.png
3 M1 c, S' m5 Q+ |2 X/ O! _
虽然我在实际使用中抓到(报告并得到修复)的Arm Compiler 6 bug的数量超过在座99%的人,但正因如此,我知道要遇到一个Arm Compiler 6的bug有多难——更多时候,其实是我们自己对编译器理解不深刻,甚至是基于自己对C语法的错误认知导致的“乌龙”。在我看来,与其怀疑Arm Compiler 6不稳定,不如怀疑下自己对C语言的理解。不要屈服于由“未知带来的恐惧”,不要拿“污名化”当做掩盖自己“偷懒和无知”的遮羞布(对这句话感到愤怒的人,我送你一句:爱听不听,欢迎取关,谢谢)。
- a* R) \/ \7 J1 h, a& s" h. I" A" B H- O, I U# v
看到这里,如果你决定继续往下阅读,我就假设你已经有兴趣去尝试使用Arm Compiler 6 来逐步取代已有的 Arm Compiler 5了。基于这一前提,我们将用随后的一系列文章来介绍:# W0 u' o. w' }: C2 S
短期内:MDK 5.37 抛弃 armcc 的补救措施中期:从 armcc 向 armclang 进行过渡时期的一些快速应对的方法面对一些 armcc 独有的编译器特性的应对方法吧" A+ Z' w* s2 J0 K+ X
$ }& `3 n+ E$ M2 B( \7 K
( b6 `, Y/ e( W# {+ J6 i" N' `
kei5bxg5mm26401916733.gif
; h: w e, _2 u; S. g
1 j9 y) ^! U% _9 r
' j+ Y- q E0 t% L$ `( V
【临时补救】
5 |, t; Z9 x1 J虽然最新的 MDK 抛弃了Arm Compiler 5,但它仍然允许我们通过手动添加的方法将其请回来,具体方法我在《惊爆内幕:老MDK也可以使用新编译器》文章中已经详细介绍过,这里就不再赘述,值得补充说明的是:% B* d1 f* d+ m3 H- k! P
1、新MDK也可以手工添加老版本的编译器,不要被文章的标题限制住了思路2、Arm Compiler 5的下载链接如下:https://developer.arm.com/downloads/-/legacy-compilers
7 s. Q& D7 T9 ?. a! B7 `
6 h5 d' V! S5 D; X! C' C4 Q【几颗定心丸】; ]' U/ {3 N- s; K; J; F; C, F
- ^, @9 \: U/ ?
1、“接头霸王”
9 m% E' p3 `; ^- m' W8 l我们知道MDK是一个集成开发环境(Integrated Development Environment),它默认原生支持Arm Compiler 5(armcc)、Arm Compiler 6(armclang)和 arm gcc。虽然这三个编译器都是由Arm所维护和提供的,但前两者算是彼此兼容的编译器:
4 s; x# u2 p# k% Y: w- S使用共同的 armlink8 D. P6 r& W5 ^8 i
使用相同的方式来描述地址空间布局(分散加载脚本 scatter script)/ L: l+ B4 R1 {1 \! e5 n k* X# H
从Arm Compiler 6.14开始,armclang甚至开始支持armasm的汇编语法了
" }! m2 u* S3 @% `& e: O
0 k2 P: y2 x0 _# w- m! ]实际上可以认为,armcc和armclang是一对连体兄弟,身子是armlink,而两个脑袋分别是 armcc 和 armclang。大约是这种感觉,你体会下。- S! ^0 \$ |* N# J5 d0 X2 G2 Z
" W- `, P' j3 I( y
uppozjgbkmb6401916833.jpg
5 l2 l/ i+ |* t/ F! A, i
/ ^& L; _/ k, l% t$ e( K6 @
作为定心丸的结论是:8 \& U6 N$ e4 f
原来 Arm Compiler 5 项目下的所有库(*.lib)都可以在 Arm Compiler 6 下直接使用原来由 Arm Compiler 5 生成的对象文件(*.o)都可以在 Arm Compiler 6 下直接使用原来 Arm Compiler 5下所用到的“几乎所有” armlink 相关的特性都可以在 Arm Compiler 6 直接使用(因为基本就是同一个armlink,所以几乎不存在“移植”的说法)1 w7 _9 [% u6 J. \+ W+ x
当然,还是有一些特例的,比如 __attribute__((at(地址))) 语法,这个我们将出一个专题来介绍应对方式。 . `# @ ~: n( n. ^, d
7 R; V i1 F A
6 q9 z: K+ I' t t& O2、“偷懒是第一生产力”
/ ^4 A/ u8 X( O由于 Arm Compiler 6 脱胎于LLVM,因此在汇编语法上它也继承了 clang 的特性——使用 GNU Assembly Syntax,而非 Arm 此前一直尝试推广的 Unified Assembly Language(UAL)汇编语法。由于 Arm Compiler 5 一直使用的是 UAL 汇编语法,广大用户长时间来积累了大量使用该语法编写的 .s 文件。
1 K9 e1 S# _% x汇编原本就是个头疼的东西——不到万不得已谁写汇编啊?对很多项目来说,且不说汇编原本就是少数大牛才敢碰的东西——几乎就是“Golden Code(屎山)”的代名词,实际上,这些“历史尘埃”的作者可能早就已经离职了——就算你把本人找回来,恐怕很多时候连当事人自己也是狗咬刺猬无法下嘴了。) Z/ {/ X# k* X; @2 Q$ U
xtmcyhba0a46401916933.jpg
, r% B, ?; X* o( X5 t7 [4 O& g7 R6 I1 I& F
尽管 Arm 专门写了一个名为《Migrating from armasm to the armclang Integrated Assembler》的文档来“教大家做事”,但社区的反馈可想而知……
) M/ Z# m2 O( i( \( [
1 }) v+ F6 _8 A4 H! R
w2oqx0g12bf6401917033.jpg
' ]8 f- b! i0 O0 s# F文章链接如下:
+ j Q+ f% D2 m9 h u! r. h1 Yhttps://developer.arm.com/documentation/100068/0618/Migrating-from-armasm-to-the-armclang-Integrated-Assembler?lang=en0 N: r9 @9 d( U/ T, X7 j
在众多“我不想,你求我啊……”的声音中,Arm Compiler 6从 6.14版本开始,重新把 UAL 的支持加了回来,并在 MDK 中引入了这样一个选项:- L( b3 M( V! Y/ ^7 ~
& i \/ @, e/ \. c/ Z% K) E% }/ G* s5 E" y: @. ]( ]
l2nrkp5b04j6401917133.png
$ Q: h5 k! Y1 Y/ C这里几个选项的意义如下:9 p* p3 O1 U" @. N; P
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 汇编语法风格。* r" e/ f( k6 z2 C4 C! J
其实,这里 armclang 也是个二道贩子——它也是调用 armasm 来完成编译的,只不过在这之前,它会默认用C预编译器对汇编源代码进行预处理,换句话说,折磨armasm很多年的“如何在汇编代码中使用C语言宏和预处理”的问题,得到了根治——你可以大大方方的在汇编代码里用 #include、各类宏定义和 #if 了。
" J. h3 z- c! d3 q9 F6 iarmasm(Arm Syntax):直接使用 armasm 来编译汇编源代码。该选项对 老的 UAL 源代码文件兼容性最好。如果使用 armclang(Arm Syntax)遇到问题,不妨用这个选项来试一下——一般都可以顺利解决问题。% z+ j& V# c9 ^+ n" d1 n& _. H
4 Y( l+ z0 a/ f( o怎么样,不用修改屎山了,是不是如释重负?2 z3 [: q7 h4 O3 e* r6 U( ]
" X) p9 y/ l/ ]" w/ J7 r& K
3、在线汇编(Inline Assembly)和嵌入C代码的汇编(Embedded Assembly)
/ s+ A9 A/ X7 U, s& d- j无论你是否了解 Arm Compiler 5所支持的这两种在C语言中使用汇编的方法,也不用关心它们的区别,结论是——任何Arm Compiler 5下的C代码只要使用了上述两种方法之一,基本上就是“需要手工干预”的。* V! h) E8 A! ^# x" n B) W/ {! n; ^/ F
这里我给出一个万能药方:
9 a8 F# _* g3 e2 r1 P% X对这部分C源文件,请使用 armcc 编译,生成 .o 后扔到 Arm Compiler 6里直接参与链接即可。, ?' H5 D% Q) A8 h9 A
. O4 Y$ [8 B2 [/ P+ @( s. t
当然,如果你有兴趣依照前面文档里的介绍进行改写,我祝你好胃口。# B" \; c; e) e- ^' F
4r40sykik1c6401917234.png
+ _6 P2 r! r; q
" A4 L8 e. y- Q/ A, }. ~至于如何让改写后的C代码同时兼容 Arm Compiler 5 和 Arm Compiler 6,就离不开下面的内容了——它也是我们后续一系列差异化改造的基础。
5 W6 g5 P; A$ f( ?; a/ O! [$ `
% x7 \+ r- J7 m【如何检测编译器】* m$ j3 q, M9 ~% P
一般来说,当我们要对某一部分代码进行跨编译器移植的时候,当然可以按照新语法一改了之,但对很多人来说,老的编译器总是会让大家萌生一种说不上来的留念之情,* M) w# [8 s0 k; C
ipdrhszvkxo6401917334.jpg
# q' E1 F" g9 q, N继而抱有:
# [2 F9 k8 M) c6 T“我要让修改后的代码仍然兼容过去老编译器”;
) {( b: a# I1 M4 P. ]或是:( Y% ` Y, @6 X( u' O8 |- X
“老代码删除太可惜了,我要留下来,以后万一有用呢?”4 W5 C _; l7 d
这样的想法。我也是这么想的。% o/ D, b- Q4 w% {/ S% O
ilppkbm0qkl6401917434.gif
. f" b. L0 c" h要做到这一点,就绕不开一个核心问题:如何可靠的检测出当前编译器版本呢?" c I& F0 A* B
! B5 s# O8 t" l, E8 r; K一般来说,编译器的宏检测有两个思路:
2 V6 {9 V% s3 k b' C, s2 g5 M借助某一编译器独有的特征宏来判断编译器" A- u/ h m! C9 [% G* s
借助多个编译器共有但值不同的宏来判断
: C# I7 {$ z3 N8 s8 U. C2 k
" N: `! h$ A9 l4 f! U对于第一种思路,有两个比较有名的宏:__GNUC__ 和 __clang__ 。过去,很多人喜欢用下面的代码来判断编译环境是否是GCC或者CLANG:0 g, ?+ D6 D: \: c. {
#if defined(__GNUC__) /* 我觉得编译器gcc */#endif- W- c- ~4 M8 K% r
#if defined(__clang__) /* 我觉得编译器是 clang */#endif然而,遗憾的是,由于很多编译器都在某种程度上对 GCC 扩展提供支持,因而也会定义宏__GNUC__,比如 armcc、armclang、clang、IAR都定义了该宏……因此,它几乎失去了GCC特征宏的价值,退化为“当前编译器支持GCC扩展(但具体哪些GCC扩展,这就看我心情了)”的标志。其实 __clang__ 宏也是类似的情况,因为 armclang 也会定义该宏,毕竟Arm Compiler 6是从LLVM中派生而出的。4 v! N: [0 S3 Z- r* z
当然,更为常见和有用的编译器特征宏是 __IAR_SYSTEMS_ICC__ ,借助它的帮助,我们可以判断当前开发环境是否为 IAR://! * z! X ^! C( W' o8 c1 U+ j; L
ote for IAR#undef __IS_COMPILER_IAR__#if defined(__IAR_SYSTEMS_ICC__)# define __IS_COMPILER_IAR__ 1#endif
, T2 q6 o R8 T' uArm Compiler 5 和 Arm Compiler 6 都是 Arm Compiler,区别它们二者有很多方法,但官方推荐的方法是判断宏 __ARMCC_VERSION 的值。从名字上就可以看出,这是一个自 armcc 以来一直延续到 armclang 的共有宏,它保存了编译器的版本,因此我们很容易编写出如下的宏:
( |# | s/ Q$ C" y& {2 I, W6 _//! ( j3 _. x: v& I" P' I
ote for arm compiler 5#undef __IS_COMPILER_ARM_COMPILER_5__#if ((__ARMCC_VERSION >= 5000000) && (__ARMCC_VERSION # define __IS_COMPILER_ARM_COMPILER_5__ 1#endif//! @}
J f1 z* B" a4 q//!
2 a! a( l. ^( Fote for arm compiler 6
0 R W9 _: }, h& E$ c# s#undef __IS_COMPILER_ARM_COMPILER_6__#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)# define __IS_COMPILER_ARM_COMPILER_6__ 1#endif& f. d2 I% [1 i
#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__; ~' M, i$ X+ t0 G6 {1 ?; O
# define __IS_COMPILER_ARM_COMPILER__ 1. p4 Z: f4 S q w. S9 K, t6 C; K
#endif借助它们的帮助,我们可以很容易的通过判断 __IS_COMPILER_ARM_COMPILER_5__ 和 __IS_COMPILER_ARM_COMPILER_6__ 的值是否为“1”来确定当前的编译器版本。在只关心当前编译器是否为Arm Compiler,而不在乎它具体是哪个版本时,可以借助 __IS_COMPILER_ARM_COMPILER__ 来进行判断。1 K5 u5 L$ g% P$ x
7 Z" l4 e' M/ W6 L d
假设我们的代码只考虑支持 gcc、clang、iar、armcc和armclang,那么利用排除法,我们就可以轻松的判断当前编译环境是否是 GCC 或 LLVM了:
* r+ G% Y# i# |5 o6 S% ?- D#undef __IS_COMPILER_LLVM__#if defined(__clang__) && !__IS_COMPILER_ARM_COMPILER_6__# define __IS_COMPILER_LLVM__ 1#else//! 8 V: S2 k* p" y7 `" E. e9 {
ote 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简单说一下这里的思路:: x" i. H: X }' Y* L4 K% M9 X
1、在排除了 Arm Compiler 6 的前提下,根据 __clang__ 来判断当前编译器是否为 LLVM(即:__IS_COMPILER_LLVM__);9 \. t: d- M9 k8 \9 |
2、在排除了 LLVM、Arm Compiler 和IAR的前提下,根据 __GNUC__ 来判断当前编译器是否为 GCC
8 T5 k# v( Q3 J( N- `& c( a5 m' L/ O+ Z7 g' e
为了方便大家理解,下面介绍几个上述宏的应用场景:7 t( V9 c6 o) U' q5 y
: [' B& i4 @& |! G' j* g如何在 Arm Compiler 6 下告知编译器 main() 函数不带输入参数
& t+ b, G# f% t' O. X默认情况下(使用默认的 libc),Arm Compiler 6会认为 main() 函数是带有标准的输入参数的:
4 ?6 W' T. U6 rint main (int argc, char *argv[]);哪怕你强行把 main() 函数写成无需输入参数的情况,编译器也还是会准备好参数——而准备参数的过程很有可能会导致 hardfault(这里会涉及到semihosting的问题,比较头疼,暂时不表)。为了解决这一问题,我们一般这么做:
9 N, y/ |5 u F) ?( s#if __IS_COMPILER_ARM_COMPILER_6____asm(".global __ARM_use_no_argv- F6 Q9 i' B1 r3 l& H
");#endif又因为 MicroLib 不存在该问题,因为我们可以根据(MDK会追加的一个宏)__MICROLIB,来做一个小小的区分:
: \" o! v" y" E- V& Q. S7 l) c#if __IS_COMPILER_ARM_COMPILER_6__# ifndef __MICROLIB__asm(".global __ARM_use_no_argv9 e; z7 P9 _- W
");# endif#endif也就是当且仅当我们使用 Arm Compiler 6,且不使用MicroLib的时候,通过专门的语法结构来告诉编译器:main() 函数没有传入参数。6 q7 P1 I: W& P" }" P
' {1 n% C- Q1 d- S7 v
如何关闭 Semihosting
" Y$ j$ [6 M. s: S2 i' i* D8 d' B你有没有遇到过这样神奇的情景:在调试模式下,程序可以正常运行;一旦退出调试模式,系统就死机了,重新进入调试模式后,发现系统进入了Hardfault。恭喜你,这很可能就是(默认开启的)semihosting 在作怪。关于Semihosting的内容,篇幅过大,不在本文讨论之列。今天我们只介绍一下如何关闭它。
/ h3 \ ~' b* G: qArm Compiler 5和Arm Compiler 6关闭 Semihosting的方法是不同的:
' D! W# R. X* t3 K#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 就可能会报告类似如下的错误:
& k, h- {, u/ A( H! N0 p2 a. WError: L6915E: Library reports error: __use_no_semihosting was requested, but _sys_exit was referenced简单解释下原因:Arm Compiler 6 依赖的一个函数 _sys_exit() 原本是用Semihosting方式默认提供的,现在你把 Semihosting 关闭了,所以你要负责到底。知道了原因,解决方法也很简单——缺这个函数,我们提供一个就行:3 D+ y) T# `/ R6 p7 H
#if __IS_COMPILER_ARM_COMPILER_6__void _sys_exit(int ret){ (void)ret; while(1) {}}#endif类似的情况还会发生在一个叫 _ttywrch() 的函数上,我们可以如法炮制:2 B- ?4 r$ z2 [; c. n
/* 为 arm compiler 5 和 arm compiler 6 都添加这个空函数 */#if __IS_COMPILER_ARM_COMPILER__void _ttywrch(int ch){ ARM_2D_UNUSED(ch);}#endif
. n$ c% b* f s3 i- X# A如何解决使用 assert.h 引发的问题. A4 \3 R* F( u7 f) u: V* b" }; g2 V
很多代码都有使用 assert() 来截获错误的习惯,当我们使用 Arm Compiler 6 且开启 MicroLib的时候,由于 MicroLib并不提供对 assert() 底层函数的具体实现,当我们没有定义 NDEBUG 来关闭 assert() 时,会在链接阶段看到如下的编译错误:
" J# e- R1 X) J% m: Y* nError: L6218E: Undefined symbol __aeabi_assert (referred from main.o).知道原因后,解决也很简单:既然MicroLib没提供实现,我们就自己提供一个好了:
; `! O7 p1 l0 |#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" A/ [9 F: E" J, L3 W
既然上述这套 __IS_COMPILER_xxxx__ 这么好用,我们可以从哪里获得呢?' K8 M. F' ~( L
6 {: H/ x: D# s* K0 K! ~& M- b
s44opiegq2q6401917534.jpg
. b3 a1 u- d* m: b. X# S1 L }7 \; N3 L/ r3 u
目前已知的获取渠道包括但不限于:3 Z- T w5 h9 s1 S5 @
从本文抄下来包含获取perf_counter 并包含 perf_counter.h 在存在 arm-2d 的情况下,直接包含 arm_2d.h 或者 arm_2d_utils.h) e6 o a+ t8 h9 H+ v$ s
……6 o$ k! x3 D1 d
$ D( _, R0 O9 ~+ N【说在后面的话】
# m+ j8 m: Y* N. W4 _ A3 o我承认 Arm Compiler 5 迁移到 Arm Compiler 6 不是一个轻松的过程,但也绝非大家想象的那样痛苦,很多时候,也许只是在 MDK 中更换一个选项那么简单:% V9 B: H4 x! F1 E: W
. p7 z: R9 |: |1 b
p1k5nkmgcff6401917634.png
; q: z6 m% n: M! K+ \/ w% y' n, N
7 L! I9 m5 k* Y) T5 \4 E
不试一试怎么知道呢?* o2 c. M- n. }& u H* v" K( ^ ~
! G" r+ |, P2 a
whlx0xhdobm6401917734.jpg
3 [3 P |5 `$ V/ R& [
7 t+ S; r7 ^7 Y3 N. e& A8 E, c/ ?对主流芯片大厂,比如 ST和NXP来说,它们的库早就完成了对 Arm Compiler 6的支持,可以说如果你遇到编译器兼容问题,应该首先考虑下载最新版本的驱动库。
7 |& O6 H' T7 [2 x S$ n( j2 i本文介绍的方法,基本上可以应对常见的从Arm Compiler 5到 Arm Compiler 6可能遇到的问题。这当然不是一份万能的解药,对于一些特殊的情况,我们将在后续文章中进行专题讨论。' v" U; [& }! B2 u/ I8 a
最后附上各类编译器的下载链接:
- J1 b$ A) b: H; X0 B# pArm Compiler 5:https://developer.arm.com/downloads/-/legacy-compilers
: _' l" s: i5 N9 f& P2 d, m; ^Arm Compiler 6:https://developer.arm.com/downloads/-/arm-compiler-for-embedded# d; W5 q6 s5 v# ^8 F( j
Arm GCC:) J/ Y; K3 Q d% m: k
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/downloads
, I* |+ q, J; p" j. D F- o- V) `2 xLLVM Embedded ToolChain for Arm 14.0.0https://github.com/ARM-software/LLVM-embedded-toolchain-for-Arm/releases |
|