【教程】如何用GCC“零汇编”白嫖MDK
扫描二维码
随时随地手机看文章
【说在前面的话】其实我很久之前就想写这篇文章了,但彼时总觉得这是一个伪命题:
- 既然已经用了MDK,编译出来的代码,无论是体积还是性能都甩下arm gcc好几条街,谁还会想用gcc来进行Cortex-M开发呢?
- 对那些只能使用arm gcc、或者对gcc情有独钟的小伙伴来说,无论是配合eclipse、vscode、Embedded Studio还是其它什么开发环境,哪个不比MDK香呢?
然而,既然你点开了这篇文章,无论是否真的有这样的需求,至少说明你对这样的搭配还是“颇有些好奇”的。我就不去担心背后的真正原因了,就让我们速速切入正题,进入实操环节吧。
先说结论:
- MDK原生支持GCC开发,且不受License限制
- MDK使用GCC开发时“可以做到”不写一句汇编的程度
- MDK使用GCC开发时可以享受来自Runtime Environment配置机制的福利——也就是你可以轻松的享用来自Pack Installer所引入的各类软件包的支持——这同样也是免费的
- MDK使用GCC开发时支持调试(所能调试的代码尺寸受到License限制)
我们知道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 gcc是Arm公司从GCC开源社区“抱回来的孩子”。它虽然语法上与armclang(clang)基本相同,但却拥有自己独立的编译和连接环节,用来描述地址空间布局的方式也完全不同——采用 linker script(*.ld)来进行。
那么这些差异对我们在MDK中使用gcc进行开发有什么意义呢?我们需要做哪些工作准备工作呢?总的来说,问题集中在以下几个方面:
- 编译器的获取和集成
- 如何芯片的启动
- 如何描述目标软件的地址空间布局
- 如何对编译选项进行配置
- 如何进行代码的优化
接下来,我们就有针对性的为您解答这些问题。
【如何在将arm gcc集成到MDK环境中】
arm gcc 获取并不困难,可以访问arm的官方页面直接下载:
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm
下载后一路无脑安装即可,这里就不再赘述。接下来,我们打开MDK,通过菜单 project->New uVision Project... 新建一个工程:
为了方便,工程文件名不妨就叫 gcc_template好了:
单击 "Save" 后,MDK会弹出窗口让我们选择工程的目标芯片,实际上很多芯片公司都为MDK提供了面向gcc的工程模板,因此在这里直接选择实际芯片型号往往就可以省略后面大部分步骤,但考虑到让本教程拥有更强的通用性,这里我们选择目标芯片所使用的处理器:
假设,我们要使用的芯片是STM32F746,我们知道它的内核是Cortex-M7,因此这里就选择 Arm->ARM Cortex-M7->ARMCM7_SP(假设是单精度浮点运算单元),单击OK。
对这里选什么芯片比较纠结的小伙伴大可不必,因为后面随时可以回来改,不会存在那种“买定离手”而“无法反悔”的问题。
接下来,MDK会弹出RTE的配置界面。RTE的配置我们将在后面介绍,此时直接单击OK进行跳过即可。
如果一切顺利,你会看到如下的界面:
以上步骤只能算是准备工作,接下来才是将arm gcc集成到MDK中的正题。依次通过菜单 Project -> Manage -> Project Items 打开配置窗体:
在新打开的对话框中选择 "Folders/Extensions" 选项卡,并勾选“Use GCC Compiler (GNU)for ARM projects”(如下图所示):单击 “...” 按钮,选择arm gcc工具链所在的安装目录。以最新的的arm gcc 2020-q4-major 版本为例,默认情况下它会被安装在
“C:\Program Files (x86)\GNU Arm Embedded Toolchain”
目录下。我们选中这里的 "10 2020-q4-major" 目录,单击 Select Folder 按钮。
在回到上一级窗口时,我们注意到,此时arm gcc的路径已经被正确配置了:
单击“OK”就完成了 arm gcc 的添加工作。此时,如果打开 Project -> Options for Target 窗口,我们会看到编译器配置界面变成了一个陌生的样子:如果你看到类似这样的界面,恭喜您,您的MDK已经和arm gcc“喜结连理”了。
【实现“无汇编化”的启动】很多人可能都有错觉——以为使用gcc开发项目一定要用汇编的方式来处理启动文件——过去也许是这样,但是,“大人时代变了”!。
借助 CMSIS的帮助,我们现在也可以优雅的完全使用C语言来实现芯片的启动过程。首先,我们需要获得最新的CMSIS,具体方法可以在这篇文章《CMSIS玩家的“阴间成就”指南》中获得,这里就不在赘述。
无论是通过Pack安装还是github导入,在确保最新的CMSIS被成功的安装到MDK中以后,我们首先需要在工程中通过RTE窗口引入最新的CMSIS支持:在工具栏中,单击下面的按钮:
打开 Runtime Environment 配置窗口:
这里,我们展开CMSIS,并勾选 CORE(这里,请确保CORE的版本不低于 5.4.0),单击OK确认配置。
如果你对CMSIS的版本有所疑问,可以单击 “Select Packs” 按钮,确保窗体顶端的 “Use latest versions of all installed Software Packs” 被勾选,如果这样做以后,CMSIS-CORE的版本仍然低于 5.4.0,请务必参考这篇文章《CMSIS玩家的“阴间成就”指南》来获取最新的CMSIS。
单击CMSIS-CORE后面的注释文字:
会打开一个浏览器页面,忽略其中的内容,我们需要的是页面网址中的路径信息:
这里,我们找到了当前CMSIS Pack在本地的路径,利用这一路径信息在浏览器中打开对应文件夹,找到 Device目录:
依次进入目录 “Device\ARM\ARMCM7\Source”:
将上图选中的文件拷贝到我们的工程中来:
在MDK工程中,将startup_ARMCM7.c和system_ARMCM7.c加入到工程中参与编译(这里我们新建了一个分组叫做 low_level):
先别着急去编译,注意到这里的小钥匙图标了么?这说明这两个文件自带了“只读属性”。由于我们后面要修改这两个文件,因此必须要通过Windows的文件属性管理将只读属性去除(把下图的勾选去掉后单击OK):此时再看MDK的工程管理器,小钥匙标志就已经消失了:
接下来,打开 “Option for Target...” 窗体,进入Linker选项卡:
将这里的 "Do not use Standard System Startup Files" 选项去除。
注意,这一步骤非常重要,不可以省略,否则你会看到如下的编译错误:
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).
正如错误提示中指出的那样,CMSIS会在一个叫做 __cmsis_start的函数中,调用 "_start" 函数,而这一函数正是gcc标准启动文件的入口,当你在MDK中选择"Do not use Standard System Startup Files" 时,linker自然就找不到这个“不存在”的入口函数啦。接下来,单击如下图所示的按钮:
打开我们刚刚一起拷贝过来的GCC目录,选中其中的连接脚本 gcc_arm.ld后,单击Open:最后的结果如下图所示,单击OK确认我们的配置:
虽然不是必须,但推荐在Misc controls中添加如下的内容:
--specs=nosys.specs -Wl,--gc-sections
-fshort-enums -fshort-wchar
即:接下来,为了初步检验一下我们的成果,在工程中添加一个main.c(实现一个简单的main() 函数):怀着忐忑的心理,按下编译按钮:
不用怀疑,我们已经成功的实现了“零汇编”gcc工程建立。简单不?你可以把这个工程连同文件夹一起保存好,这就是未来的工程模板了。此外,关于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函数的返回值是 int 类型,而这里的返回值会被作为 exit() 函数的传入参数——一般负数表示出错,0表示平安。
- 如果不实现一个 exit() 函数,链接器会报错。
- __attribute__((noreturn)) 就是字面意思,告诉编译器这个这个函数是有去无回的。
- 为了使用类似 __NOP() 这样的“固有函数(intrinsics)”,我们需要直接或者间接的包含头文件 "cmsis_compiler.h"
此外,如果我们不做任何的设置,MDK会将所有生成的中间文件(比如 .o、.d之类)直接保存到工程文件夹下,产生“垃圾遍布”的感觉:为了解决这一问题,我们可以在"Options for Target"窗口的Target选项卡中通过“Select Folder for Objects” 来选择一个专门的文件夹放置这些中间文件:完成基础模板的制作后,接下来我们来一一介绍一些模板在使用过程中所需要处理的细节问题:
【简单的地址空间布局、Stack和Heap的配置】在去掉 GCC/gcc_arm.ld 文件的只读属性后,我们就可以借助它根据目标芯片的实际情况描述地址空间布局,打开gcc_arm.ld,可以看到如下的内容:
如果你的目标芯片较为简单,比如,FLASH是一片完整的地址区间,则可以通过修改__ROM_BASE的方式来设置目标镜像中FLASH的起始地址,通过修改修改__ROM_SIZE来设置FLASH的实际大小,比如,起始地址为0x0800-0000,大小为256K的Flash对应的修改方式为:
/*---------------------- 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来设置,这里就不再赘述:/*--------------------- Embedded RAM Configuration ----------------------------
RAM Configuration
RAM Base Address <0x0-0xFFFFFFFF:8>
RAM Size (in Bytes) <0x0-0xFFFFFFFF:8>
-----------------------------------------------------------------------------*/
__RAM_BASE = 0x20000000;
__RAM_SIZE = 0x00020000;
最后,关于Stack和Heap大小的设置可以借助__STACK_SIZE和__HEAP_SIZE来设置:/*--------------------- 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 里定义的其实是一个默认的中断向量表:
可以看到,这一向量表完全采用的是C语言函数指针数组初始化的形式定义的。它不仅提供了默认的各类系统异常的定义,还以Interruptn_Handler的形式为我们提供了定义的范例。
更新这一文件的步骤并不复杂。实际上一般芯片公司都会提供符合CMSIS规范的芯片头文件,这一头文件中会提供对应的中断向量定义,比如STM32F746就有一个对应的头文件 STM32F746xx.h。将其打开会看到专门的向量表定义:
/**
* @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之间的每一项都对应一个外设中断,可以将它们拷贝出来,添加到我们的startup_ARMCM7.c的向量表中,并依样画葫芦,修改成对应的形式:...
/*----------------------------------------------------------------------------
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)(