【教程】如何用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)来进行。
编译器的获取和集成
如何芯片的启动
如何描述目标软件的地址空间布局
如何对编译选项进行配置
如何进行代码的优化
arm gcc 获取并不困难,可以访问arm的官方页面直接下载:
下载后一路无脑安装即可,这里就不再赘述。接下来,我们打开MDK,通过菜单 project->New uVision Project... 新建一个工程:
如果一切顺利,你会看到如下的界面:
在新打开的对话框中选择 '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 窗口,我们会看到编译器配置界面变成了一个陌生的样子:
【实现“无汇编化”的启动】
很多人可能都有错觉——以为使用gcc开发项目一定要用汇编的方式来处理启动文件——过去也许是这样,但是,“大人时代变了”!。
单击CMSIS-CORE后面的注释文字:
会打开一个浏览器页面,忽略其中的内容,我们需要的是页面网址中的路径信息:
这里,我们找到了当前CMSIS Pack在本地的路径,利用这一路径信息在浏览器中打开对应文件夹,找到 Device目录:
在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 <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#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之类)直接保存到工程文件夹下,产生“垃圾遍布”的感觉:
完成基础模板的制作后,接下来我们来一一介绍一些模板在使用过程中所需要处理的细节问题:
【简单的地址空间布局、Stack和Heap的配置】
在去掉 GCC/gcc_arm.ld 文件的只读属性后,我们就可以借助它根据目标芯片的实际情况描述地址空间布局,打开gcc_arm.ld,可以看到如下的内容:
如果你的目标芯片较为简单,比如,FLASH是一片完整的地址区间,则可以通过修改__ROM_BASE的方式来设置目标镜像中FLASH的起始地址,通过修改修改__ROM_SIZE来设置FLASH的实际大小,比如,起始地址为0x0800-0000,大小为256K的Flash对应的修改方式为:
/*---------------------- Flash Configuration ----------------------------------
<h> Flash Configuration
<o0> Flash Base Address <0x0-0xFFFFFFFF:8>
<o1> Flash Size (in Bytes) <0x0-0xFFFFFFFF:8>
</h>
-----------------------------------------------------------------------------*/
__ROM_BASE = 0x08000000;
__ROM_SIZE = 0x00040000;
同理,SRAM的起始地址和大小可以通过__RAM_BASE和__RAM_SIZE来设置,这里就不再赘述:
/*--------------------- Embedded RAM Configuration ----------------------------
<h> RAM Configuration
<o0> RAM Base Address <0x0-0xFFFFFFFF:8>
<o1> RAM Size (in Bytes) <0x0-0xFFFFFFFF:8>
</h>
-----------------------------------------------------------------------------*/
__RAM_BASE = 0x20000000;
__RAM_SIZE = 0x00020000;
最后,关于Stack和Heap大小的设置可以借助__STACK_SIZE和__HEAP_SIZE来设置:
/*--------------------- Stack / Heap Configuration ----------------------------
<h> Stack / Heap Configuration
<o0> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
<o1> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
</h>
-----------------------------------------------------------------------------*/
__STACK_SIZE = 0x00000800; /* 2K Byte */
__HEAP_SIZE = 0x00000200; /* 256 Byte */
【如何配置中断向量表】
可以看到,这一向量表完全采用的是C语言函数指针数组初始化的形式定义的。它不仅提供了默认的各类系统异常的定义,还以Interruptn_Handler的形式为我们提供了定义的范例。
/**
* @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)(&__INITIAL_SP), /* Initial Stack Pointer */
Reset_Handler, /* Reset Handler */
NMI_Handler, /* -14 NMI Handler */
HardFault_Handler, /* -13 Hard Fault Handler */
MemManage_Handler, /* -12 MPU Fault Handler */
BusFault_Handler, /* -11 Bus Fault Handler */
UsageFault_Handler, /* -10 Usage Fault Handler */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
SVC_Handler, /* -5 SVC Handler */
DebugMon_Handler, /* -4 Debug Monitor Handler */
0, /* Reserved */
PendSV_Handler, /* -2 PendSV Handler */
SysTick_Handler, /* -1 SysTick Handler */
/* Interrupts */
[WWDG_IRQn+16]= WWDG_IRQn_Handler, /* 0 WWDG_IRQn */
...
[SPDIF_RX_IRQn+16]= SPDIF_RX_IRQn_Handler,/* 97 SPDIF_RX_IRQn */
};
接下来,我们需要更新 startup_ARMCM7.c 和 system_ARMCM7.c中的头文件,同样以STM32F746为例,将原本的ARMCM7相关的包含注释掉,加入#include 'stm32f746xx.h':
#if 0
#if defined (ARMCM7)
#include 'ARMCM7.h'
#elif defined (ARMCM7_SP)
#include 'ARMCM7_SP.h'
#elif defined (ARMCM7_DP)
#include 'ARMCM7_DP.h'
#else
#error device not specified!
#endif
#endif
#include 'stm32f746xx.h'
有时候,客户芯片的头文件会缺少一些必要的定义,比如函数指针VECTOR_TABLE_Type,则直接在这两个.c文件中补充即可:
/**
\brief Exception / Interrupt Handler Function Prototype
*/
typedef void(*VECTOR_TABLE_Type)(void);
对于STM32芯片的用户来说,其实官方的CMSIS Pack已经为arm gcc提供了对应的启动文件,我们可以在RTE中将其打开:
随后在工程管理器中就可以在Device选项卡下看到它们:
遗憾的是,这里的启动文件使用的是汇编,如果你不喜欢它们,则仍然可以使用本文介绍的方法。
值得注意的是:有时候,某些芯片会提供面向Arm Compiler 5或者Arm Compiler 6的system_xxxx.c,其实我们完全可以拷贝出来直接替换掉这里的 system_ARMCM7.c。在STM32F746的例子中,我们看中了厂家提供的system_stm32f7xx.c——因为其中包含了必要的芯片初始化代码(时钟、外设等等),因此,我们将其单独拷贝到工程目录下:
加入工程管理器中参与编译,并将原本的system_ARMCM7.c从编译中剔除:
最终呈现的效果如下:
在我们着急开始编译以验证效果之前,这里有一个细节需要分情况讨论:
目标芯片原本就与针对MDK的 CMSIS-Pack支持
对于这种情况,我们需要在“Options for Target”的Device选项卡中选择对应芯片,这样MDK会自动将目标芯片头文件的路径加入编译器的头文件搜索列表中。
目标芯片没有针对MDK的CMSIS-Pack,而只提供了目标芯片的头文件(包含了寄存器定义等等)
此时,我们需要将目标芯片的头文件拷贝到工程目录下,并收工将对应路径添加到编译器的头文件搜索列表中。这里因为我们假设你直接将头文件保存在了工程目录下,因此这里的搜索路径就是'工程所在当前目录'——直接用'.'就可以了:
完成了上述步骤,基本上就完成了对新的目标芯片的最基础支持。
【如何设置开启编译优化】
MDK在“Option for Target”的'CC'选项卡中提供了简化的优化选项支持:
看似满足要求,其实远远不够——哪怕你选择了'Level 2 (Size)'优化,可能最终代码的尺寸依然大的吓人。要解决这一问题,需要在 'Misc Controlls' 文本框中追加以下的编译选项:
-ffunction-sections -fdata-sections
由于linker进行尺寸优化的基本单位是section,而section是变量和代码的容器。默认情况下,每个c源文件中所有函数生成的代码都会放在一个叫做“.text”的容器中;而所有静态分配的变量也会被类似的放在名为.data或者.bss的section中——这样的缺点是,整个section中只有一个函数或者变量被用到了,整个section中的内容都会被判定为是需要保留的。更糟糕的是,这种判定是具有“传染”性的,这意味着哪怕某一个section中存在没用到函数,只要该section被判定为要保留,则这些没有用到的函数所调用的函数,其所在section也会被传染。
此外,还有一些更高阶的优化选项并未提供在Optimisation列表中,例如,最高的性能优化'-Ofast',以及更聪明的链接优化“Link Time Optimisation”,详细的使用效果请参考gcc的官方文档,这里就不再赘述。要想使用它们:
可以将 Optimisation列表设置为<Default>;
在Misc Controls文本框中添加对应的选项'-Ofast'已开启最高性能优化;
在Misc Controls文本框中添加对应的选项'-flto'已开启Link Time Optimisation;
注意,对应的编译选项也要在 'Linker'的Misc Controls中添加:
【如何在编译成功后打印尺寸信息】
MDK使用GCC进行编译时,默认情况下就没有这么方便了。为了达到同样的效果,我们可以在'Options for Target'的“User” 选项卡中增加 After Build/Rebuild命令行:
arm-none-eabi-size.exe ./Objects/gcc_template.elf
注意,如果你修改了输出文件的名称,千万要记得同步更新这里的命令行呀!
最终实现了类似的效果:
【如何优雅的测量系统的性能】
为裸机或者RTOS提供Cycle级别的性能测量;
评估代码片段的CPU占用;
算法精细优化时用于测量和观察优化的效果;
测量中断的响应时间;
测量中断的发生间隔(查找最短时间间隔);
评估GUI的帧率或者刷新率;
与SystemCoreClock计算后,获得一个系统时间戳(Timestamp);
当做Realtime Clock的基准;
作为随机数种子
……
具体步骤如下:
1、通过下面连接获取最新版本的 perf_counter
https://github.com/GorgonMeducer/perf_counter/releases/tag/v1.5.0
2、解压缩后拷贝到gcc工程所在目录下,改名为perf_counter
3、添加perf_counter.h所在路径到编译器头文件搜索路径中:
4、修改startup_ARMCM7.c文件,将原本的:
void SysTick_Handler (void) __attribute__ ((weak, alias('Default_Handler')));
修改为
void SysTick_Handler(void) __attribute__ ((weak));
注意,千万不要在startup_ARMCM7.c中为 SysTick_Handler提供默认的weak实现函数!切记,切记!
5、更新'Options for Target'的'Linker'配置,在Misc Controls文本框中追加:
-Wl,--wrap=SysTick_Handler
'./perf_counter/lib/libperf_counter_gcc.a'
6、在要用到 perf_counter 服务的地方添加对头文件的包含:
#include 'perf_counter.h'
...
7、编译工程,如果报告如下的错误:
libperf_counter_gcc.a(systick_wrapper_gcc.o): in function `__ensure_systick_wrapper':
(.text+0x18): undefined reference to `SysTick_Handler'
则说明我们的工程中并没有实现SysTick_Handler,此时,应该在任意c文件中增加一个SysTick_Handler,并在 main() 函数中对 perf_counter.c进行初始化:
#include 'perf_counter.h'
void SysTick_Handler(void)
{
}
int main(void)
{
init_cycle_counter(false);
while(1) {
}
return 0;
}
编译后一切正常:
【说在后面的话】
编译不受License限制
可以进行调试(需要License)
可以借助RTE实现各类CMSIS Pack的快速部署(比如很多操作系统:cmsis-rtos2等等)
不必编写makefile
很多大厂芯片(比如STM32)其实都在CMSIS-Pack的软件包里提供了arm gcc的支持(提供了arm gcc下的启动文件和 *.ld 文件),直接通过RTE就可以加入对应的文件