外设读取 vs RAM 读取 + RAM 写入

Peripheral read vs RAM read + RAM write

我在使用 IOs 调试关键代码时遇到了一个难题:

这两个函数之间最快的是什么?
我的 CPU 在哪个功能上花费的时间会更少?

A : CPU读取外设寄存器并写入外设寄存器

void d_toggle_pin(void)
{ 
  NRF_P1->OUT ^= 1 << Debug_Pin; 
}

B : CPU读取一个RAM变量并写入外设寄存器+写入一个RAM变量

void d_toggle_pin(void)
{ 
  static byte pin_state = 0;
  if(pin_state) 
  { 
    NRF_P1->OUTCLR = 1U << Debug_Pin;  
    pin_state = 0;
  }
  else
  { 
    NRF_P1->OUTSET = 1U << Debug_Pin;  
    pin_state = 1;
  }
}

我正在使用 nrf52840(皮质 M4 CPU),但也许无论实现如何,答案都是一样的

TL;DR:第一个版本性能更好。


性能方面的差异微不足道。 Cortex M3 及更高版本具有简单的分支预测和流水线,但这不会对这里的这个简单的小代码产生太大影响。当然,第二个版本在分支预测器上可能会稍微粗糙一些,因为它们是两个独立的 memory-mapped 寄存器,但差异可以忽略不计。

如果您坚持比较它们,那么这里有一个 gcc ARM 的小基准测试 non-eabi -O3 我在其中替换了寄存器名称并将“调试引脚”设为硬编码常量:https://godbolt.org/z/88vn1EqKj.该分支已被优化掉,但第一个版本的性能仍然略好。


然而,您在这里的首要任务应该是功能性和可读性。这两个函数都还可以,但是如果我来剖析一下...

  • XOR 版本的优点是 XOR 是一种切换位的惯用方式,因此它是可读的。您还可以保证代码始终与实际寄存器值同步,以防万一。

  • XOR 版本的缺点是 read-modify-write 访问硬件寄存器有时会出现问题,因为它会引入副作用,在某些情况下可能会导致 re-entrancy问题太多。因此,与其将寄存器值用作 XOR 的占位符,我认为您的其他版本单独跟踪端口并仅执行写访问就可以了。


其他注意事项:

1 << ... 在 C 中总是错误的。几乎可以肯定的是,您永远不应该移动带符号的 int,这是整数常量 1 的类型。例如 1 << 31 调用未定义的行为。始终使用 1u.

为像 setting/clearing/toggling GPIO pin 这样的非常基础的东西编写包装函数已经完成了数百次......而且没有人设法编写比这更容易阅读的函数包装器:

  • reg |= mask(套)
  • reg &= ~mask(清除)
  • reg ^= mask;(切换)

这是惯用语,super-fast,super-readable C 代码,100% 的 C 程序员都可以轻松理解。在查看了数百个失败的、臃肿的 GPIO HAL 之后,我可以自信地说简单 GPIO 的抽象可以而且只会导致臃肿。我自己写了很多这样的东西,但总是出错。

(对于带有一堆路由寄存器、中断处理、奇怪的状态标志等的更复杂的 GPIO,请务必编写一个 HAL 和一个驱动程序。但不是为了只做简单的端口 I/O.)