arm上延迟循环中每条指令的周期

Posted

技术标签:

【中文标题】arm上延迟循环中每条指令的周期【英文标题】:Cycles per instruction in delay loop on arm 【发布时间】:2015-09-22 14:35:44 【问题描述】:

我试图了解 arm-none-eabi-gcc 为 stm32f103 芯片组生成的一些汇编程序,它的运行速度似乎正好是我预期的一半。我对汇编器不是很熟悉,但是因为每个人都说如果你想了解你的编译器在做什么,请阅读 asm,我看到了我能走多远。它是一个简单的功能:

void delay(volatile uint32_t num)  
    volatile uint32_t index = 0; 
    for(index = (6000 * num); index != 0; index--)  

时钟速度是 72MHz,上面的函数给了我 1ms 的延迟,但我预计是 0.5ms(因为 (6000*6)/72000000 = 0.0005)。

汇编器是这样的:

delay:
        @ args = 0, pretend = 0, frame = 16
        @ frame_needed = 0, uses_anonymous_args = 0
        @ link register save eliminated.
        sub     sp, sp, #16         stack pointer = stack pointer - 16
        movs    r3, #0              move 0 into r3 and update condition flags
        str     r0, [sp, #4]        store r0 at location stack pointer+4
        str     r3, [sp, #12]       store r3 at location stack pointer+12 
        ldr     r3, [sp, #4]        load r3 with data at location stack pointer+4 
        movw    r2, #6000           move 6000 into r2 (make r2 6000)
        mul     r3, r2, r3          r3 = r2 * r3
        str     r3, [sp, #12]       store r3 at stack pointer+12
        ldr     r3, [sp, #12]       load r3 with data at stack pointer+12
        cbz     r3, .L1             Compare and Branch on Zero
.L4:
        ldr     r3, [sp, #12]   2   load r3 with data at location stack pointer+12
        subs    r3, r3, #1      1   subtract 1 from r3 with 'set APSR flag' if any conditions met
        str     r3, [sp, #12]   2   store r3 at location sp+12 
        ldr     r3, [sp, #12]   2   load r3 with data at location sp+12
        cmp     r3, #0          1   status = 0 - r3 (if r3 is 0, set status flag)
        bne     .L4             1   branch to .L4 if not equal
.L1:
        add     sp, sp, #16         add 16 back to the stack pointer
        @ sp needed
        bx      lr
        .size   delay, .-delay
        .align  2
        .global blink
        .thumb
        .thumb_func
        .type   blink, %function

我已经通过查找评论了我认为每条指令的含义。所以我相信 .L4 部分是延迟函数的循环,它有 6 条指令长。我确实意识到时钟周期并不总是与指令相同,但是由于存在如此大的差异,并且由于这是一个我认为可以有效预测和流水线化的循环,我想知道我看到 2 个时钟是否有充分的理由每条指令的周期数。

背景: 在我正在进行的项目中,我需要使用 5 个输出引脚来控制线性 ccd,并且据说时序要求相当严格。绝对频率不会被最大化(我会为引脚提供比 CPU 能力更慢的时钟),但引脚之间的相对时序很重要。因此,与其使用处于我能力极限并且可能使相对时序复杂化的中断,我正在考虑使用循环来提供引脚电压变化事件之间的短延迟(大约 100 ns),或者甚至在展开的汇编程序中对整个部分进行编码,因为我有充足的程序存储空间。有一段时间引脚没有变化,在此期间我可以运行 ADC 对信号进行采样。

虽然我要问的奇怪行为不是表演障碍,但我宁愿在继续之前理解它。

编辑:从评论中,arm tech ref 给出了指令时间。我已将它们添加到程序集中。但它仍然只有 9 个周期,而不是我预期的 12 个。跳跃本身是一个循环吗?

TIA,皮特

我想我必须把这个交给 ElderBug,尽管 Dwelch 提出了一些可能也非常相关的观点,所以谢谢大家。从这个开始,我将尝试使用展开的组件来切换更改时相隔 20ns 的引脚,然后返回 C 等待更长的等待时间,然后进行 ADC 转换,然后返回到组件以重复该过程,密切关注组件gcc 的输出以大致了解我的时间是否正常。顺便说一句,修改后的 wait_cycles 函数确实如您所说的那样工作。再次感谢。

【问题讨论】:

请记住,当您执行加载时,您仍然需要等待该请求进入内存和返回值所需的时间(提示:>0 个周期)在下一条指令可以使用它之前,不管how well the core tries to pipeline everything else。 这个有用吗? ***.com/questions/18220928/… 都很有趣。我没有看过的手臂参考页面,如果我理解正确,我会用似乎是什么时间来更新问题。部分解释。 【参考方案1】:

首先,在 C 中执行自旋等待循环是个坏主意。在这里,我可以看到您使用-O0 编译(没有优化),如果启用优化,您的等待会更短(编辑:实际上,您发布的未优化代码可能只是来自volatile 的结果,但事实并非如此真的很重要)。 C 等待循环不可靠。我维护了一个依赖于这样一个函数的程序,每次我们必须更改编译器标志时,时间都会变得混乱(幸运的是,结果有一个蜂鸣器走调了,提醒我们更改等待循环)。

关于每个周期看不到 1 条指令的原因,是因为有些指令不需要 1 个周期。例如,如果采用分支,bne 可能会花费额外的周期。问题是您可以拥有较少的确定性因素,例如公共汽车使用情况。访问 RAM 意味着使用总线,该总线可能正忙于从 ROM 获取数据或正在被 DMA 使用。这意味着像 STRLDR 这样的指令可能会延迟。在您的示例中,您在同一位置有一个STR,后跟一个LDR(典型的-O0);如果 MCU 没有存储到加载转发,则可能会有延迟。


我为计时所做的是使用硬件计时器来实现 1µs 以上的延迟,并使用硬编码的组装循环来实现非常短的延迟。

对于硬件定时器,您只需设置一个固定频率的定时器(如果您希望延迟精确到 1µs,则周期

void wait_us( uint32_t us ) 
    uint32_t mark = GET_TIMER();
    us *= TIMER_FREQ/1000000;
    while( us > GET_TIMER() - mark );

你甚至可以使用mark作为参数在某些任务之前设置它,然后使用该函数等待剩余时间。示例:

uint32_t mark = GET_TIMER();
some_task();
wait_us( mark, 200 );

对于组装等待,我将这个用于 ARM Cortex-M4(接近你的):

#define CYCLES_PER_LOOP 3
inline void wait_cycles( uint32_t n ) 
    uint32_t l = n/CYCLES_PER_LOOP;
    asm volatile( "0:" "SUBS %[count], 1;" "BNE 0b;" :[count]"+r"(l) );

这非常简短、精确,并且不会受到编译器标志或总线负载的影响。 您可能需要调整 CYCLES_PER_LOOP,但我认为它对于您的 MCU 将具有相同的值(这里是 SUBS+BNE 的 1+2)。

【讨论】:

很好的答案,我必须阅读一下 asm 语法才能理解该宏。但我无法让它工作,我需要将一些东西传递给编译器以使其正常工作吗?我已经包含了与汇编相关的 2 行,用从 1 到 250 的各种 n 值调用它(不确定这个函数是否需要超过 8 个位数),但每次调用它仍然只给我几个延迟周期。我确定我缺少一些编译器标志或其他东西。 @Pete 使用上一代处理器,您可能已经从软件延迟循环中“摆脱”航位推算(忽略可能存在的少数中断),但那些日子已经一去不复返了。延迟的唯一明智方法(即使您有空闲时间)是使用定时器,或者直接使用定时器,或者更好地通过监视由常规定时器中断维护的计数器变量,以程序上层所需的粒度。如果您不得不坐下来等待,没有更好的事情可做,那么该计划的结构很糟糕。 对于大多数用途,我完全理解您的意思 WV。但是我需要在这里以低至 20 ns 的时间将引脚转换分开,而且我认为我不会使用定时器技术来管理它,我认为开销太高了。如果您知道那是错误的,请务必说出该怎么做。为了说明这一点,我需要提供 3 个相隔 20 ns 转换的引脚(即一个引脚变高,下一个 20ns,下一个 20ns 之后)。在专用硬件中执行此操作可能会更好,但如果可以的话,我宁愿避免这样做。 @WeatherVane 当您只需要等待几个周期时,使用计时器并不总是一个好主意。当某些外围设备只需要一些 ns 时,我个人使用汇编循环。在 Op 的情况下,也许他能找到更好的设计,但我认为这仍然是明智的。 @Pete 关于程序集,它不应该需要任何标志。可以肯定的是,您可以这样使用它:WAIT_CYCLES(10);?实际生成的程序集是什么?此外,您可以将BGT 替换为BNE,它应该最多允许n = 2**32 * CYCLES_PER_LOOP,所以这不是问题。【参考方案2】:

这是一个 cortex-m3,所以你的闪存可能用完了?您是否尝试过从 ram 运行和/或调整闪存速度,或调整时钟与闪存速度(减慢主时钟),这样您就可以让闪存尽可能接近每次访问的单个周期。

您还对这些指令的一半进行了内存访问,这是一个或更多周期用于获取(如果您在 sram 上运行在同一时钟上)和另一个时钟用于 ram 访问(由于使用易失性) .所以这可能占每个时钟一个时钟和每个两个时钟之间差异的一定百分比,分支也可能花费超过一个时钟,在 m3 上不确定您是否可以打开或关闭它(分支预测)和分支预测无论如何,它的工作方式有点有趣,如果它太靠近 fetch 块的开头那么它将无法工作,因此分支在 ram 中的位置会影响性能,其中任何一个在 ram 中都会影响性能,您可以通过在代码前面的任何位置添加 nop 来更改循环的对齐方式来进行实验,影响缓存(您可能在这里没有)并且还可以根据指令的大小和位置影响其他事情. (例如,有些手臂一次获取 8 条指令)。

您不仅需要了解汇编以了解您想要做什么,还需要了解如何操作该汇编以及其他事情,例如对齐、重新排列指令组合、有时更多指令比更少指令更快等等。管道和缓存很难预测,如果有的话,并且可以很容易地用手动优化的代码抛弃假设和实验。

即使你克服了慢闪、缺少缓存(尽管你不能依赖它的性能)和其他一些东西,内核和 I/O 之间的逻辑和 I/O 的速度对于 bit banging可能是另一个性能损失,没有理由期望 I/O 每次访问的周期数很少,甚至可能是两位数的时钟数。在这项研究的早期,您需要启动 gpio 只读循环、只写循环和读/写循环。如果您依靠 gpio 逻辑仅触摸端口中的一位而不是可能具有周期成本的整个端口,那么您还需要对其进行性能调整。

如果您甚至接近时序余量并且必须是硬实时的,您可能想要考虑使用 cpld,因为一行额外的代码或编译器的新版本可以完全摆脱时序项目。

【讨论】:

谢谢,这是一个全新的信息负载,我将不得不花一些时间来正确理解和阅读它,所以我今天可能不会这样做。但是,它提出了许多新问题。我越来越想知道我是否真的必须在硬件中实现它......希望不是。 最简单的事情是,如果你有一个调试器,编译/链接并从 ram 加载和运行程序。如果你不能这样做,那么让 rom 中的程序将真实程序复制到 ram 然后分支到它。让闪光灯脱离循环。只是更快地运行这些微控制器并不会自动提高性能,因为闪存的等待状态数量通常会增加,这取决于品牌和系列,可能有人在提高时钟时必须设置一个闪存寄存器,该部分描述了以下比率高水平 你可能没有数据或者我没有缓存,所以你不能直接打开它看看是否有帮助。我的猜测是,对于许多指令(获取本身和数据访问),每条指令都有两个数据周期的组合,因此即使您可以消除所有其他周期,您也不应该在每条指令一个周期,而是 1.5 或 2 个周期钢人队... 另一条评论是我认为 arm 声称 cortex-m 或至少 m3 是哈佛架构,I 和 D 是分开的,但您可以使用数据操作写入 ram 然后执行这些操作,以便有点失败。我愿意打赌,尤其是要成为微控制器。它实际上不是两条总线,而是一条具有不同命令的总线。 之所以这么说,如果它真的是两条总线,那么你的指令获取(在这种情况下是闪存)和数据操作理论上可以在同一个时钟周期上发生,所以你不会为这些数据操作至少消耗两个时钟(如果指令在 i 总线上,闪存和数据在 d 总线 sram 上)。如果您从 ram 运行,尽管指令获取并且数据操作可能必须被序列化,这会花费您更多的时钟......除非他们在这个核心上有多个数据总线......

以上是关于arm上延迟循环中每条指令的周期的主要内容,如果未能解决你的问题,请参考以下文章

ARM指令集Thumb指令集Thumb-2指令集

关于arm指令时序

如何查找 i7 处理器的每条指令的周期

arm指令周期

ARM Cortex A9的流水线介绍

stm32 DSP指令的周期