<div> 
拉閱讀量第二彈,希望你能有所收穫。 我不想聽你放那麼多屁,我只想知道怎麼加速 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的原理圖: 開發板引腳與單片機引腳的對應關係:
開發板引腳 單片機引腳
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 的編寫過程非常機械,嘗試寫一個程序,用配置文件來生成代碼。

相關文章