尽可能快地设置 STM32 GPIO 时钟和数据引脚

电器工程 stm32
2022-02-01 07:47:55

我有一个 STM32 可以反复切换九个 GPIO 引脚(一个时钟引脚和八个数据引脚,以使用 SelectMap 加载 FPGA 图像)。我正在使用GPIO_WriteBit修改一个 GPIO 位并一次更改一个引脚的标准库函数来执行此操作。

不幸的是,这很慢。有没有办法让“原始 GPIO 切换”非常非常快?我应该更改时钟参数吗?我可以使用某种 FIFO 或基于中断的方法吗?

我已将 GPIO 引脚配置如下:

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

(注意:参考手册(参见第 133 页)指出 GPIO 能够:

Fast toggle capable of changing every two clock cycles

但我看不到如何激活这种快速切换。)

4个回答

设置或清除 I/O 端口中的任何位组合(即使设置某些位和清除其他位)最多需要三个指令;当重复这样的指令时,每个指令应该有一个指令。不幸的是,似乎许多供应商(其中包括 ST)倾向于定义 I/O 库,这些库即使对于诸如此类的常见操作也会生成子例程调用。

我建议定义自己的方法来设置和清除 I/O 位,并__inline在 .h 文件中包含这些方法的代码,用限定符标记[而不是仅在 .h 和方法中包含原型.c 文件中的代码]。

如果在 .h 文件中,则编写:

__inline void SetPorts(GPIO_TypeDef* Addr, uint16_t data) { *Addr = data; }
__inline void ClearPorts(GPIO_TypeDef* Addr, uint16_t data) { *Addr = data; }

然后代码:

setPorts(GPIOA, 1);
clearPorts(GPIOA, 2);
clearPorts(GPIOA, 1);

可能会产生类似的东西(我对一些细节并不积极):

    ldr  r0,=GPIOA
    mov  r1,#1
    strh r1,[r0+20]
    mov  r2,#2
    strh r2,[r0+22]
    strh r1,[r0+22]

相比之下,如果使用GPIO_WriteBit代码最终会更像:

    ldr  r0,=GPIOA
    mov  r1,#1
    mov  r2,#1
    bl   _GPIO_WriteBit
    ldr  r0,=GPIOA
    mov  r1,#2
    mov  r2,#0
    bl   _GPIO_WriteBit
    ldr  r0,=GPIOA
    mov  r1,#2
    mov  r2,#0
    bl   _GPIO_WriteBit

_GPIO_WriteBit: ; Code below assumes pretty good compiler optimization 
    cmp   r2,#0
    itteq
    streq r1,[r0+20]
    strne r1,[r0+22]
    bx    lr

第一个示例对所有三个操作总共执行六条指令。第二个示例在主代码中执行 12 条指令,在 WriteBits 函数中执行大约 4 条指令,每条指令执行 3 次,总共 24 条[跳过的指令有时会增加执行时间,有时不会]。通常调用子程序的目的是在代码大小与执行速度之间进行权衡,但在这种情况下,子程序调用从空间和时间的角度来看都是一场灾难。指令完成的唯一有用工作是单个存储操作;该代码可能会使寄存器 r0-r2 不受干扰,但调用代码将无法知道这一点。因此,必须在每个方法调用之前显式设置所有三个参数。

我不知道为什么芯片供应商会定义写入 GPIO 位但不费心将它们内联的方法,但我建议在大多数情况下,如果有人关心任何事情,应该避免使用芯片供应商提供的函数来写入 GPIO 端口关于效率。

顺便说一下,作为一般性的进一步说明,我通常对供应商提供的 I/O 功能持怀疑态度。虽然它们有时可以为程序员提供比原始硬件更高级别的抽象,这很有用,但在许多情况下,它们会使代码更难编写、更难阅读且效率更低。它们有时还会产生不希望的副作用,可能会导致其他地方出现问题。例如,如果外设需要将时钟设置为两种模式中的一种,并且通常与其中一种模式配合得更好,则供应商提供的外设库可能会将时钟配置为“更好”模式,即使该时钟是与另一个需要它的外围设备共享。当外围设备共享资源时(就像他们经常做的那样),如果不阅读和理解其中的所有代码,可能无法正确使用这些库。如果库的全部目的是保存一些寄存器写入,那么库调用的后果可能比它替换的代码的后果更难弄清楚。

我回答了您的相关问题,为什么您在预期 100 MHz 时只能切换到 4 MHz:

如果 4 MHz大约是4 MHz,而不是 100 MHz/25,那么问题可能出在 C 函数GPIO_WriteBit上。

对于高速操作和需要精确计时的操作,汇编代码比 C 代码更好。如果您查看由GPIO_WriteBit它创建的汇编代码可能有半页长,具体取决于函数具有什么样的功能,以及多少编译器的优化器可以处理它。

您没有说您正在使用哪个开发工具链,但许多/大多数 C 编译器可以处理内联汇编。

所以,在汇编中编写函数。像这样的功能

 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;

在汇编中应该不超过 2 条指令,而编译的 C 代码可能需要 20 倍。或者更多。

您设置的速度仅控制引脚的转换速率。它越快,上升沿越快。它并不直接代表您可以多快切换端口。

该功能允许您与需要特定上升/下降时间的其他设备适当地连接。

您将 GPIO 的速度设置为 100 MHz;这是硬件可以支持的速度限制。但是最终的数据交换速度也可能会受到你的代码运行速度的限制,因为数据时钟等现在是由你的代码驱动的。

在我看来,并不是所有的STM32家族成员都支持100MHz的GPIO速度,有些家族的MCU速度低于这个速度,比如72MHz。如果您的 MCU 支持 100 MHz GPIO,则优化您的代码。否则,选择时钟速度更高的MCU;STM32F429 支持 180 MHz 时钟速度。

请查看 MCU 的数据表。在“I/O AC特性定义”部分,给出了GPIO的电气特性,以及“100 MHz”是如何定义的。并且您应该注意:“对于 50 MHz 以上的最大频率,应使用补偿单元。