STM32f103c8 gpio限速

Posted

技术标签:

【中文标题】STM32f103c8 gpio限速【英文标题】:STM32f103c8 gpio speed limit 【发布时间】:2020-04-29 16:42:33 【问题描述】:

我有这个简单的内联汇编代码:

__asm__ volatile (

    ".equ GPIOA_ODR, 0x4001080C \n\t" //GPIOA base address is 0x40010800 and ODR offset is 0x0C


    //turns on PA8
    "ldr r1, =(1 << 8)     \n\t"        
    "ldr r2, =#GPIOA_ODR   \n\t"     
    "str r1, [r2]          \n\t"   

    //turn off PA8
    "ldr r1, =0            \n\t"        
    "ldr r2, =#GPIOA_ODR   \n\t"     
    "str r1, [r2]          \n\t"          

);

PA8 只在 2.4MHz 振荡,我想要 36MHz 的速度。我之前尝试过使用计时器并达到了 36MHz 的速度,但由于一些限制,我想避免使用它们。

我不明白为什么 TIMER1 通道 1 (PA8) 可以配置为 36MHz 的开关速度,但是当我尝试在组装中做同样的事情时,我只能在同一个引脚上达到 2.4MHz 的速度。

我也在使用 PinMode(PA8, OUTPUT); 设置 pin

我已经尝试过此汇编代码的其他变体,但在 PA8 上最高只能达到 2.8MHz。我的问题是:STM32f103C8 上的 GPIO 引脚上的开关速度是否高于 2.4-2.8MHz?

(这是Need Help Manipulating Registers in Inline Assembly (STM32F103 "BluePill")之后的后续问题)

【问题讨论】:

一般使用软件会有相当多的开销,如果你想要的只是一个脉冲,你的代码可能会更有效率。所以根据你对上一个问题的编辑,你真的读过那篇文章吗?请注意,stm32f1 和 stm32f4 是不同的芯片,具有不同的性能。如果 stm32f103c8 在 gpio 前面有 dma,那么您可以像作者那样使用它 如果您想模仿作者所做的软件实验,那么您需要更好地了解系统,正如您在上一个问题中所指出的那样。但无论如何,您在发布的代码中创建的开销是没有理由的。预先设置带有地址和数据的寄存器,通过使用一系列 str 指令执行一系列开关。从闪存运行它,从内存运行它,在一个循环中运行它,每个循环一对一地运行(四个指令 str、str、subs bnz,所有 16 位拇指而不是拇指 2)。然后尝试使用更多的 strs 对,例如 4、8、16、32 检查示波器上的输出,看看它在从 st 上的闪存运行时第一次通过循环时的行为,随着 stm32f103 的使用年限,它是否有闪存缓存?接下来的循环呢?你能看到循环结束时的延迟吗(应该可以)。长时间线性运行无循环等情况如何?输出与系统时钟和外围时钟速度相比如何? 如果您使用 ldm 和 str 以便您可以从 ram 读取数据然后将其泵入 gpio 端口会怎样。 理解当你切换到一个足够快的芯片来做你想做的事情时(这不是你所拥有的),那么你必须重复所有这些,因为时间可能会改变。 【参考方案1】:

STM32F103C8 以最高 72 MHz 的时钟速度运行。因此 36 MHz 是可以在 GPIO 上生成的最大频率,因为需要单独的时钟周期来设置和清除引脚。这个频率只能通过定时器来实现。

如果您尝试对代码进行相同操作,则至少需要三个指令:两个商店和一个分支。这些指令需要大约 6 个时钟周期来执行,因此会产生大约 12 Mhz 的最大频率。

为了在软件中实现这一点,您的代码应如下所示:

while (1) 
    GPIOA->ODR = 1 << 8;
    GPIOA->ODR = 0;

不应该需要汇编代码,因为编译器会提供最佳代码。它看起来像这样:

        ldr     r3, .L3
        movs    r1, #128
        movs    r2, #0
.L2:
        str     r1, [r3]
        str     r2, [r3]
        b       .L2
.L3:
        .word   1207959572

更新

我已经在真实世界的设备上对其进行了测试,我得到了 8 MHz 的频率。我估计这三个指令需要 6 个时钟周期,但似乎需要 9 个周期。

生成的代码或多或少符合预期:

7a:   60d9            str     r1, [r3, #12]
7c:   60da            str     r2, [r3, #12]
7e:   e7fc            b.n     7a <main+0x7a>

范围清楚地表明所有三个指令都花费相同的时间。

【讨论】:

“这个频率只能通过定时器来实现”我想了解为什么会这样。我了解基本的 36 MHz 限制,但我不明白为什么 TIMER1 和 GPIOA 在同一总线上无法达到相同的速度。您还说您的第一个示例代码应该可以使用 12MHz。然而,使用相同的代码我仍然只能在 PA8 上达到 2.8MHz,那么是什么阻碍了我从 12MHz 回到这里? @SirSpunk:36MHz 显然只有在字面上执行的每条指令都是存储时才有可能,因为您的 72MHz CPU 每个周期只执行 1 条指令。没有任何循环开销的空间,所以你必须大规模展开,我猜?或者它可以在与商店相同的周期中运行无条件分支吗?无论如何,您问题中的代码显然很慢,因为您使用了两次 ldr reg, =#address (编译器无法将其提升出循环),并且可能将其放入 C 循环中。就像人们在上一个问题告诉你的那样,使用 volatile 会比你写的代码更快。 @SirSpunk:或者你的意思是这个答案中的代码?编译的时候忘记开启优化了吗? @PeterCordes 问题是我试过“static volatile uint16 GPIO_ODR = (volatile uint16 *)&GPIOA_BASE->ODR;” (全局定义)带有“volatile uint16 odr = (GPIO_ODR); *odr = (1 @SirSpunk:我的回答的第二段解释了只能通过计时器实现 36 MHz 的原因。将引脚设置为高电平和低电平的最短代码大约需要 6 个时钟周期。因此 72 MHz 的时钟频率除以 6 个周期为 12 MHz。因此:硬件(定时器,可能是 DMA 或 SPI 时钟线)解决方案:36 MHz;软件解决方案 12 MHz。【参考方案2】:

我想回答我自己的问题,因为在这个问题上得到一个可靠的答案是令人沮丧的,而且在你进行实际测试之前结果并不明显。 Old_timer 的 cmets 被证明是最有帮助的。

void setup()

#define FLASH_ACR (*(volatile uint32_t *)(0x40022000))
FLASH_ACR = 0b110010; //enable flash prefetch and wait states 

pinMode(PA8, OUTPUT);

__asm__ volatile (

"ldr r0, =(0x4001080C) \n\t" //GPIOA ODR
"ldr r1, =(1<<8) \n\t" //turn on PA8
"ldr r2, =0 \n\t" //turn off PA8
".loop: \n\t"
"str r1, [r0] \n\t" // ON and OFF commands are unrolled (repeated) about 100 times
"str r2, [r0] \n\t" // inside the loop
"b .loop \n\t"

); 

当 MCU 以 72MHz 运行时,我使用上面的代码在 PA8 上获得了非常接近 18MHz 的切换速度。据我了解,使用立即数或 XOR 指令可以更快地切换引脚(以及您可能做的其他事情),这是因为某些指令或某些编码方法使用的时钟周期更少,从而导致更快的性能。

如果您还查看 STM32f103 PDF:

https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&ved=2ahUKEwjT-qaNw53nAhUZac0KHe8_Cb8QFjADegQIBBAB&url=https%3A%2F%2Fwww.st.com%2Fresource%2Fen%2Fdatasheet%2Fstm32f103c8.pdf&usg=AOvVaw0rd6I_7fuhTLdZOoycvGV5

您将在第 20 页第 2.3.21 节中看到“APB2 上的 I/O 具有高达 18 MHz 的切换速度”。所以我想如果文档中提到了它,我会达到一个限制。如果您还看一下第 66 页,您会看到一个带有“I/O AC 特性”的漂亮表格,它表明您可以达到 50MHz。

因此,在达到几乎 18MHz 后,我决定将电路板超频至 128MHz,并在 PA8 上实现几乎 32MHz 的切换速度,引脚上的电压为 1.6VDC。现在我很满意,感谢所有 cmets 和帮助家伙。我仍然是这方面的初学者,但我想我现在已经理解了很多。

【讨论】:

据我了解,使用立即数或 XOR 指令可以更快地切换引脚这没有意义; ARM 没有立即值存储或内存目标 XOR。背靠背str 指令(展开以隐藏循环开销)是接近最大 CPU 驱动吞吐量的唯一方法。这意味着在循环之外设置寄存器,就像你在这个答案中所做的那样。 你不需要那个godbolt.org/z/C2BtZe的汇编器 @PeterCordes 你是对的,我在这个特定的 CPU 上犯了一个错误。但是在不同的 CPU 上,我听说您可以使用不同的技巧,例如使用 XOR 在循环内更快地切换某些东西,因为它使用的时钟周期更少。或者使用立即数可以节省几个时钟周期。在某些情况下是这样还是我弄错了?【参考方案3】:

我可能完全不在这里,因为我不为您的平台编写代码...

无论如何,GPIO 直接映射到内存或寄存器的时代已经很遥远了。现代 MCU 具有通过接口(通常是内存映射寄存器)与 MCU CPU 内核互连的 GPIO 接口,您可以在其中对 GPIO 命令进行排队,而不是直接操作 GPIO 位。

定时器绕过这个接口,因此速度更快。但是,如果有一些方法可以提高 MCU 轮询 GPIO 的速度:

    GPIO API 时钟

    MCU CPU 内核和 GPIO 模块之间的 API(接口)通常由单独的时钟控制。如果设置为慢速,无论 MCU 时钟或 GPIO 功能如何,GPIO 也会很慢。

    所以尽量去寻找它并尽可能地增强它。

    GPIO 组

    GPIO 引脚通常分组为共享相同 API 寄存器的端口。因此,通常可以以与处理单个引脚相同的速度一次处理同一组中的所有引脚。因此,如果您仔细选择您使用的引脚,您可以大幅调整极化频率。

    因此,如果可能的话,只使用单个组...计算所有引脚的操作,然后使用 GPIO api 一次性设置/清除/切换所有内容,而不是一个一个。

    DMA

    一些 MCU 允许在内存和 GPIO 之间进行 DMA,您可以绕过 GPIO API 并获得与计时器相似的速度。只需创建内存缓冲区,其中所有位状态都预先以一定的采样率预先计算,然后使用 DMA 在 GPIO 上“播放”它,就像在声卡上播放 wav 文件一样......

    不使用 GPIO

    有些 MCU 根本不是为 GPIO 速度而构建的,而是为了更多计算能力或不同目的而构建的,在这种情况下,无论您做什么,都不会大幅提高 GPIO 速度。在这种情况下,MCU 通常配备接口,用于与外部存储器、IDE、LCD、SPI、USART 等不同硬件互连。

    其中一些可以用来代替 GPIO,外部存储器的接口通常很快,并且支持 DMA,即使 GPIO 太慢也能实现快速传输速度......例如:VGA pixel grouping on STM32

    只是为了比较,我习惯了 AVR32 UC3 MCU,它在 ~66MHz CPU 时钟上具有 ~5MHz GPIO 切换频率(轮询)......但通过使用接口,我甚至可以拥有 33MHz 采样率......

    问题是这样的接口通常没有很多可供使用的引脚,而且有时它们被共享或时间映射为总线,在这种情况下,您有时需要向硬件添加一些额外的东西(如二极管+电容器,或 LATCH 或 (DE)MUX ...) 以避免故障

【讨论】:

以上是关于STM32f103c8 gpio限速的主要内容,如果未能解决你的问题,请参考以下文章

STM32F103 GPIO 端口

STM32F103C8Z6流水灯程序

点亮STM32F103C8T6上的LED

基于STM32F103c8t6的智能垃圾桶项目

基于STM32F103c8t6的智能垃圾桶项目

基于STM32F103c8t6的智能垃圾桶项目