20

巧用模板加速引脚电平读写

 4 years ago
source link: http://www.cnblogs.com/jerry-fuyi/p/12773955.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

拉阅读量第二弹,希望你能有所收获。

我不想听你放那么多屁,我只想知道怎么加速 digitalWrite

digitalWrite有多慢

template<typename T>
inline void test(T&& f)
{
  auto start = micros();
  f(); f(); f(); f(); f();
  f(); f(); f(); f(); f();
  auto finish = micros();
  Serial.println(finish - start);
}

void setup() {
  Serial.begin(9600);
  test([] { });
  test([] { pinMode(2, OUTPUT); });
  test([] { digitalWrite(2, HIGH); });
  test([] { shiftOut(2, 4, LSBFIRST, 0); });
}

void loop() {
  digitalWrite(2, LOW);
  digitalWrite(2, HIGH);
}

这个程序测试调用10次某语句需要的时间。在山寨版Uno Rev3上运行,程序输出:

第一组空函数是对照组, 0 的结果表明 test 函数没有什么overhead。第二组 pinMode 的成绩为36μs,无所谓,毕竟 pinMode 是放在初始化里只调用一次的。第三组 digitalWrite 为44μs,平均每次4.4μs,看起来还行,但是第四组 shiftOut 就不太乐观了,每一次需要88.8μs——实际上它调用了24次 digitalWrite

最后,我还用 loop 函数在 2 号引脚上输出了方波,利用逻辑分析仪测得其频率为135kHz。

通常情况下,这个速度已经够了,但是总有追求极致的人,比如我,或者追求极致的项目,不想浪费单片机的每一点性能。

数字IO寄存器

AVR单片机教程——数字IO寄存器

在AVR架构tiny与mega系列的单片机中,每个端口都有3个寄存器控制数字信号IO,分别是PORTx、DDRx和PINx。这里的x是A、B、C或D,由于这4个端口在数字IO方面完全相同,就把它们合并起来讲。相应地,对于每个引脚Pxn,有PORTxn、DDxn(没有R)和PINxn三个bit控制其数字IO。

DDxn控制引脚方向:当DDxn为1时,Pxn为输出;当DDxn为0时,Pxn为输入。

当Pxn为输入时,如果PORTxn为1,则该引脚通过一个上拉电阻连接到VCC;否则引脚悬空。

当Pxn为输出时,如果PORTxn为1,引脚输出高电平;否则输出低电平。

PINxn的值为Pxn引脚的电平。如果给PINxn写入1,PORTxn的值会翻转。

Arduino Uno Rev3的原理图:

6fyeMbm.jpg!web

开发板引脚与单片机引脚的对应关系:

开发板引脚 单片机引脚 0 PD0 1 PD1 2 PD2 3 PD3 4 PD4 5 PD5 6 PD6 7 PD7 8 PB0 9 PB1 10 PB2 11 PB3 12 PB4 13 PB5 A0 PC0 A1 PC1 A2 PC2 A3 PC3 A4 PC4 A5 PC5

digitalWrite 换成寄存器操作,重新测试:

template<typename T>
inline void test(T&& f)
{
  auto start = micros();
  f(); f(); f(); f(); f();
  f(); f(); f(); f(); f();
  auto finish = micros();
  Serial.println(finish - start);
}

void myShiftOut(uint8_t val)
{
  uint8_t i;
  for (i = 0; i < 8; i++)
  {
    if (val & 1 << i)
      PORTD |= 1 << PORTD2;
    else
      PORTD &= ~(1 << PORTD2);
    PORTD |= 1 << PORTD4;
    PORTD &= ~(1 << PORTD4);
  }
}

void setup() {
  Serial.begin(9600);
  test([] { });
  test([] { pinMode(2, OUTPUT); });
  test([] { digitalWrite(2, HIGH); });
  test([] { shiftOut(2, 4, LSBFIRST, 0); });
  test([] { DDRD |= 1 << DDD2; });
  test([] { PORTD |= 1 << PORTD2; });
  test([] { myShiftOut(0); });
}

void loop() {
//  digitalWrite(2, LOW);
//  digitalWrite(2, HIGH);
  PORTD |= 1 << PORTD2;
  PORTD &= ~(1 << PORTD2);
}

输出:

引脚 2 上方波低电平62.5ns,高电平437.5ns(不准确,仪器只有16MHz采样率),频率2.0MHz。

原来, loop 中的两句寄存器操作会编译为以下汇编代码:

cbi 0x0b, 2
sbi 0x0b, 2

sbicbi 都是双周期指令,单片机频率16MHz,理论上用软件最快可以输出4MHz方波。

digitalWrite为何慢

编程中充满了权衡。Arduino库偏向可移植性与易用性,因此性能较差也是常理之中。

#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )
#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
#define digitalPinToTimer(P) ( pgm_read_byte( digital_pin_to_timer_PGM + (P) ) )
#define portOutputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_output_PGM + (P))) )

const uint16_t PROGMEM port_to_output_PGM[] = {
  NOT_A_PORT,
  NOT_A_PORT,
  (uint16_t) &PORTB,
  (uint16_t) &PORTC,
  (uint16_t) &PORTD,
};

const uint8_t PROGMEM digital_pin_to_port_PGM[] = {
  /*  0 */ PD, PD, PD, PD, PD, PD, PD, PD,
  /*  8 */ PB, PB, PB, PB, PB, PB,
  /* 14 */ PC, PC, PC, PC, PC, PC,
};

const uint8_t PROGMEM digital_pin_to_bit_mask_PGM[] = {
  /*  0, port D */ _BV(0), _BV(1), _BV(2), _BV(3), _BV(4), _BV(5), _BV(6), _BV(7),
  /*  8, port B */ _BV(0), _BV(1), _BV(2), _BV(3), _BV(4), _BV(5),
  /* 14, port C */ _BV(0), _BV(1), _BV(2), _BV(3), _BV(4), _BV(5),
};

const uint8_t PROGMEM digital_pin_to_timer_PGM[] = {
  /* 0 - port D */ NOT_ON_TIMER, NOT_ON_TIMER, NOT_ON_TIMER, TIMER2B, NOT_ON_TIMER, TIMER0B, TIMER0A, NOT_ON_TIMER,
  /* 8 - port B */ NOT_ON_TIMER, TIMER1A, TIMER1B, TIMER2A, NOT_ON_TIMER, NOT_ON_TIMER,
  /* 14 - port C */ NOT_ON_TIMER, NOT_ON_TIMER, NOT_ON_TIMER, NOT_ON_TIMER, NOT_ON_TIMER, NOT_ON_TIMER,
};

void digitalWrite(uint8_t pin, uint8_t val)
{
  uint8_t timer = digitalPinToTimer(pin);
  uint8_t bit = digitalPinToBitMask(pin);
  uint8_t port = digitalPinToPort(pin);
  volatile uint8_t *out;

  if (port == NOT_A_PIN) return;

  // If the pin that support PWM output, we need to turn it off
  // before doing a digital write.
  if (timer != NOT_ON_TIMER) turnOffPWM(timer);

  out = portOutputRegister(port);

  uint8_t oldSREG = SREG;
  cli();

  if (val == LOW) {
    *out &= ~bit;
  } else {
    *out |= bit;
  }

  SREG = oldSREG;
}

digitalWrite 的实现分为三个部分:

  1. pin 映射到 timerbitport ,分别表示 pin 在哪个定时器上、对应的bit mask和 PORTx 寄存器的编号,如果在定时器上还要关闭定时器的PWM;

  2. 把编号 port 映射到 PORTx 的指针 out

  3. 关闭全局中断,通过 out 指针对寄存器 PORTx 进行位操作,最后恢复中断状态。

每一步映射都是常数时间的,但是4次加起来就是比较可观的时间了,还要考虑中断,还要通过指针访问寄存器,难怪 digitalWrite 很慢。

我想要加速 digitalWrite ,但是又不想硬编码,即使用 digitalWrite_1(LOW) 这样的形式,需要参数化的引脚编号,怎么办呢?是时候让模板出场了。

C++模板

三五行字肯定讲不清模板,这里只介绍一些基本概念和后面会用到的语法。

在C++中,模板是一系列类、一系列函数或一系列变量(C++14),对于每一组模板参数,类/函数/变量模板都会实例化为一个模板类/函数/变量。模板参数可以是类型、非类型常量或另一个模板。

对于非类型模板参数,实例化所用参数必须是编译期常量。参数可以进行隐式类型转换,包括整值提升但不包括窄化转换。

对于函数模板,如果可以从函数参数类型推导出模板参数,则可以无需指明模板参数。在重载决议时,模板函数的优先级位于非模板函数之后。

模板可以特化,为一种或一系列特定的模板参数提供特殊的实现,其他的仍然遵循主模板的实现。模板参数全部指定的称为全特化,部分指定的称为偏特化,模板函数不能偏特化。从C++11开始,主模板可以是 delete 的。所有特化都必须出现在第一次实例化之前。

digitalWrite函数模板

digitalWrite 可以改写成函数模板,引脚编号为模板参数:

template<int P>
void digitalWrite(uint8_t) = delete;

template<>
inline void digitalWrite<0>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD0;
  else
    PORTD &= ~(1 << PORTD0);
}
template<>
inline void digitalWrite<1>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD1;
  else
    PORTD &= ~(1 << PORTD1);
}
template<>
inline void digitalWrite<2>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD2;
  else
    PORTD &= ~(1 << PORTD2);
}
template<>
inline void digitalWrite<3>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD3;
  else
    PORTD &= ~(1 << PORTD3);
}
template<>
inline void digitalWrite<4>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD4;
  else
    PORTD &= ~(1 << PORTD4);
}
template<>
inline void digitalWrite<5>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD5;
  else
    PORTD &= ~(1 << PORTD5);
}
template<>
inline void digitalWrite<6>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD6;
  else
    PORTD &= ~(1 << PORTD6);
}
template<>
inline void digitalWrite<7>(uint8_t level)
{
  if (level)
    PORTD |= 1 << PORTD7;
  else
    PORTD &= ~(1 << PORTD7);
}
template<>
inline void digitalWrite<8>(uint8_t level)
{
  if (level)
    PORTB |= 1 << PORTB0;
  else
    PORTB &= ~(1 << PORTB0);
}
template<>
inline void digitalWrite<9>(uint8_t level)
{
  if (level)
    PORTB |= 1 << PORTB1;
  else
    PORTB &= ~(1 << PORTB1);
}
template<>
inline void digitalWrite<10>(uint8_t level)
{
  if (level)
    PORTB |= 1 << PORTB2;
  else
    PORTB &= ~(1 << PORTB2);
}
template<>
inline void digitalWrite<11>(uint8_t level)
{
  if (level)
    PORTB |= 1 << PORTB3;
  else
    PORTB &= ~(1 << PORTB3);
}
template<>
inline void digitalWrite<12>(uint8_t level)
{
  if (level)
    PORTB |= 1 << PORTB4;
  else
    PORTB &= ~(1 << PORTB4);
}
template<>
inline void digitalWrite<13>(uint8_t level)
{
  if (level)
    PORTB |= 1 << PORTB5;
  else
    PORTB &= ~(1 << PORTB5);
}
template<>
inline void digitalWrite<A0>(uint8_t level)
{
  if (level)
    PORTC |= 1 << PORTC0;
  else
    PORTC &= ~(1 << PORTC0);
}
template<>
inline void digitalWrite<A1>(uint8_t level)
{
  if (level)
    PORTC |= 1 << PORTC1;
  else
    PORTC &= ~(1 << PORTC1);
}
template<>
inline void digitalWrite<A2>(uint8_t level)
{
  if (level)
    PORTC |= 1 << PORTC2;
  else
    PORTC &= ~(1 << PORTC2);
}
template<>
inline void digitalWrite<A3>(uint8_t level)
{
  if (level)
    PORTC |= 1 << PORTC3;
  else
    PORTC &= ~(1 << PORTC3);
}
template<>
inline void digitalWrite<A4>(uint8_t level)
{
  if (level)
    PORTC |= 1 << PORTC4;
  else
    PORTC &= ~(1 << PORTC4);
}
template<>
inline void digitalWrite<A5>(uint8_t level)
{
  if (level)
    PORTC |= 1 << PORTC5;
  else
    PORTC &= ~(1 << PORTC5);
}

测试一下性能:

template<typename T>
inline void test(T&& f)
{
  auto start = micros();
  f(); f(); f(); f(); f();
  f(); f(); f(); f(); f();
  auto finish = micros();
  Serial.println(finish - start);
}

void setup() {
  Serial.begin(9600);
  test([] { });
  test([] { digitalWrite(2, HIGH); });
  test([] { PORTD |= 1 << PORTD2; });
  test([] { digitalWrite<2>(HIGH); });
  pinMode(2, OUTPUT);
}

void loop() {
//  digitalWrite(2, LOW);
//  digitalWrite(2, HIGH);
//  PORTD |= 1 << PORTD2;
//  PORTD &= ~(1 << PORTD2);
  digitalWrite<2>(HIGH);
  digitalWrite<2>(LOW);
}

程序输出:

逻辑分析仪测得方波频率为2.0MHz,这表明模板 digitalWrite 的性能与直接寄存器操作相当。

讨论

高性能源于信息的编译期可知性。 digitalWrite<Pin>(HIGH) 中的 Pin 必须是编译期常量,这使编译器可以调用对应的函数,无需表格、寻址等一系列操作。 Pin 不能是函数参数,这限制了它的适用范围。

为了在保留非模板 digitalWrite 的通用性的同时获得模板 digitalWrite 的高性能,由于参数数量不同,两个版本可以共存,客户可以按需取用。如果Arduino库中同时存在两者,较好的实现方法是定义函数指针数组存放模板 digitalWrite 的指针,非模板 digitalWrite 通过函数指针调用。

Arduino的 digitalWrite 实现是分组讨论的,可以减少代码长度,而模板 digitalWrite 必须对每一个引脚进行特化。解决方案有:

  1. 仅对有需求的引脚特化模板,其余沿用非模板 digitalWrite ,用20%的时间优化80%的代码,把工作量花在刀刃上;

  2. 见思考题3;

  3. 使用特殊的模板技巧:

namespace std
{
  template<bool B, typename T = void>
  struct enable_if { };
  template<typename T>
  struct enable_if<true, T>
  {
    using type = T;
  };
  template<bool B, typename T = void>
  using enable_if_t = typename enable_if<B, T>::type;
}

namespace detail
{
  inline void digitalWriteImpl(bool level, volatile uint8_t& reg, uint8_t bit)
  {
    if (level)
      reg |= 1 << bit;
    else
      reg &= ~(1 << bit);
  }
}

template<int P>
inline std::enable_if_t<(P >= 0 && P < 8)> digitalWrite(uint8_t level)
{
  detail::digitalWriteImpl(level, PORTD, P);
}

template<int P>
inline std::enable_if_t<(P >= 8 && P < 14)> digitalWrite(uint8_t level)
{
  detail::digitalWriteImpl(level, PORTB, P - 8);
}

template<int P>
inline std::enable_if_t<(P >= 14 && P < 20)> digitalWrite(uint8_t level)
{
  detail::digitalWriteImpl(level, PORTC, P - 14);
}

模板 digitalWrite 声明为 inline ,事实上在头文件中定义 inline 函数和声明并在源文件中实现都是可行的。当编译器或链接器内联该函数时,代码体积增加,运行性能提高。对于 inline 函数和“偏特化”的函数,头文件中需要提供实现,无法隐藏,但是Arduino作为开源社区很少考虑这一点。

调用处的模板参数不能来自函数参数,但可以来自调用者的模板参数,基于非模板 digitalWrite 的函数都可以改写成基于模板 digitalWrite 的模板函数,如 shiftOut

void myShiftOut(uint8_t val)
{
  uint8_t i;
  for (i = 0; i < 8; i++)
  {
    if (val & 1 << i)
      PORTD |= 1 << PORTD2;
    else
      PORTD &= ~(1 << PORTD2);
    PORTD |= 1 << PORTD4;
    PORTD &= ~(1 << PORTD4);
  }
}

template<int Data, int Clock>
void shiftOut(uint8_t bitOrder, uint8_t val)
{
  uint8_t i;
  for (i = 0; i < 8; i++)
  {
    if (bitOrder == LSBFIRST)
      digitalWrite<Data>(val & 1 << i);
    else
      digitalWrite<Data>(val & 1 << (7 - i));
    digitalWrite<Clock>(HIGH);
    digitalWrite<Clock>(LOW);
  }
}

template<typename T>
inline void test(T&& f)
{
  auto start = micros();
  f(); f(); f(); f(); f();
  f(); f(); f(); f(); f();
  auto finish = micros();
  Serial.println(finish - start);
}

void setup() {
  Serial.begin(9600);
  test([] { });
  test([] { shiftOut(2, 4, LSBFIRST, 0); });
  test([] { myShiftOut(0); });
  test([] { shiftOut<2, 4>(LSBFIRST, 0); });
  pinMode(2, OUTPUT);
}

void loop() {
  
}

然而,非模板情况下 shiftOut(2, 4, LSBFIRST, 0)shiftOut(7, 8, LSBFIRST, 0) 是同一个函数,而模板函数 shiftOut<2, 4>(LSBFIRST, 0)shiftOut<7, 8>(LSBFIRST, 0) 则是两个函数,当模板实例较多时程序体积会显著增大,而换来的则是15倍以上的速度提升。

思考题

  1. 把更多函数改写成模板形式,如 pinModedigitalReadanalogWriteshiftIn 等。

  2. * 把模板 shiftOut 的参数 bitOrder 改为模板参数。

  3. 模板 digitalWrite 的编写过程非常机械,尝试写一个程序,用配置文件来生成代码。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK