内容简介:拉阅读量第二弹,希望你能有所收获。我不想听你放那么多屁,我只想知道怎么加速这个程序测试调用10次某语句需要的时间。在山寨版Uno Rev3上运行,程序输出:
拉阅读量第二弹,希望你能有所收获。
我不想听你放那么多屁,我只想知道怎么加速 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架构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
进行位操作,最后恢复中断状态。
每一步映射都是常数时间的,但是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
必须对每一个引脚进行特化。解决方案有:
-
仅对有需求的引脚特化模板,其余沿用非模板
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
的编写过程非常机械,尝试写一个程序,用配置文件来生成代码。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 想用数据库“读写分离” 请先明白“读写分离”解决什么问题
- Java 读写锁浅析
- Golang文件读写
- ReentrantReadWriteLock 读写锁解析
- 用Python读写文件
- MySQL -- 读写分离
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。