【教程】如何用GCC“零匯編”白嫖MDK
【說在前面的話】其實(shí)我很久之前就想寫這篇文章了,但彼時(shí)總覺得這是一個(gè)偽命題:
- 既然已經(jīng)用了MDK,編譯出來的代碼,無論是體積還是性能都甩下arm gcc好幾條街,誰還會(huì)想用gcc來進(jìn)行Cortex-M開發(fā)呢?
- 對(duì)那些只能使用arm gcc、或者對(duì)gcc情有獨(dú)鐘的小伙伴來說,無論是配合eclipse、vscode、Embedded Studio還是其它什么開發(fā)環(huán)境,哪個(gè)不比MDK香呢?
然而,既然你點(diǎn)開了這篇文章,無論是否真的有這樣的需求,至少說明你對(duì)這樣的搭配還是“頗有些好奇”的。我就不去擔(dān)心背后的真正原因了,就讓我們速速切入正題,進(jìn)入實(shí)操環(huán)節(jié)吧。
先說結(jié)論:
- MDK原生支持GCC開發(fā),且不受License限制
- MDK使用GCC開發(fā)時(shí)“可以做到”不寫一句匯編的程度
- MDK使用GCC開發(fā)時(shí)可以享受來自Runtime Environment配置機(jī)制的福利——也就是你可以輕松的享用來自Pack Installer所引入的各類軟件包的支持——這同樣也是免費(fèi)的
- MDK使用GCC開發(fā)時(shí)支持調(diào)試(所能調(diào)試的代碼尺寸受到License限制)
我們知道MDK是一個(gè)集成開發(fā)環(huán)境(Integrated Development Environment),它默認(rèn)原生支持Arm Compiler 5(armcc)、Arm Compiler 6(armclang)和 arm gcc。雖然這三個(gè)編譯器都是由Arm所維護(hù)和提供的,但前兩者算是彼此兼容的編譯器:
- 使用共同的?armlink
- 使用相同的方式來描述地址空間布局(分散加載腳本 scatter script)
- 從Arm Compiler 6.14開始,armclang甚至開始支持armasm的匯編語法了
實(shí)際上可以認(rèn)為,armcc和armclang是一對(duì)連體兄弟,身子是armlink,而兩個(gè)腦袋分別是 armcc 和 armclang。大約是這種感覺,你體會(huì)下。
與親生的兩兄弟不同,牛頭人arm gcc是Arm公司從GCC開源社區(qū)“抱回來的孩子”。它雖然語法上與armclang(clang)基本相同,但卻擁有自己獨(dú)立的編譯和連接環(huán)節(jié),用來描述地址空間布局的方式也完全不同——采用 linker script(*.ld)來進(jìn)行。
那么這些差異對(duì)我們?cè)?strong>MDK中使用gcc進(jìn)行開發(fā)有什么意義呢?我們需要做哪些工作準(zhǔn)備工作呢?總的來說,問題集中在以下幾個(gè)方面:
- 編譯器的獲取和集成
- 如何芯片的啟動(dòng)
- 如何描述目標(biāo)軟件的地址空間布局
- 如何對(duì)編譯選項(xiàng)進(jìn)行配置
- 如何進(jìn)行代碼的優(yōu)化
接下來,我們就有針對(duì)性的為您解答這些問題。
【如何在將arm gcc集成到MDK環(huán)境中】
arm gcc 獲取并不困難,可以訪問arm的官方頁面直接下載:
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm
下載后一路無腦安裝即可,這里就不再贅述。接下來,我們打開MDK,通過菜單?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í)可以回來改,不會(huì)存在那種“買定離手”而“無法反悔”的問題。
接下來,MDK會(huì)彈出RTE的配置界面。RTE的配置我們將在后面介紹,此時(shí)直接單擊OK進(jìn)行跳過即可。
如果一切順利,你會(huì)看到如下的界面:
以上步驟只能算是準(zhǔn)備工作,接下來才是將arm gcc集成到MDK中的正題。依次通過菜單 Project -> Manage -> Project Items 打開配置窗體:
在新打開的對(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í),如果打開 Project ->?Options for Target 窗口,我們會(huì)看到編譯器配置界面變成了一個(gè)陌生的樣子:如果你看到類似這樣的界面,恭喜您,您的MDK已經(jīng)和arm gcc“喜結(jié)連理”了。
【實(shí)現(xiàn)“無匯編化”的啟動(dòng)】很多人可能都有錯(cuò)覺——以為使用gcc開發(fā)項(xiàng)目一定要用匯編的方式來處理啟動(dòng)文件——過去也許是這樣,但是,“大人時(shí)代變了”!。
借助 CMSIS的幫助,我們現(xiàn)在也可以優(yōu)雅的完全使用C語言來實(shí)現(xiàn)芯片的啟動(dòng)過程。首先,我們需要獲得最新的CMSIS,具體方法可以在這篇文章《CMSIS玩家的“陰間成就”指南》中獲得,這里就不在贅述。
無論是通過Pack安裝還是github導(dǎo)入,在確保最新的CMSIS被成功的安裝到MDK中以后,我們首先需要在工程中通過RTE窗口引入最新的CMSIS支持:在工具欄中,單擊下面的按鈕:
?
打開?Runtime Environment?配置窗口:
這里,我們展開CMSIS,并勾選?CORE(這里,請(qǐng)確保CORE的版本不低于?5.4.0),單擊OK確認(rèn)配置。
如果你對(duì)CMSIS的版本有所疑問,可以單擊 “Select Packs” 按鈕,確保窗體頂端的 “Use latest versions of all installed Software Packs” 被勾選,如果這樣做以后,CMSIS-CORE的版本仍然低于 5.4.0,請(qǐng)務(wù)必參考這篇文章《CMSIS玩家的“陰間成就”指南》來獲取最新的CMSIS。
單擊CMSIS-CORE后面的注釋文字:
會(huì)打開一個(gè)瀏覽器頁面,忽略其中的內(nèi)容,我們需要的是頁面網(wǎng)址中的路徑信息:
這里,我們找到了當(dāng)前CMSIS Pack在本地的路徑,利用這一路徑信息在瀏覽器中打開對(duì)應(yīng)文件夾,找到 Device目錄:
依次進(jìn)入目錄?“Device\ARM\ARMCM7\Source”:
將上圖選中的文件拷貝到我們的工程中來:
在MDK工程中,將startup_ARMCM7.c和system_ARMCM7.c加入到工程中參與編譯(這里我們新建了一個(gè)分組叫做 low_level):
先別著急去編譯,注意到這里的小鑰匙圖標(biāo)了么?這說明這兩個(gè)文件自帶了“只讀屬性”。由于我們后面要修改這兩個(gè)文件,因此必須要通過Windows的文件屬性管理將只讀屬性去除(把下圖的勾選去掉后單擊OK):此時(shí)再看MDK的工程管理器,小鑰匙標(biāo)志就已經(jīng)消失了:
接下來,打開?“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ù)啦。接下來,單擊如下圖所示的按鈕:
打開我們剛剛一起拷貝過來的GCC目錄,選中其中的連接腳本 gcc_arm.ld后,單擊Open:最后的結(jié)果如下圖所示,單擊OK確認(rèn)我們的配置:
雖然不是必須,但推薦在Misc controls中添加如下的內(nèi)容:
--specs=nosys.specs?-Wl,--gc-sections
-fshort-enums?-fshort-wchar
即:接下來,為了初步檢驗(yàn)一下我們的成果,在工程中添加一個(gè)main.c(實(shí)現(xiàn)一個(gè)簡單的main() 函數(shù)):懷著忐忑的心理,按下編譯按鈕:
不用懷疑,我們已經(jīng)成功的實(shí)現(xiàn)了“零匯編”gcc工程建立。簡單不?你可以把這個(gè)工程連同文件夾一起保存好,這就是未來的工程模板了。此外,關(guān)于main.c中的代碼,需要做一些簡單的說明:
#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 類型,而這里的返回值會(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ù)是有去無回的。
- 為了使用類似 __NOP() 這樣的“固有函數(shù)(intrinsics)”,我們需要直接或者間接的包含頭文件? "cmsis_compiler.h"
此外,如果我們不做任何的設(shè)置,MDK會(huì)將所有生成的中間文件(比如 .o、.d之類)直接保存到工程文件夾下,產(chǎn)生“垃圾遍布”的感覺:為了解決這一問題,我們可以在"Options for Target"窗口的Target選項(xiàng)卡中通過“Select Folder for Objects” 來選擇一個(gè)專門的文件夾放置這些中間文件:完成基礎(chǔ)模板的制作后,接下來我們來一一介紹一些模板在使用過程中所需要處理的細(xì)節(jié)問題:
【簡單的地址空間布局、Stack和Heap的配置】在去掉 GCC/gcc_arm.ld 文件的只讀屬性后,我們就可以借助它根據(jù)目標(biāo)芯片的實(shí)際情況描述地址空間布局,打開gcc_arm.ld,可以看到如下的內(nèi)容:
如果你的目標(biāo)芯片較為簡單,比如,F(xiàn)LASH是一片完整的地址區(qū)間,則可以通過修改__ROM_BASE的方式來設(shè)置目標(biāo)鏡像中FLASH的起始地址,通過修改修改__ROM_SIZE來設(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的起始地址和大小可以通過__RAM_BASE和__RAM_SIZE來設(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來設(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語言函數(shù)指針數(shù)組初始化的形式定義的。它不僅提供了默認(rèn)的各類系統(tǒng)異常的定義,還以Interruptn_Handler的形式為我們提供了定義的范例。
更新這一文件的步驟并不復(fù)雜。實(shí)際上一般芯片公司都會(huì)提供符合CMSIS規(guī)范的芯片頭文件,這一頭文件中會(huì)提供對(duì)應(yīng)的中斷向量定義,比如STM32F746就有一個(gè)對(duì)應(yīng)的頭文件 STM32F746xx.h。將其打開會(huì)看到專門的向量表定義:
/**
* @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è)中斷,可以將它們拷貝出來,添加到我們的startup_ARMCM7.c的向量表中,并依樣畫葫蘆,修改成對(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)(