zoukankan      html  css  js  c++  java
  • 巧用模板加速引脚电平读写

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

    我不想听你放那么多屁,我只想知道怎么加速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
    36
    44
    888
    

    第一组空函数是对照组,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);
    }
    

    输出:

    0
    36
    44
    888
    8
    12
    52
    

    引脚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);
    }
    

    程序输出:

    0
    36
    8
    8
    

    逻辑分析仪测得方波频率为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的编写过程非常机械,尝试写一个程序,用配置文件来生成代码。

  • 相关阅读:
    2011全国大学生电子竞赛我们组的方案——A题开关电源模块并联供电系统(草稿)
    perl 引用实例
    R 批量读取本地文件
    R语言对数据集进行排序
    perl 常用函数和符号
    Linux下设置环境变量
    读取前200行
    R字符串处理
    R graph:如何自定义坐标轴刻度标示(tick label)
    perl中如何调用R语言
  • 原文地址:https://www.cnblogs.com/jerry-fuyi/p/12773955.html
Copyright © 2011-2022 走看看