多个Serial.print拼成一条Serial.print省时间吗?

https://www.arduino.cn/thread-15018-1-1.html

很多人以为把许多 Serial.print 合并成一个 Serial.print 可以节省时间 !?

其实错了 ! 除了比较好用且程序代码通常比较短之外, 不但耗用比较多时间也耗用比较多内存 ?

以前我也以为有省时间, 但经过仔细查看各相关程序代码之后, 发现没赚到反而亏很大 !

大家都知道少调用函数是可以省一点点 clock,
你要把多项信息串在一起用一个 Serial.print( ), 结果要串在一起所花的时间却可能大于省下的函数调用时间 !! (其实不是可能, 而是确实浪费更多倍数的时间 !!)
我这说法是有依据的,

(第一)

因为要串在一起最简单做法如下:
假设 六个 float data 在 x, x2, x3, x4, x5, x6, 以及一个 long nn (假设这是时间), 则:
原先你可能写:

  Serial.print(x); Serial.print(" ");
  Serial.print(x2); Serial.print(" ");
   ...
    Serial.print(x6); Serial.print(": "); // 故意用 : 表示后面与前面不同性质
   Serial.println(nn);   // 用 .println( ) 会换行

看起来很不爽对不对, 然后又不能使用 printf( ),
是可以使用 sprintf( ), 但不支持 %f
(P.S.奈何大神有写一篇偷改档案指针让你可以用 printf( ); 但当然与 sprintf( ) 内部一样不支持 %f )
于是, 要串接使用一次 Serial.print 最简单的方法如下:

Serial.println(String("")+ x +" "+x2 +" "+x3 +" "+x4 +  " "+x5  +" "+x6+", " + nn);

这样, 我中间各项故意用" “空白隔开, 最后一项用”, "隔开, 已经最省啰 ?
很好用, 但其实这样比较费时间, 也比较浪费程序代码内存空间 !
主要是因为这些用 + 串接的动作会跳入 String class, 在 WString.cpp 里面
(程序 都在 Arduino IDE 的 hardware\arduino\cores\arduino 内,
以下 HardwareSerial.cpp, Print.cpp 也是);

我有实际写 Arduino 程序在 16MHz 的 UNO R3 测试过,
所花时间远比用 13个 Serial.print( )外加一个 Serial.println( )
还要多 !!

(第二) 进出函数大约用 2 us (以 16MHz UNO 为例)

调用函数 4 clock, 返回也是 4 clock, 进入之后关于对寄存器进行堆栈(Stack; 堆栈)操作部分,
包括开头 push 推入寄存器, 返回前 pop 抓出寄存器,
这样总共大约 30 clock,
这就是以前我说的调用函数成本大约 2 micro second; (以 16MHz UNO, 32 clock = 2 us )
所以, 每多一次 Serial.print 浪费 2 us (micro second)
但是, 每省一个 Serial.print 就必须用一个 + 串接, 这大约浪费额外的 十次函数呼叫与 10 个函数运行时间 ! 至少浪费 20 micro second, 十几倍ㄟ !

(第三)关于每个 + 串接浪费至少 20 us 的说明

前面的字符串串接动作, 每个 + 串接则是调用更多次的函数:

  1. 先依据 + 右边 type 调用不同类似如下的 String:: operator+( )
   StringSumHelper & operator + (const StringSumHelper &lhs,  float num)
    {
      StringSumHelper &a =const_cast(lhs);
      if (!a.concat(num)) a.invalidate();
      return a;
    }
  1. 里面你看到了, 它调用 a.concat(num)
  2. 再来看看 String::concat( )
    unsigned char String::concat(float num)
    {
      char buf[20];
      char* string = dtostrf(num,  4,  2,  buf);
      return concat(string,  strlen(string));
    }
  1. 看到了, 它先调用 dtostrf( ) 处理实数, 把 float 转为小数后两位的字符串, 处理 float 类似"%.2f" 格式化很耗费时间! 不过也没办法啰 ?

  2. 还有, 又调用一次 concat(string, strlen(string));

  3. 请注意, 在这句里面调用一次 strlen(string) 计算字符串长度 ! 看过 strlen( ) 源代码你就会发现它有多笨,
    它会逐字一直看, 边看边 +1, 直到看到一个 NULL 也就是一个 byte 内容是 0 为止!

  4. 再来看看那 String::concat(String, len)

    unsigned char String::concat(const char *cstr,  unsigned intlength)
    {
      unsigned int newlen = len + length;
      if (!cstr) return 0;
      if (length == 0) return 1;
      if (!reserve(newlen)) return 0;
      strcpy(buffer + len,  cstr);
      len = newlen;
      return 1;
    }
  1. 看到没, 它去调用 reserve(newlen)
    在该 reserve 内作法我大概说一下(程序代码就略去)
    • 看看 buffer 保留的空间是否 >= newlen,
      是则立即返回
    • 如果不够, 去调用 malloc( ) 要一块新的够大的 RAM buffer
      这时如果要不到不会报错, 直接返回!(当然这样有问题, 可是又能怎样呢?)
      调用 strcpy( )把原先 buffer 的都逐 char 复制过去
      把原先 buffer 位置暂时记在 tmps
      把 buffer 改指向新的 RAM buffer
      把 tmps 指针指过去, 就是原先字符串占用的 RAM 还给系统
  2. 最后再次调用 strcpy 把 cstr 复制到原先字符串的尾巴: buffer + len 处 !
    看到没 !?
    你省了一次 Serial.print 的调用,
    然后却换来多耗费了大约十次的调用, 省了 2 us, 却浪费了至少 20 us !!!

(第四)

每次的 Serial.print 并不是真正送出 char, 所以没有时钟时序 clock 的问题, 那不是 Serial.print 管的 !

(4A)先来看看 Serial.print 如果参数是 C++ 的 String:

  (参看 WString.cpp,      在 IDE 的 hardware\arduino\cores\arduino 内)
    size_t Print::print(const String &s)
    {
       return write(s.c_str(),  s.length()); //这就是 (4C) 说的函数
    }

看到没, 它调用 write( C++ String 的内部, 该字符串长度);

(4B)如果 Serial.print( C_Style 的字符串 ):

    size_t Print::print(const char str[])
    {
      return write(str);  // 这会先调用 strlen,  再调用 (4C) 说的函数
    }

看到没, 又多一次函数调用, 调用 write( C_Style 的字符串 );

(4C) write( charArray[ ], size) 函数

    size_t Print::write(const uint8_t *buffer,  size_t size)
    {
      size_t n = 0;
      while (size--) {
        n += write(*buffer++);   // 这就是 (4D)说的函数
      }
      return n;
    }

做啥呢 ?
原来是用一个 while Loop,
把字符串从头到尾一个 char 一个 char 传给函数 write( 一个 char );
换句话说, 已经接近尾声, 每个 char 都必须调用一次函数 write( char );

(4D)看看这 write(char) 函数

因为 Serial 所用的 HardwareSerial 改写(oerride)了这函数,
所以这次不可以看 Print::write(char), 要改看 HardwareSerial::write( )
P.S. 其它前面说的都是看 Print:: 的, 因为 HardwareSerial 建立在 Print 上!

    size_t HardwareSerial::write(uint8_t c)
    {
      int i = (_tx_buffer->head + 1) % SERIAL_BUFFER_SIZE;
      // If the output buffer is full,  
      // we can ONLY WAIT ...
      while (i == _tx_buffer->tail)
        ;;;  // 如果 tail 都没变化表示 ISR( ) 没送出一个 char 以便空出一个位置
      ///
      _tx_buffer->buffer[_tx_buffer->head] = c;
      _tx_buffer->head = i; 
      ///
      sbi(*_ucsrb,  _udrie);
      // clear the TXC bit -- "can be cleared by writing aone to its bit location"
      transmitting = true;
      sbi(*_ucsra,  TXC0);
      return 1;
    }

看到没, 要印出的每个 char 或 byte 都要到这函数来 !!
它的工作就是把 c 塞入串口的输出缓存区 !
如果缓存区已满了, 就只能这样:

      while (i == _tx_buffer->tail)
        ;  // 如果 tail 都没变化表示 ISR( ) 没送出一个 char

这个等待会 Loop 到缓存区的 tail 有改变为止!
tail 是因为另一个中断程序 ISR( )把一个 char 送出然后修改了 tail;
**所以, 在自定义的 ISR( ) 内调用 Serial.print 很危险, **
因为如果因我们的 Serial.print( ) 导致 buffer 缓存区满了,
那程序就死了!
原因是在 ISR( ) 内中断自动禁止(除非你有故意打开),
然而缓存区满了之后, 中断如果被禁止,
就没办法把缓存区的送出任何一个 char,
于是我们写在 ISR( ) 内的 Serial.print( ) 会因为该 while( );;; 回不来 !!
既然 Serial.print 回不来, 那我们写的 ISR( ) 就不会结束,
ISR() 不结束, 则中断维持在禁止状态, 不会自己打开;
没有中断, 就没有 ISR( ) 能把缓存区的 char 送出 !!!
(4E)在刚刚的 HardwareSerial::write( char )
你会发现唯一与硬件似乎比较有关的只有:
sbi(_ucsrb, _udrie); transmitting = true; sbi(_ucsra, TXC0);
这三句简单说就是启动(也可能原先已经启动)相关 TX 的中断 ISR( ),
通知真正负责送出的 ISR( ) 一定有char要送出!
(在该 ISR( )内, 如果发现缓存区已经空了,
会把负责送char的 ISR( )自己关闭, 所以这边要打开该中断)
(4F)在 C++ 允许很多个函数用相同名称, 只要可以从参数(Parameter)区分即可,
这称之为函数名称重载(Function name overloading), 或简称重载(Overloading).
意思是函数名称重复使用来定义不同但相关且类似的函数.
结论:
(1)除非你原先就是一个字符串, 否则为了用一个 Serial.print( )取代原先数个 Serial.print,
使用 + 做串接将耗掉十几倍于原先多个 Serial.print 所浪费函数呼叫的时间!
(2)C++的String字符串很好用, 但用了它会多耗掉大约 1.5KB
(3)如果使用到输出 float, 则因会调用 dtostrf( ); 又会另外多大约 1.6KB

你可能感兴趣的:(arduino,知识)