【教程】如何用GCC“零汇编”白嫖MDK

2021-07-29 15:48:29 浏览数 (1)

【说在前面的话】


其实我很久之前就想写这篇文章了,但彼时总觉得这是一个伪命题:

  • 既然已经用了MDK,编译出来的代码,无论是体积还是性能都甩下arm gcc好几条街,谁还会想用gcc来进行Cortex-M开发呢?
  • 对那些只能使用arm gcc、或者对gcc情有独钟的小伙伴来说,无论是配合eclipsevscodeEmbedded 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的汇编语法了

实际上可以认为,armccarmclang是一对连体兄弟,身子是armlink,而两个脑袋分别是 armccarmclang。大约是这种感觉,你体会下。

与亲生的两兄弟不同,牛头人arm gccArm公司从GCC开源社区“抱回来的孩子”。它虽然语法上与armclang(clang)基本相同,但却拥有自己独立的编译和连接环节,用来描述地址空间布局的方式也完全不同——采用 linker script(*.ld)来进行。

那么这些差异对我们在MDK中使用gcc进行开发有什么意义呢?我们需要做哪些工作准备工作呢?总的来说,问题集中在以下几个方面:

  1. 编译器的获取和集成
  2. 如何芯片的启动
  3. 如何描述目标软件的地址空间布局
  4. 如何对编译选项进行配置
  5. 如何进行代码的优化

接下来,我们就有针对性的为您解答这些问题。

【如何在将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目录:

依次进入目录 “DeviceARMARMCM7Source”:

将上图选中的文件拷贝到我们的工程中来:

MDK工程中,将startup_ARMCM7.csystem_ARMCM7.c加入到工程中参与编译(这里我们新建了一个分组叫做 low_level):

先别着急去编译,注意到这里的小钥匙图标了么?这说明这两个文件自带了“只读属性”。由于我们后面要修改这两个文件,因此必须要通过Windows的文件属性管理将只读属性去除(把下图的勾选去掉后单击OK):

此时再看MDK的工程管理器,小钥匙标志就已经消失了:

接下来,打开 “Option for Target...” 窗体,进入Linker选项卡:

将这里的 "Do not use Standard System Startup Files" 选项去除。


注意,这一步骤非常重要,不可以省略,否则你会看到如下的编译错误:

代码语言:javascript复制
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 Projectgcc_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中添加如下的内容:

代码语言:javascript复制
--specs=nosys.specs -Wl,--gc-sections
-fshort-enums -fshort-wchar

即:

接下来,为了初步检验一下我们的成果,在工程中添加一个main.c(实现一个简单的main() 函数):

怀着忐忑的心理,按下编译按钮:

不用怀疑,我们已经成功的实现了“零汇编”gcc工程建立。简单不?你可以把这个工程连同文件夹一起保存好,这就是未来的工程模板了。此外,关于main.c中的代码,需要做一些简单的说明:

代码语言:javascript复制
#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之类)直接保存到工程文件夹下,产生“垃圾遍布”的感觉:

为了解决这一问题,我们可以在"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对应的修改方式为:

代码语言:javascript复制
/*---------------------- 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来设置,这里就不再赘述:

代码语言:javascript复制
/*--------------------- 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;

最后,关于StackHeap大小的设置可以借助__STACK_SIZE__HEAP_SIZE来设置:

代码语言:javascript复制
/*--------------------- 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 */

【如何配置中断向量表】


不同的芯片拥有不同的中断向量表,而此前我们所建立的gcc工程模板中,startup_ARMCM7.c 里定义的其实是一个默认的中断向量表:

可以看到,这一向量表完全采用的是C语言函数指针数组初始化的形式定义的。它不仅提供了默认的各类系统异常的定义,还以Interruptn_Handler的形式为我们提供了定义的范例。

更新这一文件的步骤并不复杂。实际上一般芯片公司都会提供符合CMSIS规范的芯片头文件,这一头文件中会提供对应的中断向量定义,比如STM32F746就有一个对应的头文件 STM32F746xx.h。将其打开会看到专门的向量表定义:

代码语言:javascript复制
/**
 * @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_IRQnSPDIF_RX_IRQn之间的每一项都对应一个外设中断,可以将它们拷贝出来,添加到我们的startup_ARMCM7.c的向量表中,并依样画葫芦,修改成对应的形式:

代码语言:javascript复制
...
/*----------------------------------------------------------------------------
  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"

代码语言:javascript复制
#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文件中补充即可:

代码语言:javascript复制
  /**
  brief Exception / Interrupt Handler Function Prototype
*/
typedef void(*VECTOR_TABLE_Type)(void);

对于STM32芯片的用户来说,其实官方的CMSIS Pack已经为arm gcc提供了对应的启动文件,我们可以在RTE中将其打开:

随后在工程管理器中就可以在Device选项卡下看到它们:

遗憾的是,这里的启动文件使用的是汇编,如果你不喜欢它们,则仍然可以使用本文介绍的方法。

再次重申下,本文使用STM32作为例子仅仅是因为我手上有这块板子,并不是说对于STM32来说,使用GCC一定要用我说的方法而不使用官方提供好的gcc启动文件。本文讲解的目标时提供最大的通用性,因此会涉及到很多具体的细节。


值得注意的是:有时候,某些芯片会提供面向Arm Compiler 5或者Arm Compiler 6system_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" 文本框中追加以下的编译选项:

代码语言:javascript复制
-ffunction-sections -fdata-sections

由于linker进行尺寸优化的基本单位是section,而section是变量和代码的容器。默认情况下,每个c源文件中所有函数生成的代码都会放在一个叫做“.text”的容器中;而所有静态分配的变量也会被类似的放在名为.data或者.bss的section中——这样的缺点是,整个section中只有一个函数或者变量被用到了,整个section中的内容都会被判定为是需要保留的。更糟糕的是,这种判定是具有“传染”性的,这意味着哪怕某一个section中存在没用到函数,只要该section被判定为要保留,则这些没有用到的函数所调用的函数,其所在section也会被传染。

要解决这一问题的方式很简单,直接给每一个函数或者静态变量都分配一个独立的section即可。

关于这方面的详细讨论,请参考文章《真刀真枪模块化(3.5)——骚操作?不!这才是正统》,这里就不再赘述。


此外,还有一些更高阶的优化选项并未提供在Optimisation列表中,例如,最高的性能优化"-Ofast",以及更聪明的链接优化“Link Time Optimisation”,详细的使用效果请参考gcc的官方文档,这里就不再赘述。要想使用它们:

  1. 可以将 Optimisation列表设置为<Default>;
  2. Misc Controls文本框中添加对应的选项"-Ofast"已开启最高性能优化;
  3. Misc Controls文本框中添加对应的选项"-flto"已开启Link Time Optimisation

注意,对应的编译选项也要在 "Linker"的Misc Controls中添加:

【如何在编译成功后打印尺寸信息】


MDK使用Arm Compiler 5或者Arm Compiler 6进行编译,成功后会在Build Output窗口中打印一些尺寸信息,比如:

MDK使用GCC进行编译时,默认情况下就没有这么方便了。为了达到同样的效果,我们可以在"Options for Target"的“User” 选项卡中增加 After Build/Rebuild命令行:

代码语言:javascript复制
arm-none-eabi-size.exe ./Objects/gcc_template.elf

注意,如果你修改了输出文件的名称,千万要记得同步更新这里的命令行呀!

最终实现了类似的效果:

【如何优雅的测量系统的性能】


熟悉我公众号的朋友一定注意到我有一个开源项目 perf_counter,可以帮助用户在不额外占用SysTick的情况下提供一系列服务,包括但不限于:

  • 为裸机或者RTOS提供Cycle级别的性能测量;
  • 评估代码片段的CPU占用;
  • 算法精细优化时用于测量和观察优化的效果;
  • 测量中断的响应时间;
  • 测量中断的发生间隔(查找最短时间间隔);
  • 评估GUI的帧率或者刷新率;
  • 与SystemCoreClock计算后,获得一个系统时间戳(Timestamp);
  • 当做Realtime Clock的基准;
  • 作为随机数种子
  • ……

其详细使用方法在文章《如何“优雅”的测量系统性能》中有详细介绍,这里就不再赘述。在Github上的最新版本中,优化了gcc的部署体验——也能像Arm Compiler 5以及Arm Compiler 6那样简单拖放lib即可完成部署

具体步骤如下:

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文件,将原本的:

代码语言:javascript复制
void SysTick_Handler (void) __attribute__ ((weak, alias("Default_Handler")));

修改为

代码语言:javascript复制
void SysTick_Handler(void) __attribute__ ((weak));

注意,千万不要在startup_ARMCM7.c中为 SysTick_Handler提供默认的weak实现函数!切记,切记!

5、更新"Options for Target"的"Linker"配置,在Misc Controls文本框中追加:

代码语言:javascript复制
 -Wl,--wrap=SysTick_Handler
"./perf_counter/lib/libperf_counter_gcc.a"

6、在要用到 perf_counter 服务的地方添加对头文件的包含:

代码语言:javascript复制
#include "perf_counter.h"
...

7、编译工程,如果报告如下的错误:

代码语言:javascript复制
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进行初始化:

代码语言:javascript复制
#include "perf_counter.h"

void SysTick_Handler(void)
{
    
}

int main(void)
{
    init_cycle_counter(false);
    
    while(1) {
    }
    
    return 0;
}

编译后一切正常:

详细的使用方法,还请参考《如何“优雅”的测量系统性能》。

【说在后面的话】


MDK中使用GCC具有很多实际意义,比如:

  • 编译不受License限制
  • 可以进行调试(需要License)
  • 可以借助RTE实现各类CMSIS Pack的快速部署(比如很多操作系统:cmsis-rtos2等等)
  • 不必编写makefile
  • 很多大厂芯片(比如STM32)其实都在CMSIS-Pack的软件包里提供了arm gcc的支持(提供了arm gcc下的启动文件和 *.ld 文件),直接通过RTE就可以加入对应的文件

不管你是否喜欢MDK,总的来说是多了一种选择把。


0 人点赞