一、說在前面的話
時間大約在2015年,Arm第一次在 MDK 5.20 中引入了Arm Compiler6(那時候的版本是 6.9),正式拉開了Arm官方編譯器從第五版(armcc)到第六版(armclang)升級替換的序幕……
嵌入式行業(yè)的長尾效應(yīng)是及其突出的,且不說都2022年了還有很多人在堅持 MDK4,即便是從“Arm在2017年對外宣布停止維護(hù) Arm Compiler 5”算起,如今5年過去了,堅持使用 armcc 的用戶仍然不在少數(shù)。
Arm Compiler 5,也就是大家口中的 armcc,它很弱么?相對免費的工具鏈 arm gcc來說,它還是強(qiáng)很明顯;但你要說它非常能打么?作為一個“理論上”收費的編譯器,它甚至已經(jīng)全方位落后于最新發(fā)布的“免費開源”編譯器LLVM Embedded ToolChain For Arm 14.0.0(clang),更不用說現(xiàn)在的當(dāng)紅貴人Arm Compiler 6(armclang)了。
如果非要我給出一份“不負(fù)責(zé)任”的編譯器性能對比的話,這是獨屬于我的答案:
arm gcc
別問我為什么,問就是誰用誰知道。 如果不是因為產(chǎn)品存在 Golden Code(屎山),只要你選定了Arm Compiler 而不是IAR,既然橫豎要使用付費編譯器,為什么不用Arm例行維護(hù)(幾乎每半年不到就發(fā)布一個新版本)的Arm Compiler 6,而繼續(xù)死守Arm Compiler 5呢? 有的人說“Arm Compiler 6不如Arm Compiler 5”穩(wěn)定。這里給出我的幾個反駁的幾個理由,但我不指望能說服那些抱有這類想法的人:
Arm Compiler 5已經(jīng)停止維護(hù),Arm Compiler 6還在持續(xù)更新。沒有bug的編譯器是不存在的,一個生命周期已經(jīng)結(jié)束的編譯器就幾乎不在存在修復(fù)已有bug和未發(fā)現(xiàn)bug的可能性;而一個積極維護(hù)的編譯器則可以及時的將發(fā)現(xiàn)的問題進(jìn)行修復(fù);
Arm Compiler 5過去只有Arm維護(hù),而 Arm Compiler 6是基于LLVM(clang)的商業(yè)化改進(jìn)版,這里L(fēng)LVM是一個開源項目,由眾多的個人和商業(yè)組織共同維護(hù),參考過去gcc的成功——這么多“大聰明”在盯著的項目,即便發(fā)現(xiàn)錯誤,估計也是“分分鐘”就被拿去“邀功請賞”了吧?
雖然我在實際使用中抓到(報告并得到修復(fù))的Arm Compiler 6 bug的數(shù)量超過在座99%的人,但正因如此,我知道要遇到一個Arm Compiler 6的bug有多難——更多時候,其實是我們自己對編譯器理解不深刻,甚至是基于自己對C語法的錯誤認(rèn)知導(dǎo)致的“烏龍”。在我看來,與其懷疑Arm Compiler 6不穩(wěn)定,不如懷疑下自己對C語言的理解。
不要屈服于由“未知帶來的恐懼”,不要拿“污名化”當(dāng)做掩蓋自己“偷懶和無知”的遮羞布(對這句話感到憤怒的人,我送你一句:愛聽不聽,歡迎取關(guān),謝謝)。
看到這里,如果你決定繼續(xù)往下閱讀,我就假設(shè)你已經(jīng)有興趣去嘗試使用Arm Compiler 6 來逐步取代已有的 Arm Compiler 5了?;谶@一前提,我們將用隨后的一系列文章來介紹:
短期內(nèi):MDK 5.37 拋棄 armcc 的補(bǔ)救措施
中期:從 armcc 向 armclang 進(jìn)行過渡時期的一些快速應(yīng)對的方法
面對一些armcc 獨有的編譯器特性的應(yīng)對方法吧
二、臨時補(bǔ)救
雖然最新的 MDK拋棄了Arm Compiler 5,但它仍然允許我們通過手動添加的方法將其請回來,具體方法我在《驚爆內(nèi)幕:老MDK也可以使用新編譯器》文章中已經(jīng)詳細(xì)介紹過,這里就不再贅述,值得補(bǔ)充說明的是:
1、新MDK也可以手工添加老版本的編譯器,不要被文章的標(biāo)題限制住了思路
三、幾顆定心丸
1.“街頭霸王”
我們知道MDK是一個集成開發(fā)環(huán)境(Integrated Development Environment),它默認(rèn)原生支持Arm Compiler 5(armcc)、Arm Compiler 6(armclang)和 arm gcc。雖然這三個編譯器都是由Arm所維護(hù)和提供的,但前兩者算是彼此兼容的編譯器:
使用共同的armlink
使用相同的方式來描述地址空間布局(分散加載腳本 scatter script)
從Arm Compiler 6.14開始,armclang甚至開始支持armasm的匯編語法了
實際上可以認(rèn)為,armcc和armclang是一對連體兄弟,身子是armlink,而兩個腦袋分別是 armcc 和 armclang。大約是這種感覺,你體會下。
作為定心丸的結(jié)論是:
原來Arm Compiler 5 項目下的所有庫(*.lib)都可以在 Arm Compiler 6下直接使用
原來由 Arm Compiler 5 生成的對象文件(*.o)都可以在 Arm Compiler 6下直接使用
原來 Arm Compiler 5下所用到的“幾乎所有” armlink 相關(guān)的特性都可以在 Arm Compiler 6 直接使用(因為基本就是同一個armlink,所以幾乎不存在“移植”的說法)
當(dāng)然,還是有一些特例的,比如 __attribute__((at(地址))) 語法,這個我們將出一個專題來介紹應(yīng)對方式。
2.“偷懶是第一生產(chǎn)力”
由于 Arm Compiler 6 脫胎于LLVM,因此在匯編語法上它也繼承了 clang 的特性——使用 GNU Assembly Syntax,而非 Arm 此前一直嘗試推廣的 Unified Assembly Language(UAL)匯編語法。
由于 Arm Compiler 5 一直使用的是 UAL 匯編語法,廣大用戶長時間來積累了大量使用該語法編寫的 .s 文件。
匯編原本就是個頭疼的東西——不到萬不得已誰寫匯編啊?對很多項目來說,且不說匯編原本就是少數(shù)大牛才敢碰的東西——幾乎就是“Golden Code(屎山)”的代名詞,實際上,這些“歷史塵?!钡淖髡呖赡茉缇鸵呀?jīng)離職了——就算你把本人找回來,恐怕很多時候連當(dāng)事人自己也是狗咬刺猬無法下嘴了。
盡管 Arm 專門寫了一個名為《Migrating from armasm to the armclang Integrated Assembler》的文檔來“教大家做事”,但社區(qū)的反饋可想而知……
在眾多“我不想,你求我啊……”的聲音中,Arm Compiler 6從 6.14版本開始,重新把 UAL 的支持加了回來,并在 MDK 中引入了這樣一個選項:
這里幾個選項的意義如下:
armclang(Auto Select):使用 armclang 來編譯匯編源代碼(對應(yīng)命令行選項-masm=auto),然后armclang會根據(jù)語法風(fēng)格自動決定是當(dāng)做 GNU Assembly Syntax 來處理,還是使用 UAL 語法來解析。我吐血推薦使用這個選項。
armclang (GNU Syntax):使用armclang來編譯匯編源代碼(對應(yīng)命令行選項-masm=gnu),然后強(qiáng)制使用 GNU 匯編語法風(fēng)格。
armclang(Arm Syntax):使用armclang來編譯匯編源代碼(對應(yīng)命令行選項-masm=armasm),然后強(qiáng)制使用 UAL 匯編語法風(fēng)格。
其實,這里armclang也是個二道販子——它也是調(diào)用armasm來完成編譯的,只不過在這之前,它會默認(rèn)用C預(yù)編譯器對匯編源代碼進(jìn)行預(yù)處理,換句話說,折磨armasm很多年的“如何在匯編代碼中使用C語言宏和預(yù)處理”的問題,得到了根治——你可以大大方方的在匯編代碼里用 #include、各類宏定義和 #if 了。
armasm(Arm Syntax):直接使用armasm來編譯匯編源代碼。該選項對 老的 UAL 源代碼文件兼容性最好。如果使用armclang(Arm Syntax)遇到問題,不妨用這個選項來試一下——一般都可以順利解決問題。
怎么樣,不用修改屎山了,是不是如釋重負(fù)?
3.在線匯編(InlineAssembly)和嵌入C代碼的匯編(Embedded Assembly)
無論你是否了解 Arm Compiler 5所支持的這兩種在C語言中使用匯編的方法,也不用關(guān)心它們的區(qū)別,結(jié)論是——任何Arm Compiler 5下的C代碼只要使用了上述兩種方法之一,基本上就是“需要手工干預(yù)”的。 這里我給出一個萬能藥方: 對這部分C源文件,請使用 armcc 編譯,生成 .o 后扔到 Arm Compiler 6里直接參與鏈接即可。 當(dāng)然,如果你有興趣依照前面文檔里的介紹進(jìn)行改寫,我祝你好胃口。
至于如何讓改寫后的C代碼同時兼容 Arm Compiler 5 和 Arm Compiler 6,就離不開下面的內(nèi)容了——它也是我們后續(xù)一系列差異化改造的基礎(chǔ)。
四、如何檢測編譯器
一般來說,當(dāng)我們要對某一部分代碼進(jìn)行跨編譯器移植的時候,當(dāng)然可以按照新語法一改了之,但對很多人來說,老的編譯器總是會讓大家萌生一種說不上來的留念之情,繼而抱有:
“我要讓修改后的代碼仍然兼容過去老編譯器”; 或是: “老代碼刪除太可惜了,我要留下來,以后萬一有用呢?” 這樣的想法。我也是這么想的。
要做到這一點,就繞不開一個核心問題:如何可靠的檢測出當(dāng)前編譯器版本呢?
一般來說,編譯器的宏檢測有兩個思路:
借助某一編譯器獨有的特征宏來判斷編譯器
借助多個編譯器共有但值不同的宏來判斷
對于第一種思路,有兩個比較有名的宏:__GNUC__和__clang__。過去,很多人喜歡用下面的代碼來判斷編譯環(huán)境是否是GCC或者CLANG:
#if defined(__GNUC__) /* 我覺得編譯器gcc */ #endif #ifdefined(__clang__) /*我覺得編譯器是 clang */ #endif然而,遺憾的是,由于很多編譯器都在某種程度上對 GCC 擴(kuò)展提供支持,因而也會定義宏__GNUC__,比如 armcc、armclang、clang、IAR都定義了該宏……因此,它幾乎失去了GCC特征宏的價值,退化為“當(dāng)前編譯器支持GCC擴(kuò)展(但具體哪些GCC擴(kuò)展,這就看我心情了)”的標(biāo)志。
其實__clang__ 宏也是類似的情況,因為 armclang 也會定義該宏,畢竟Arm Compiler 6是從LLVM中派生而出的。
當(dāng)然,更為常見和有用的編譯器特征宏是 __IAR_SYSTEMS_ICC__ ,借助它的幫助,我們可以判斷當(dāng)前開發(fā)環(huán)境是否為 IAR:
//! ote for IAR #undef __IS_COMPILER_IAR__ #if defined(__IAR_SYSTEMS_ICC__) # define __IS_COMPILER_IAR__ 1 #endifArm Compiler 5 和 Arm Compiler 6 都是 Arm Compiler,區(qū)別它們二者有很多方法,但官方推薦的方法是判斷宏__ARMCC_VERSION 的值。
從名字上就可以看出,這是一個自 armcc 以來一直延續(xù)到 armclang 的共有宏,它保存了編譯器的版本,因此我們很容易編寫出如下的宏:
//! ote 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 //! @} //! ote 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”來確定當(dāng)前的編譯器版本。
在只關(guān)心當(dāng)前編譯器是否為Arm Compiler,而不在乎它具體是哪個版本時,可以借助 __IS_COMPILER_ARM_COMPILER__ 來進(jìn)行判斷。
假設(shè)我們的代碼只考慮支持 gcc、clang、iar、armcc和armclang,那么利用排除法,我們就可以輕松的判斷當(dāng)前編譯環(huán)境是否是 GCC 或 LLVM了:
#undef __IS_COMPILER_LLVM__ #if defined(__clang__) && !__IS_COMPILER_ARM_COMPILER_6__ # define __IS_COMPILER_LLVM__ 1 #else //! 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
簡單說一下這里的思路:
1、在排除了 Arm Compiler 6 的前提下,根據(jù) __clang__來判斷當(dāng)前編譯器是否為 LLVM(即:__IS_COMPILER_LLVM__);
2、在排除了 LLVM、Arm Compiler和IAR的前提下,根據(jù) __GNUC__ 來判斷當(dāng)前編譯器是否為GCC
為了方便大家理解,下面介紹幾個上述宏的應(yīng)用場景:
如何在 Arm Compiler 6下告知編譯器 main() 函數(shù)不帶輸入參數(shù)
默認(rèn)情況下(使用默認(rèn)的 libc),Arm Compiler 6會認(rèn)為 main() 函數(shù)是帶有標(biāo)準(zhǔn)的輸入?yún)?shù)的:
int main (int argc, char *argv[]);
哪怕你強(qiáng)行把 main() 函數(shù)寫成無需輸入?yún)?shù)的情況,編譯器也還是會準(zhǔn)備好參數(shù)——而準(zhǔn)備參數(shù)的過程很有可能會導(dǎo)致 hardfault(這里會涉及到semihosting的問題,比較頭疼,暫時不表)。
為了解決這一問題,我們一般這么做:
#if__IS_COMPILER_ARM_COMPILER_6__ __asm(".global__ARM_use_no_argv "); #endif
又因為 MicroLib 不存在該問題,因為我們可以根據(jù)(MDK會追加的一個宏)__MICROLIB,來做一個小小的區(qū)分:
#if__IS_COMPILER_ARM_COMPILER_6__ # ifndef __MICROLIB __asm(".global __ARM_use_no_argv "); #endif #endif
也就是當(dāng)且僅當(dāng)我們使用 Arm Compiler 6,且不使用MicroLib的時候,通過專門的語法結(jié)構(gòu)來告訴編譯器:main() 函數(shù)沒有傳入?yún)?shù)。
如何關(guān)閉 Semihosting
你有沒有遇到過這樣神奇的情景:在調(diào)試模式下,程序可以正常運(yùn)行;一旦退出調(diào)試模式,系統(tǒng)就死機(jī)了,重新進(jìn)入調(diào)試模式后,發(fā)現(xiàn)系統(tǒng)進(jìn)入了Hardfault。
恭喜你,這很可能就是(默認(rèn)開啟的)semihosting 在作怪。關(guān)于Semihosting的內(nèi)容,篇幅過大,不在本文討論之列。
今天我們只介紹一下如何關(guān)閉它。
Arm Compiler 5和Arm Compiler 6關(guān)閉 Semihosting的方法是不同的:
#if __IS_COMPILER_ARM_COMPILER_6__ __asm(".global __use_no_semihosting"); #elif __IS_COMPILER_ARM_COMPILER_5__ #pragmaimport(__use_no_semihosting) #endif一旦關(guān)閉了 Semihosting,Arm Compiler 6 就可能會報告類似如下的錯誤:
Error:L6915E:Libraryreportserror:__use_no_semihostingwasrequested,but_sys_exitwasreferenced簡單解釋下原因:Arm Compiler 6 依賴的一個函數(shù) _sys_exit()原本是用Semihosting方式默認(rèn)提供的,現(xiàn)在你把 Semihosting 關(guān)閉了,所以你要負(fù)責(zé)到底。
知道了原因,解決方法也很簡單——缺這個函數(shù),我們提供一個就行:
#if __IS_COMPILER_ARM_COMPILER_6__ void _sys_exit(int ret) { (void)ret; while(1) {} } #endif類似的情況還會發(fā)生在一個叫 _ttywrch() 的函數(shù)上,我們可以如法炮制:
/*為armcompiler5 和 arm compiler 6 都添加這個空函數(shù) */ #if__IS_COMPILER_ARM_COMPILER__ void _ttywrch(int ch) { ARM_2D_UNUSED(ch); } #endif
如何解決使用 assert.h引發(fā)的問題
很多代碼都有使用 assert() 來截獲錯誤的習(xí)慣,當(dāng)我們使用 Arm Compiler 6 且開啟 MicroLib的時候,由于 MicroLib并不提供對 assert() 底層函數(shù)的具體實現(xiàn),當(dāng)我們沒有定義 NDEBUG 來關(guān)閉 assert() 時,會在鏈接階段看到如下的編譯錯誤:
Error: L6218E: Undefined symbol __aeabi_assert (referred from main.o).
知道原因后,解決也很簡單:既然MicroLib沒提供實現(xiàn),我們就自己提供一個好了:
#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
……
五、說在后面的話
我承認(rèn)Arm Compiler 5遷移到Arm Compiler 6不是一個輕松的過程,但也絕非大家想象的那樣痛苦,很多時候,也許只是在MDK中更換一個選項那么簡單:
不試一試怎么知道呢?
對主流芯片大廠,比如 ST和NXP來說,它們的庫早就完成了對 Arm Compiler 6的支持,可以說如果你遇到編譯器兼容問題,應(yīng)該首先考慮下載最新版本的驅(qū)動庫。
本文介紹的方法,基本上可以應(yīng)對常見的從Arm Compiler 5到 Arm Compiler 6可能遇到的問題。
這當(dāng)然不是一份萬能的解藥,對于一些特殊的情況,我們將在后續(xù)文章中進(jìn)行專題討論。
審核編輯:劉清
-
ARM
+關(guān)注
關(guān)注
134文章
9030瀏覽量
366548 -
GCC
+關(guān)注
關(guān)注
0文章
105瀏覽量
24807 -
編譯器
+關(guān)注
關(guān)注
1文章
1617瀏覽量
49019 -
MDK
+關(guān)注
關(guān)注
4文章
208瀏覽量
32015 -
GNU
+關(guān)注
關(guān)注
0文章
143瀏覽量
17468
原文標(biāo)題:【反復(fù)橫跳】從AC5到AC6轉(zhuǎn)型之路(1)——補(bǔ)救和準(zhǔn)備
文章出處:【微信號:Ithingedu,微信公眾號:安芯教育科技】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論