最快的轮询循环 - 我怎样才能 trim 1 CPU 循环?

Fastest polling loop - how can I trim 1 CPU cycle?

在 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 上可以用 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 emulator 及其 仿真器触发 功能在寄存器地址读取时获得的。之前我在循环中尝试了带有断点的 "States" 计数器,但结果取决于断点的位置。单步更糟:它总是为 LDR 提供 4 个周期,而这至少有时会下降到 3 个。

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

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

如果我对问题的理解正确,则不一定是需要减少的循环周期,而是后续样本(即 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 个周期)。采样率肯定提高了。

值得称赞的是, 提议将 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: