【教程】如何用GCC“零匯編”白嫖MDK
【說(shuō)在前面的話】其實(shí)我很久之前就想寫(xiě)這篇文章了,但彼時(shí)總覺(jué)得這是一個(gè)偽命題:
- 既然已經(jīng)用了MDK,編譯出來(lái)的代碼,無(wú)論是體積還是性能都甩下arm gcc好幾條街,誰(shuí)還會(huì)想用gcc來(lái)進(jìn)行Cortex-M開(kāi)發(fā)呢?
- 對(duì)那些只能使用arm gcc、或者對(duì)gcc情有獨(dú)鐘的小伙伴來(lái)說(shuō),無(wú)論是配合eclipse、vscode、Embedded Studio還是其它什么開(kāi)發(fā)環(huán)境,哪個(gè)不比MDK香呢?
然而,既然你點(diǎn)開(kāi)了這篇文章,無(wú)論是否真的有這樣的需求,至少說(shuō)明你對(duì)這樣的搭配還是“頗有些好奇”的。我就不去擔(dān)心背后的真正原因了,就讓我們速速切入正題,進(jìn)入實(shí)操環(huán)節(jié)吧。
先說(shuō)結(jié)論:
- MDK原生支持GCC開(kāi)發(fā),且不受License限制
- MDK使用GCC開(kāi)發(fā)時(shí)“可以做到”不寫(xiě)一句匯編的程度
- MDK使用GCC開(kāi)發(fā)時(shí)可以享受來(lái)自Runtime Environment配置機(jī)制的福利——也就是你可以輕松的享用來(lái)自Pack Installer所引入的各類(lèi)軟件包的支持——這同樣也是免費(fèi)的
- MDK使用GCC開(kāi)發(fā)時(shí)支持調(diào)試(所能調(diào)試的代碼尺寸受到License限制)
我們知道MDK是一個(gè)集成開(kāi)發(fā)環(huán)境(Integrated Development Environment),它默認(rèn)原生支持Arm Compiler 5(armcc)、Arm Compiler 6(armclang)和 arm gcc。雖然這三個(gè)編譯器都是由Arm所維護(hù)和提供的,但前兩者算是彼此兼容的編譯器:
- 使用共同的?armlink
- 使用相同的方式來(lái)描述地址空間布局(分散加載腳本 scatter script)
- 從Arm Compiler 6.14開(kāi)始,armclang甚至開(kāi)始支持armasm的匯編語(yǔ)法了
實(shí)際上可以認(rèn)為,armcc和armclang是一對(duì)連體兄弟,身子是armlink,而兩個(gè)腦袋分別是 armcc 和 armclang。大約是這種感覺(jué),你體會(huì)下。
與親生的兩兄弟不同,牛頭人arm gcc是Arm公司從GCC開(kāi)源社區(qū)“抱回來(lái)的孩子”。它雖然語(yǔ)法上與armclang(clang)基本相同,但卻擁有自己獨(dú)立的編譯和連接環(huán)節(jié),用來(lái)描述地址空間布局的方式也完全不同——采用 linker script(*.ld)來(lái)進(jìn)行。
那么這些差異對(duì)我們?cè)?strong>MDK中使用gcc進(jìn)行開(kāi)發(fā)有什么意義呢?我們需要做哪些工作準(zhǔn)備工作呢?總的來(lái)說(shuō),問(wèn)題集中在以下幾個(gè)方面:
- 編譯器的獲取和集成
- 如何芯片的啟動(dòng)
- 如何描述目標(biāo)軟件的地址空間布局
- 如何對(duì)編譯選項(xiàng)進(jìn)行配置
- 如何進(jìn)行代碼的優(yōu)化
接下來(lái),我們就有針對(duì)性的為您解答這些問(wèn)題。
【如何在將arm gcc集成到MDK環(huán)境中】
arm gcc 獲取并不困難,可以訪問(wèn)arm的官方頁(yè)面直接下載:
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm
下載后一路無(wú)腦安裝即可,這里就不再贅述。接下來(lái),我們打開(kāi)MDK,通過(guò)菜單?project->New uVision Project... 新建一個(gè)工程:
為了方便,工程文件名不妨就叫 gcc_template好了:
單擊?"Save" 后,MDK會(huì)彈出窗口讓我們選擇工程的目標(biāo)芯片,實(shí)際上很多芯片公司都為MDK提供了面向gcc的工程模板,因此在這里直接選擇實(shí)際芯片型號(hào)往往就可以省略后面大部分步驟,但考慮到讓本教程擁有更強(qiáng)的通用性,這里我們選擇目標(biāo)芯片所使用的處理器:
假設(shè),我們要使用的芯片是STM32F746,我們知道它的內(nèi)核是Cortex-M7,因此這里就選擇 Arm->ARM Cortex-M7->ARMCM7_SP(假設(shè)是單精度浮點(diǎn)運(yùn)算單元),單擊OK。
對(duì)這里選什么芯片比較糾結(jié)的小伙伴大可不必,因?yàn)楹竺骐S時(shí)可以回來(lái)改,不會(huì)存在那種“買(mǎi)定離手”而“無(wú)法反悔”的問(wèn)題。
接下來(lái),MDK會(huì)彈出RTE的配置界面。RTE的配置我們將在后面介紹,此時(shí)直接單擊OK進(jìn)行跳過(guò)即可。
如果一切順利,你會(huì)看到如下的界面:
以上步驟只能算是準(zhǔn)備工作,接下來(lái)才是將arm gcc集成到MDK中的正題。依次通過(guò)菜單 Project -> Manage -> Project Items 打開(kāi)配置窗體:
在新打開(kāi)的對(duì)話框中選擇 "Folders/Extensions" 選項(xiàng)卡,并勾選“Use GCC Compiler (GNU)for ARM projects”(如下圖所示):單擊 “...” 按鈕,選擇arm gcc工具鏈所在的安裝目錄。以最新的的arm gcc 2020-q4-major 版本為例,默認(rèn)情況下它會(huì)被安裝在?
“C:\Program Files (x86)\GNU Arm Embedded Toolchain”
目錄下。我們選中這里的 "10 2020-q4-major" 目錄,單擊 Select Folder 按鈕。
在回到上一級(jí)窗口時(shí),我們注意到,此時(shí)arm gcc的路徑已經(jīng)被正確配置了:
單擊“OK”就完成了 arm gcc 的添加工作。此時(shí),如果打開(kāi) Project ->?Options for Target 窗口,我們會(huì)看到編譯器配置界面變成了一個(gè)陌生的樣子:如果你看到類(lèi)似這樣的界面,恭喜您,您的MDK已經(jīng)和arm gcc“喜結(jié)連理”了。
【實(shí)現(xiàn)“無(wú)匯編化”的啟動(dòng)】很多人可能都有錯(cuò)覺(jué)——以為使用gcc開(kāi)發(fā)項(xiàng)目一定要用匯編的方式來(lái)處理啟動(dòng)文件——過(guò)去也許是這樣,但是,“大人時(shí)代變了”!。
借助 CMSIS的幫助,我們現(xiàn)在也可以優(yōu)雅的完全使用C語(yǔ)言來(lái)實(shí)現(xiàn)芯片的啟動(dòng)過(guò)程。首先,我們需要獲得最新的CMSIS,具體方法可以在這篇文章《CMSIS玩家的“陰間成就”指南》中獲得,這里就不在贅述。
無(wú)論是通過(guò)Pack安裝還是github導(dǎo)入,在確保最新的CMSIS被成功的安裝到MDK中以后,我們首先需要在工程中通過(guò)RTE窗口引入最新的CMSIS支持:在工具欄中,單擊下面的按鈕:
?
打開(kāi)?Runtime Environment?配置窗口:
這里,我們展開(kāi)CMSIS,并勾選?CORE(這里,請(qǐng)確保CORE的版本不低于?5.4.0),單擊OK確認(rèn)配置。
如果你對(duì)CMSIS的版本有所疑問(wèn),可以單擊 “Select Packs” 按鈕,確保窗體頂端的 “Use latest versions of all installed Software Packs” 被勾選,如果這樣做以后,CMSIS-CORE的版本仍然低于 5.4.0,請(qǐng)務(wù)必參考這篇文章《CMSIS玩家的“陰間成就”指南》來(lái)獲取最新的CMSIS。
單擊CMSIS-CORE后面的注釋文字:
會(huì)打開(kāi)一個(gè)瀏覽器頁(yè)面,忽略其中的內(nèi)容,我們需要的是頁(yè)面網(wǎng)址中的路徑信息:
這里,我們找到了當(dāng)前CMSIS Pack在本地的路徑,利用這一路徑信息在瀏覽器中打開(kāi)對(duì)應(yīng)文件夾,找到 Device目錄:
依次進(jìn)入目錄?“Device\ARM\ARMCM7\Source”:
將上圖選中的文件拷貝到我們的工程中來(lái):
在MDK工程中,將startup_ARMCM7.c和system_ARMCM7.c加入到工程中參與編譯(這里我們新建了一個(gè)分組叫做 low_level):
先別著急去編譯,注意到這里的小鑰匙圖標(biāo)了么?這說(shuō)明這兩個(gè)文件自帶了“只讀屬性”。由于我們后面要修改這兩個(gè)文件,因此必須要通過(guò)Windows的文件屬性管理將只讀屬性去除(把下圖的勾選去掉后單擊OK):此時(shí)再看MDK的工程管理器,小鑰匙標(biāo)志就已經(jīng)消失了:
接下來(lái),打開(kāi)?“Option for Target...” 窗體,進(jìn)入Linker選項(xiàng)卡:
將這里的 "Do not use Standard System Startup Files" 選項(xiàng)去除。
注意,這一步驟非常重要,不可以省略,否則你會(huì)看到如下的編譯錯(cuò)誤:
linking...
c:/program files (x86)/gnu arm embedded toolchain/10 2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld.exe: warning: cannot find entry symbol _start; defaulting to 00008000
c:/program files (x86)/gnu arm embedded toolchain/10 2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld.exe: ./startup_armcm7.o: in function `__cmsis_start':
C:/Users/gabriel/AppData/Local/Arm/Packs/ARM/CMSIS/5.8.0/CMSIS/Core/Include/cmsis_gcc.h:163: undefined reference to `_start'
c:/program files (x86)/gnu arm embedded toolchain/10 2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/gabriel/AppData/Local/Arm/Packs/ARM/CMSIS/5.8.0/CMSIS/Core/Include/cmsis_gcc.h:163: undefined reference to `__copy_table_start__'
c:/program files (x86)/gnu arm embedded toolchain/10 2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/gabriel/AppData/Local/Arm/Packs/ARM/CMSIS/5.8.0/CMSIS/Core/Include/cmsis_gcc.h:163: undefined reference to `__copy_table_end__'
c:/program files (x86)/gnu arm embedded toolchain/10 2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/gabriel/AppData/Local/Arm/Packs/ARM/CMSIS/5.8.0/CMSIS/Core/Include/cmsis_gcc.h:163: undefined reference to `__zero_table_start__'
c:/program files (x86)/gnu arm embedded toolchain/10 2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/gabriel/AppData/Local/Arm/Packs/ARM/CMSIS/5.8.0/CMSIS/Core/Include/cmsis_gcc.h:163: undefined reference to `__zero_table_end__'
c:/program files (x86)/gnu arm embedded toolchain/10 2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld.exe: ./startup_armcm7.o:E:\Temp Project\gcc_template/startup_ARMCM7.c:84: undefined reference to `__StackTop'
collect2.exe: error: ld returned 1 exit status
".\gcc_template.elf" - 1 Error(s), 0 Warning(s).
正如錯(cuò)誤提示中指出的那樣,CMSIS會(huì)在一個(gè)叫做 __cmsis_start的函數(shù)中,調(diào)用 "_start"?函數(shù),而這一函數(shù)正是gcc標(biāo)準(zhǔn)啟動(dòng)文件的入口,當(dāng)你在MDK中選擇"Do not use Standard System Startup Files"?時(shí),linker自然就找不到這個(gè)“不存在”的入口函數(shù)啦。接下來(lái),單擊如下圖所示的按鈕:
打開(kāi)我們剛剛一起拷貝過(guò)來(lái)的GCC目錄,選中其中的連接腳本 gcc_arm.ld后,單擊Open:最后的結(jié)果如下圖所示,單擊OK確認(rèn)我們的配置:
雖然不是必須,但推薦在Misc controls中添加如下的內(nèi)容:
--specs=nosys.specs?-Wl,--gc-sections
-fshort-enums?-fshort-wchar
即:接下來(lái),為了初步檢驗(yàn)一下我們的成果,在工程中添加一個(gè)main.c(實(shí)現(xiàn)一個(gè)簡(jiǎn)單的main() 函數(shù)):懷著忐忑的心理,按下編譯按鈕:
不用懷疑,我們已經(jīng)成功的實(shí)現(xiàn)了“零匯編”gcc工程建立。簡(jiǎn)單不?你可以把這個(gè)工程連同文件夾一起保存好,這就是未來(lái)的工程模板了。此外,關(guān)于main.c中的代碼,需要做一些簡(jiǎn)單的說(shuō)明:
#include
#include
#include
#include "cmsis_compiler.h"
int main(void)
{
while(1) {
}
return 0;
}
__attribute__((noreturn))
void exit(int err_code) {
while(1) {
__NOP();
}
}
- GCC要求main函數(shù)的返回值是 int 類(lèi)型,而這里的返回值會(huì)被作為 exit() 函數(shù)的傳入?yún)?shù)——一般負(fù)數(shù)表示出錯(cuò),0表示平安。
- 如果不實(shí)現(xiàn)一個(gè) exit()?函數(shù),鏈接器會(huì)報(bào)錯(cuò)。
- __attribute__((noreturn)) 就是字面意思,告訴編譯器這個(gè)這個(gè)函數(shù)是有去無(wú)回的。
- 為了使用類(lèi)似 __NOP() 這樣的“固有函數(shù)(intrinsics)”,我們需要直接或者間接的包含頭文件? "cmsis_compiler.h"
此外,如果我們不做任何的設(shè)置,MDK會(huì)將所有生成的中間文件(比如 .o、.d之類(lèi))直接保存到工程文件夾下,產(chǎn)生“垃圾遍布”的感覺(jué):為了解決這一問(wèn)題,我們可以在"Options for Target"窗口的Target選項(xiàng)卡中通過(guò)“Select Folder for Objects” 來(lái)選擇一個(gè)專(zhuān)門(mén)的文件夾放置這些中間文件:完成基礎(chǔ)模板的制作后,接下來(lái)我們來(lái)一一介紹一些模板在使用過(guò)程中所需要處理的細(xì)節(jié)問(wèn)題:
【簡(jiǎn)單的地址空間布局、Stack和Heap的配置】在去掉 GCC/gcc_arm.ld 文件的只讀屬性后,我們就可以借助它根據(jù)目標(biāo)芯片的實(shí)際情況描述地址空間布局,打開(kāi)gcc_arm.ld,可以看到如下的內(nèi)容:
如果你的目標(biāo)芯片較為簡(jiǎn)單,比如,F(xiàn)LASH是一片完整的地址區(qū)間,則可以通過(guò)修改__ROM_BASE的方式來(lái)設(shè)置目標(biāo)鏡像中FLASH的起始地址,通過(guò)修改修改__ROM_SIZE來(lái)設(shè)置FLASH的實(shí)際大小,比如,起始地址為0x0800-0000,大小為256K的Flash對(duì)應(yīng)的修改方式為:
/*---------------------- Flash Configuration ----------------------------------
Flash Configuration
Flash Base Address <0x0-0xFFFFFFFF:8>
Flash Size (in Bytes) <0x0-0xFFFFFFFF:8>
-----------------------------------------------------------------------------*/
__ROM_BASE = 0x08000000;
__ROM_SIZE = 0x00040000;
同理,SRAM的起始地址和大小可以通過(guò)__RAM_BASE和__RAM_SIZE來(lái)設(shè)置,這里就不再贅述:/*--------------------- Embedded RAM Configuration ----------------------------
RAM Configuration
RAM Base Address <0x0-0xFFFFFFFF:8>
RAM Size (in Bytes) <0x0-0xFFFFFFFF:8>
-----------------------------------------------------------------------------*/
__RAM_BASE = 0x20000000;
__RAM_SIZE = 0x00020000;
最后,關(guān)于Stack和Heap大小的設(shè)置可以借助__STACK_SIZE和__HEAP_SIZE來(lái)設(shè)置:/*--------------------- Stack / Heap Configuration ----------------------------
Stack / Heap Configuration
Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
-----------------------------------------------------------------------------*/
__STACK_SIZE = 0x00000800; /* 2K Byte */
__HEAP_SIZE??=?0x00000200;???/* 256 Byte */
【如何配置中斷向量表】不同的芯片擁有不同的中斷向量表,而此前我們所建立的gcc工程模板中,startup_ARMCM7.c 里定義的其實(shí)是一個(gè)默認(rèn)的中斷向量表:
可以看到,這一向量表完全采用的是C語(yǔ)言函數(shù)指針數(shù)組初始化的形式定義的。它不僅提供了默認(rèn)的各類(lèi)系統(tǒng)異常的定義,還以Interruptn_Handler的形式為我們提供了定義的范例。
更新這一文件的步驟并不復(fù)雜。實(shí)際上一般芯片公司都會(huì)提供符合CMSIS規(guī)范的芯片頭文件,這一頭文件中會(huì)提供對(duì)應(yīng)的中斷向量定義,比如STM32F746就有一個(gè)對(duì)應(yīng)的頭文件 STM32F746xx.h。將其打開(kāi)會(huì)看到專(zhuān)門(mén)的向量表定義:
/**
* @brief STM32F7xx Interrupt Number Definition, according to the selected device
* in @ref Library_configuration_section
*/
typedef enum
{
/****** Cortex-M7 Processor Exceptions Numbers ****************************************************************/
NonMaskableInt_IRQn = -14, /*!< 2 Non Maskable Interrupt */
MemoryManagement_IRQn = -12, /*!< 4 Cortex-M7 Memory Management Interrupt */
BusFault_IRQn = -11, /*!< 5 Cortex-M7 Bus Fault Interrupt */
UsageFault_IRQn = -10, /*!< 6 Cortex-M7 Usage Fault Interrupt */
SVCall_IRQn = -5, /*!< 11 Cortex-M7 SV Call Interrupt */
DebugMonitor_IRQn = -4, /*!< 12 Cortex-M7 Debug Monitor Interrupt */
PendSV_IRQn = -2, /*!< 14 Cortex-M7 Pend SV Interrupt */
SysTick_IRQn = -1, /*!< 15 Cortex-M7 System Tick Interrupt */
/****** STM32 specific Interrupt Numbers **********************************************************************/
WWDG_IRQn = 0, /*!< Window WatchDog Interrupt */
??...
SPDIF_RX_IRQn = 97, /*!< SPDIF-RX global Interrupt */
} IRQn_Type;
這里,WWDG_IRQn到SPDIF_RX_IRQn之間的每一項(xiàng)都對(duì)應(yīng)一個(gè)外設(shè)中斷,可以將它們拷貝出來(lái),添加到我們的startup_ARMCM7.c的向量表中,并依樣畫(huà)葫蘆,修改成對(duì)應(yīng)的形式:...
/*----------------------------------------------------------------------------
Exception / Interrupt Handler
*----------------------------------------------------------------------------*/
/* Exceptions */
void NMI_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
void HardFault_Handler (void) __attribute__ ((weak));
void MemManage_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
void BusFault_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
void UsageFault_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
void SVC_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
void DebugMon_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
void PendSV_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
void SysTick_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
/*
void Interrupt0_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
...
void Interrupt9_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
*/
void WWDG_IRQn_Handler (void) __attribute__ ((weak, alias("Default_Handler")));
...
void?SPDIF_RX_IRQn_Handler??(void)?__attribute__?((weak,?alias("Default_Handler")));
...
extern const VECTOR_TABLE_Type __VECTOR_TABLE[240];
const VECTOR_TABLE_Type __VECTOR_TABLE[240] __VECTOR_TABLE_ATTRIBUTE = {
(VECTOR_TABLE_Type)(