巧用模板加速引腳電平讀寫
<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
sbi
和 cbi
都是雙週期指令,單片機頻率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
的實現分爲三個部分:
-
把
pin
映射到timer
、bit
和port
,分別表示pin
在哪個定時器上、對應的bit mask和PORTx
寄存器的編號,如果在定時器上還要關閉定時器的PWM; -
把編號
port
映射到PORTx
的指針out
; -
關閉全局中斷,通過
out
指針對寄存器PORTx
進行位操作,最後恢復中斷狀態。
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
必須對每一個引腳進行特化。解決方案有:
-
僅對有需求的引腳特化模板,其餘沿用非模板
digitalWrite
,用20%的時間優化80%的代碼,把工作量花在刀刃上; -
見思考題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倍以上的速度提升。
思考題
-
把更多函數改寫成模板形式,如
pinMode
、digitalRead
、analogWrite
、shiftIn
等。 -
* 把模板
shiftOut
的參數bitOrder
改爲模板參數。 -
模板
digitalWrite
的編寫過程非常機械,嘗試寫一個程序,用配置文件來生成代碼。