最快的轮询循环 - 如何修剪 1 个 CPU 周期?



在 ARM Cortex M3 上的实时应用程序¹(类似于 STM32F101(中,我需要轮询一些内部外设寄存器,直到它为零,在尽可能紧密的循环中。我使用位带来访问适当的位。(工作(C 代码是

while (*(volatile uint32_t*)kMyBit != 0);

该代码被复制到片上可执行RAM中。经过一些手动优化²,轮询循环归结为以下内容,我定时³为6个周期:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 D1FC      BNE      0x00600200

如何降低投票的不确定性?一个 5 周期的循环符合我的目标:在它归零后尽可能接近 15.5 个周期。

我的规范要求可靠地检测至少6.5个CPU时钟周期的低脉冲;如果持续时间少于12.5个周期,则可靠地将其分类为短脉冲;如果持续时间超过18.5个周期,则可靠地将其分类为长脉冲。脉冲与CPU时钟没有明确的相位关系,这是我唯一准确的时序参考。这需要最多 5 个时钟的轮询循环。实际上,我正在模拟在几十年前的 8 位 CPU 上运行的代码,该 CPU 可以以 5 个时钟周期进行轮询,它所做的已经成为规范。


我试图通过在循环之前插入 NOP 来抵消代码对齐方式,在我尝试的许多变体中,但从未观察到变化。

我试图反转 CMP 和 LDR,但仍然得到 6 个周期:

0x00600200 681A      LDR      r2,[r3,#0x00]
; we loop here
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FC      BNE      0x00600202

这个是 8 个周期

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 681A      LDR      r2,[r3,#0x00]
0x00600204 2A00      CMP      r2,#0x00
0x00600206 D1FB      BNE      0x00600200

但这是 9 个周期:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FB      BNE      0x00600200

¹ 测量在没有发生中断的上下文中位的低时间。

² 初始编译器生成的代码使用 r12 作为目标寄存器,并在循环中增加了 4 个代码字节,成本为 1 个周期。

³ 给出的数字是使用一个据称周期精确的实时STIce仿真器及其仿真器触发功能在寄存地址读取时获得的。以前我尝试在循环中使用断点的"States"计数器,但结果取决于断点的位置。单步更糟糕:它总是为 LDR 提供 4 个周期,而至少在某个时候下降到 3 个。

如果我正确理解了这个问题,那么需要减少的不一定是循环周期,而是后续样本之间的循环数(即LDR指令(。但每次迭代可以有多个 LDR。你可以尝试这样的事情:

ldrb    r1, [r0]
loop:
cbz     r1, out
ldrb    r2, [r0]
cbz     r2, out
ldrb    r1, [r0]
b       loop
out:

两个LDRB指令之间的间距不同,因此样本间距不均匀。

这可能会稍微延迟退出循环,但从问题描述来看,我不能说它是否重要。

我碰巧可以访问周期精确的 M7 模型,当过程稳定时,您的原始循环在 M7 上运行,每次迭代 3 个周期(意味着每 3 个周期运行 LDR(,而上面提出的循环在 4 个周期中运行,但现在有两个 LDR(所以 LDR 每 2 个周期(。采样率肯定有所提高。

为了给予信任,@Peter Cordes在评论中提出了与CBZ一起展开作为休息。

诚然,M3 会更慢,但如果它是您所追求的采样率,它仍然值得一试。

您还可以检查 LDRB 而不是 LDR(如上面的代码中(是否更改了任何内容,尽管我不希望它改变。

UPD:我有另一个 2-LDR 循环版本,它在 M7 上以 3 个周期完成,您可以出于兴趣尝试(CBZ 中断也允许在循环后轻松平衡路径(:

ldr     r1, [r0]
loop:
ldr     r2, [r0]
cbz     r1, out_slow
cbz     r2, out_fast
ldr     r1, [r0]
b       loop
out_fast:
/* NOPs as required */
out_slow:

你可以试试这个,但我怀疑它会给出相同的 6 个周期

0x00600200 581a      LDR      r2,[r3,r0]; initialize r0 to 0x0
0x00600202 4282      CMP      r2,r0
0x00600204 D1FC      BNE      0x00600200

最新更新