从STM32 MCU获得快速性能


11

我正在使用STM32F303VC 发现套件,但对其性能感到有些困惑。为了熟悉该系统,我编写了一个非常简单的程序,只是为了测试该MCU的位速。该代码可以分解如下:

  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;

      }
}

该代码是通过带有-O1优化的带有GNU ARM嵌入式工具链的CoIDE V2进行编译的。用示波器检查的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倍。有办法加快速度吗?


您是否尝试过与其他MCU进行比较?
MarkoBuršič17年

3
您想达到什么目的?如果要快速振荡输出,则应使用定时器。如果要与快速串行协议接口,则应使用相应的硬件外围设备。
乔纳斯·谢弗

2
从工具包开始!!
Scott Seidman

您不得| = BSRR或BRR寄存器,因为它们是只写的。
P__J__

Answers:


25

这里的问题实际上是:您从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使用r2具有常量的寄存器,该常量1024 == (1<<10)对应于将该寄存器中的第10位设置为1,并将结果写入r2自身。
  • 然后str将结果(存储)到我们在第一步中读取的存储位置中
  • 然后出于懒惰:最有可能BRR是地址,对不同的存储位置重复相同的操作。
  • 最后b(分支)回到第一步。

因此,我们有7条指令,而不是3条。仅b发生一次,因此很可能需要花费奇数个周期(我们总共有13个周期,因此必须来自一个奇数周期)。因为低于13所有的奇数是1,3,5,7,9,11,我们可以排除任何数目大于13-6(假设CPU不能执行一条指令超过一个周期),我们知道这b需要1、3、5或7个CPU周期。

作为我们的本人,我查看了ARM的说明文档以及 M3 花费了多少周期

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

一切都符合您的观察:

13=2Cd[R+CØ[R[R+CsŤ[R+Cb=22+1个+2+3=25+3

如以上计算所示,几乎没有办法使循环更快— ARM处理器上的输出引脚通常是内存映射的,而不是CPU内核寄存器的,因此,如果需要,则必须进行通常的加载–修改–存储例程你想对那些做任何事情。

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

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


现在,我试图重现您的问题与“低”时期越来越长,但我根本进不了-循环长相一模一样的同-O3-O1我的编译器版本。您必须自己做!也许您使用的是GCC较旧的版本,而ARM支持不够理想。


4
正如您所说,不只是存储(=而不是|=)是OP正在寻找的提速对象吗?ARM分别拥有BRR和BSRR寄存器的原因是不需要读-修改-写。在这种情况下,常量可以存储在循环外部的寄存器中,因此内部循环只有2个str和一个分支,因此整轮2 + 2 +3 = 7个周期?
Timo

谢谢。这确实清除了很多东西。坚持只需要3个时钟周期有点草率,我实际上希望的是6到7个周期。-O3清洗并重建解决方案后,该错误似乎消失了。但是,我的汇编代码中似乎还包含其他UTXH指令: .L5: ldrh r3, [r2, #24] uxth r3, r3 orr r3, r3, #1024 strh r3, [r2, #24] @ movhi ldr r3, [r2, #40] orr r3, r3, #1024 str r3, [r2, #40] b .L5
KR

1
uxth之所以存在,GPIO->BSRRL是因为(错误地)在标头中将其定义为16位寄存器。使用STM32CubeF3中标头的最新版本,其中没有BSRRL和BSRRH,只有一个32位BSRR寄存器。@Marcus显然具有正确的标头,因此他的代码执行完整的32位访问,而不是加载半字并将其扩展。
berendi-抗议

为什么加载单个字节需要额外的指令?ARM体系结构具有LDRB并且STRB可以在单个指令中执行字节读/写,不是吗?
psmears '17

1
M3内核可以支持位带(不确定此特定实现是否支持),其中1 MB的外围存储空间区域被别名为32 MB的区域。每个位都有一个离散的字地址(仅使用位0)。大概仍然比加载/存储慢。
肖恩·霍利哈内

8

BSRRBRR寄存器是用于设置和复位独立端口位:

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

...

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

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

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

0:对相应的ODRx位无作用

1:将相应的ODRx位置1

如您所见,读取这些寄存器始终为0,因此您的代码是什么

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

确实是有效GPIOE->BRR = 0 | GPIO_BRR_BR_10的,但优化器不知道,所以它产生的序列LDRORRSTR说明书,而不是一个单店。

您只需编写即可避免昂贵的读取-修改-写入操作

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

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


1

补充说明一下:当然,对于Cortex-M,但是几乎所有处理器(具有管道,缓存,分支预测或其他功能),即使是最简单的循环也很简单:

top:
   subs r0,#1
   bne top

您可以根据需要运行它数百万次,但是能够使该循环的性能差异很大,仅需执行这两个指令,就可以在中间添加一些点。没关系

更改循环的对齐方式可能会极大地改变性能,尤其是在一个小循环中,例如,如果它需要两条读取线而不是一条,那么您将在闪存这样的微控制器上花费额外的成本,该闪存的速度比CPU慢2倍。或3,然后通过增加时钟,该比率甚至比添加额外的访存差3或4或5。

您可能没有缓存,但是如果有缓存,它在某些情况下会有所帮助,但在其他情况下会有所伤害和/或没有作用。您可能在此处或可能没有(可能没有)的分支预测只能看到管道中所设计的范围,因此,即使您将循环更改为分支,并且最后有无条件分支(更容易使用分支预测器,使用),这样做可以为您节省下一次提取的许多时钟(通常从其获取的管道大小到预测变量可以看到的深度)和/或不做预取以防万一。

通过更改获取和缓存行的对齐方式,您可以影响分支预测器是否在帮助您,这可以从整体性能中看出,即使您仅测试两条指令或通过一些测试也可以测试这两条指令。 。

这样做是微不足道的,一旦您了解了这一点,然后进行编译后的代码,甚至是手写的汇编,您就会发现由于这些因素,其性能可能相差很大,从而增加或节省了数百%,一行C代码,一行nop放置不正确。

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


0

此MCU有这种现象吗?

这是您的代码的行为。

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

  2. 您还将招致循环开销。为了获得最佳性能,请一遍又一遍地复制BRR / BSRR操作→将其复制并粘贴到循环中多次,以便在经历一个循环开销之前要经历许多设置/重置周期。

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

对BRR / BSRR的写操作在中等优化下需要6条指令,在最高优化水平下需要3条指令;翻阅RMW'ng需要10条指令/ 6条指令。

循环额外开销。


通过更改|==单个位,设置/复位阶段会消耗9个时钟周期(链接)。汇编代码的长度为3条指令:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
不要手动展开循环。这实际上不是一个好主意。在这种特殊情况下,这尤其是灾难性的:它使波形非周期性。同样,多次在闪存中拥有相同的代码并不一定会更快。这在这里可能不适用(可能!),但是许多人认为循环展开是有帮助的,编译器(gcc -funroll-loops)可以做得很好,并且在被滥用时(如此处)会产生与您想要的相反的效果。
MarcusMüller17年

永远无法有效地展开无限循环以保持一致的定时行为。
MarcusMüller17年

1
@MarcusMüller:有时可以展开无限循环,同时保持一致的时序,如果在循环的某些重复中有任何点导致指令没有可见效果的话。例如,如果somePortLatch控制端口的低4位被设置为输出的端口,则有可能展开while(1) { SomePortLatch ^= (ctr++); }为输出15个值的代码,然后循环返回开始,否则它将连续两次输出相同的值。
超级猫

超级猫,是的。同样,诸如存储器接口的时序等的效果可能使得“部分”展开是明智的。我的发言太笼统了,但我觉得丹尼的建议更加笼统,甚至很危险
MarcusMüller17
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.