从 STM32 MCU 获得快速性能

电器工程 微控制器 stm32 手臂 stm32f3
2022-01-29 19:57:51

我正在使用 STM32F303VC发现套件,我对它的性能感到有些困惑。为了熟悉这个系统,我编写了一个非常简单的程序来测试这个 MCU 的 bit-banging 速度。代码可以分解如下:

  1. HSI 时钟 (8 MHz) 开启;
  2. PLL 以 16 的预分频器启动,以实现 HSI / 2 * 16 = 64 MHz;
  3. PLL 被指定为 SYSCLK;
  4. SYSCLK 在 MCO 引脚 (PA8) 上进行监控,其中一个引脚 (PE10) 在无限循环中不断切换。

该程序的源代码如下所示:

#include "stm32f3xx.h"

int main(void)
{
      // Initialize the HSI:
      RCC->CR |= RCC_CR_HSION;
      while(!(RCC->CR&RCC_CR_HSIRDY));

      // Initialize the LSI:
      // RCC->CSR |= RCC_CSR_LSION;
      // while(!(RCC->CSR & RCC_CSR_LSIRDY));

      // PLL configuration:
      RCC->CFGR &= ~RCC_CFGR_PLLSRC;     // HSI / 2 selected as the PLL input clock.
      RCC->CFGR |= RCC_CFGR_PLLMUL16;   // HSI / 2 * 16 = 64 MHz
      RCC->CR |= RCC_CR_PLLON;          // Enable PLL
      while(!(RCC->CR&RCC_CR_PLLRDY));  // Wait until PLL is ready

      // Flash configuration:
      FLASH->ACR |= FLASH_ACR_PRFTBE;
      FLASH->ACR |= FLASH_ACR_LATENCY_1;

      // Main clock output (MCO):
      RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
      GPIOA->MODER |= GPIO_MODER_MODER8_1;
      GPIOA->OTYPER &= ~GPIO_OTYPER_OT_8;
      GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR8;
      GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8;
      GPIOA->AFR[0] &= ~GPIO_AFRL_AFRL0;

      // Output on the MCO pin:
      //RCC->CFGR |= RCC_CFGR_MCO_HSI;
      //RCC->CFGR |= RCC_CFGR_MCO_LSI;
      //RCC->CFGR |= RCC_CFGR_MCO_PLL;
      RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;

      // PLL as the system clock
      RCC->CFGR &= ~RCC_CFGR_SW;    // Clear the SW bits
      RCC->CFGR |= RCC_CFGR_SW_PLL; //Select PLL as the system clock
      while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL); //Wait until PLL is used

      // Bit-bang monitoring:
      RCC->AHBENR |= RCC_AHBENR_GPIOEEN;
      GPIOE->MODER |= GPIO_MODER_MODER10_0;
      GPIOE->OTYPER &= ~GPIO_OTYPER_OT_10;
      GPIOE->PUPDR &= ~GPIO_PUPDR_PUPDR10;
      GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR10;

      while(1)
      {
          GPIOE->BSRRL |= GPIO_BSRR_BS_10;
          GPIOE->BRR |= GPIO_BRR_BR_10;

      }
}

该代码是使用 CoIDE V2 和 GNU ARM Embedded Toolchain 使用 -O1 优化编译的。用示波器检查引脚 PA8 (MCO) 和 PE10 上的信号如下所示: 在此处输入图像描述

SYSCLK 似乎配置正确,因为 MCO(橙色曲线)显示出接近 64 MHz 的振荡(考虑到内部时钟的误差范围)。对我来说奇怪的部分是 PE10 上的行为(蓝色曲线)。在无限while(1) 循环中,它需要4 + 4 + 5 = 13 个时钟周期来执行一个基本的3 步操作(即位设置/位复位/返回)。它在其他优化级别(例如 -O2、-O3、ar -Os)上变得更糟:在信号的 LOW 部分添加了几个额外的时钟周期,即在 PE10 的下降沿和上升沿之间(以某种方式启用 LSI 似乎来纠正这种情况)。

此 MCU 是否会出现这种行为?我想像设置和重置位这样简单的任务应该快 2-4 倍。有没有办法加快速度?

4个回答

这里的问题实际上是:您从 C 程序生成的机器代码是什么,它与您的预期有何不同。

如果您无法访问原始代码,这将是一个逆向工程练习(基本上是以: 开头的东西radare2 -A arm image.bin; aaa; VV),但是您已经获得了代码,所以这会让一切变得更容易。

-g首先,使用添加到CFLAGS(您还指定的同一位置)的标志来编译它-O1然后,查看生成的程序集:

arm-none-eabi-objdump -S yourprog.elf

请注意,objdump二进制文件的名称和中间 ELF 文件的名称当然可能不同。

通常,您也可以跳过 GCC 调用汇编程序的部分,只查看汇编文件。只需添加-S到 GCC 命令行 - 但这通常会破坏您的构建,因此您很可能会在IDE之外执行此操作。

对您的代码进行了稍微修补的版本的组装

arm-none-eabi-gcc 
    -O1 ## your optimization level
    -S  ## stop after generating assembly, i.e. don't run `as`
    -I/path/to/CMSIS/ST/STM32F3xx/ -I/path/to/CMSIS/include
     test.c

并得到以下内容(摘录,上面链接下的完整代码):

.L5:
    ldr r2, [r3, #24]
    orr r2, r2, #1024
    str r2, [r3, #24]
    ldr r2, [r3, #40]
    orr r2, r2, #1024
    str r2, [r3, #40]
    b   .L5

这是一个循环(注意最后无条件跳转到 .L5 和开头的 .L5 标签)。

我们在这里看到的是我们

  • 首先ldr(加载寄存器)寄存器r2,其内存位置的值存储在r3+ 24 字节中。懒得查:很可能是BSRR.
  • 然后是带有常量OR的寄存器,对应于设置该寄存器中的第 10 位,并将结果写入自身。r21024 == (1<<10)r2
  • 然后str将结果(存储)到我们在第一步中读取的内存位置
  • 然后出于懒惰对不同的内存位置重复相同的操作:最有可能BRR的地址。
  • 最后b(分支)回到第一步。

所以我们有 7 条指令,而不是 3 条指令。b发生一次,因此很可能需要奇数个周期(我们总共有 13 个,因此必须来自某个奇数周期数)。由于13以下的所有奇数都是1、3、5、7、9、11,我们可以排除任何大于13-6的数字(假设CPU不能在不到一个周期内执行一条指令),我们知道需要b1、3、5 或 7 个 CPU 周期。

作为我们,我查看了 ARM 的指令文档以及M3需要多少周期:

  • ldr需要 2 个周期(在大多数情况下)
  • orr需要1个周期
  • str需要2个周期
  • b需要2到4个周期。我们知道它必须是一个奇数,所以它必须取 3,在这里。

这一切都与您的观察一致:

$$\begin{align} 13 &= 2\cdot(&c_\mathtt{ldr}&+c_\mathtt{orr}&+c_\mathtt{str})&+c_\mathtt{b}\\ &= 2 \cdot(&2&+1&+2)&+3\\ &= 2\cdot &5 &&&+3 \end{align}$$


正如上面的计算所示,几乎没有办法让你的循环更快——ARM处理器上的输出引脚通常是内存映射的,而不是CPU核心寄存器,所以你必须通过通常的加载-修改-存储例程如果你想对这些做任何事情。

您当然可以做的不是每次循环迭代都读取(|=隐式必须读取)引脚的值,而只需将局部变量的值写入它,您只需切换每次循环迭代。

请注意,我觉得您可能熟悉 8 位微控制器,并且会尝试仅读取 8 位值,将它们存储在本地 8 位变量中,并将它们写入 8 位块中。别。ARM 是 32 位架构,提取 32 位字的 8 位可能需要额外的指令。如果可以,只需读取整个 32 位字,修改您需要的内容,然后将其全部写回。当然,这是否可能取决于您要写入的内容,即内存映射 GPIO 的布局和功能。有关存储在包含您要切换的位的 32 位中的内容的信息,请参阅 STM32F3 数据表/用户指南。


-O3现在,我试图重现您的问题,“低”期变长,但我根本做不到——循环看起来与-O1我的编译器版本完全相同。你必须自己做!也许您正在使用一些旧版本的 GCC,但对 ARM 的支持并不理想。

寄存器用于设置和复位各个端口位BSRRBRR

GPIO 端口位设置/复位寄存器 (GPIOx_BSRR)

...

(x = A..H) 位 15:0

BSy:端口 x 设置位 y (y= 0..15)

这些位是只写的。读取这些位返回值 0x0000。

0:对应的 ODRx 位无动作

1:设置相应的 ODRx 位

如您所见,读取这些寄存器总是给出 0,因此您的代码是什么

GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;

确实有效的是GPIOE->BRR = 0 | GPIO_BRR_BR_10,但优化器不知道,所以它生成一系列LDR, ORR,STR指令而不是单个存储。

您可以通过简单地编写来避免昂贵的读-修改-写操作

GPIOE->BSRRL = GPIO_BSRR_BS_10;
GPIOE->BRR = GPIO_BRR_BR_10;

通过将循环对齐到可被 8 整除的地址,您可能会获得进一步的改进。尝试asm("nop");在循环之前放置一个或模式指令while(1)

补充一下这里所说的:当然对于 Cortex-M,但几乎任何处理器(带有管道、缓存、分支预测或其他功能),即使是最简单的循环也是微不足道的:

top:
   subs r0,#1
   bne top

尽可能多地运行它,但能够使该循环的性能变化很大,就这两条指令,如果你愿意,可以在中间添加一些 nop;没关系。

改变循环的对齐方式可以显着改变性能,特别是对于像这样的小循环,如果它需要两条取指线而不是一条,你会吃掉额外的成本,在这样的微控制器上,闪存比 CPU 慢 2或 3,然后通过提高时钟,比率变得比添加额外的获取更差 3 或 4 或 5。

您可能没有缓存,但如果您有缓存,它在某些情况下会有所帮助,但在其他情况下会受到伤害和/或不会产生影响。您在这里可能有或可能没有(可能没有)的分支预测只能看到管道中的设计,所以即使您将循环更改为分支并在最后有一个无条件分支(分支预测器更容易使用)所做的只是在下一次提取时为您节省那么多时钟(管道的大小,从它通常提取的位置到预测器可以看到的深度)和/或它不进行预取以防万一。

通过更改 fetch 和 cache 行的对齐方式,您可以影响分支预测器是否对您有帮助,这可以从整体性能中看出,即使您只测试两条指令或带有一些 nop 的两条指令.

这样做有些微不足道,一旦您了解了这一点,然后获取编译代码,甚至是手写程序集,您就会看到由于这些因素,它的性能可能会有很大差异,增加或节省几个到几百个百分点,一行 C 代码,一个位置不佳的 nop。

在学习使用 BSRR 寄存器后,尝试从 RAM(复制和跳转)而不是闪存运行代码,这样可以在执行时立即将性能提升 2 到 3 倍,而无需执行任何其他操作。

此 MCU 是否会出现这种行为?

这是您的代码的一种行为。

  1. 您应该写入 BRR/BSRR 寄存器,而不是像现在这样读取-修改-写入。

  2. 您还会产生循环开销。为了获得最佳性能,一遍又一遍地复制 BRR/BSRR 操作 → 在循环中多次复制和粘贴它们,这样您就可以在一个循环开销之前经历许多设置/重置循环。

编辑:IAR 下的一些快速测试。

一次翻转写入 BRR/BSRR 需要 6 条中等优化指令和 3 条最高优化指令;翻阅 RMW'ng 需要 10 条指令 / 6 条指令。

额外的循环开销。