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

8
在一个实时应用程序¹中,我需要在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时钟没有定义的相位关系,而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模拟器及其读取寄存器地址的模拟触发器功能获得的。之前我尝试了在循环中设置断点的“状态”计数器,但结果取决于断点的位置。单步执行甚至更糟:它总是给出LDR的4个周期,而实际上有时可以减少到3个周期。


1
Thumb模式没有cbnz指令来比较并跳转到另一个寄存器为零吗?您是否使用“gcc -Os -mcpu=cortex-m3”编译了? - Peter Cordes
1
@Peter Cordes:我没有使用gcc,而是ArmCC 5(ARM在转向LLVM之前的上一代编译器)。优化是为了时间和最大化效果,CPU选项应该由IDE自动设置,但我会检查一下。是的,有CBZ/CBNZ,但根据我阅读的文档,它不能向后跳转。 - fgrieu
1
好的,那么你(或编译器)可以使用“ldr”/“cbz reg,end_of_loop”展开内部循环,但底部仍需要“cmp”/“bnz”。但这将给您一个非均匀的轮询间隔,例如每8次轮询中的1次,如果这很重要的话。 - Peter Cordes
2
你确定你没有误解规格吗?也许规格是指“不是CPU周期的设备特定周期”(例如,具有自己时钟源的计时器或UART,其周期要慢得多),也许“短至13个设备特定周期”可以理解为“短至13000个CPU特定周期”。 - Brendan
1
@brendan:为了澄清:tc代表时钟周期时间,抱歉之前没有表述得够清楚。短脉冲的最小持续时间是6.5个时钟周期时间,最大为12.5个;长脉冲至少持续18.5个时钟周期时间。从CPU的角度看,6.5、12.5和18.5是相关限制,而与时钟速度无关。我希望当Ross Ridge提出问题时就已经说明这一点了。 - fgrieu
显示剩余13条评论
2个回答

8
如果我理解问题正确,需要减少的不一定是循环次数,而是在连续样本(即LDR指令)之间的循环次数。但每次迭代中可能有多个LDR。您可以尝试以下内容:
    ldrb    r1, [r0]

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

out:

两个LDRB指令之间的间隔不同,因此样本不是均匀间隔的。
这可能会稍微延迟退出循环,但从问题描述中我不能说它是否重要。
我碰巧可以访问逐周期的M7模型,当过程稳定后,您原始的循环在每次迭代中以3个周期的速度在M7上运行(意味着每3个周期LDR一次),而上面提出的循环以4个周期运行,但现在有两个LDRs(所以每2个周期LDR一次)。采样率明显提高了。
为了表示赞赏,使用CBZ作为中断的展开是由@Peter Cordes in a comment提出的。
诚然,M3将会更慢,但如果你想要采样率,那么仍然值得一试。
另外,您可以检查LDRB而不是LDR(如上面的代码)是否会改变任何内容,尽管我不指望它会。

更新:我有另一个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:

好消息:每个循环运行10个周期并进行2个采样,因此平均采样率还可以。严重问题是:从发送到 r2 的采样到发送到 r1 的采样之间的延迟为4个周期,但是从发送到 r1 的采样到发送到 r2 的采样之间的延迟为6个周期(由于中间有 b loop),而我希望最多在采样之间间隔5个周期。一个容易解决的问题是,如果退出是因为 r1 为零,那么在采样后跟随 out 的延迟会比因为 r2 为零时更大。另外,从位带地址的 ldrb 引起了问题,改为使用 ldr - fgrieu
2
很高兴它起作用了 :) 没有想要在确认基本想法适合你之前进行样本均衡化。此外,很难预测我的M7运行如何转换为M3。我给你那个循环版本,因为它在M7上产生了统一的采样率(每2个周期LDR)。但我也有另一个版本,实际上在M7上更快(每个循环3个周期仍然有2个LDR),您可以出于兴趣尝试一下。我会更新我的答案。 - alex_mv
3
我确认你的第二个轮询循环在我的Cortex-M3模拟器上运行了10个周期,每个周期有两个等间隔的样本。它比我以前的答案(现已删除)更简单,并允许测试更短的脉冲。在 loop: 之前加入2个 nop ,在 out_fast: 之后加入4个 nop ,这样在 out_slow: 之后进行的 ldr 操作将在看到零点处首次出现的样本之后的10个周期内采样,无论是哪个样本。按照我在问题中描述的规范需要13个周期,这很容易调整。问题100%解决!非常感谢,还要感谢Peter Cordes发表的评论和B Degan提供的首个悬赏。 - fgrieu
@fgrieu:哦,是的,这是最新更新中的一个聪明技巧。out_fast可能比5个nop更紧凑,也许只需要执行另一个ldr,或者可能执行一个b到下一条指令,如果在不污染分支预测(如果有的话)的情况下,它需要更多的周期比CPU上的NOP。 - Peter Cordes

1
你可以尝试这个,但我怀疑它会得到相同的6个周期。
0x00600200 581a      LDR      r2,[r3,r0]; initialize r0 to 0x0
0x00600202 4282      CMP      r2,r0
0x00600204 D1FC      BNE      0x00600200

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接