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 的说明
前面的字符串串接动作, 每个 + 串接则是调用更多次的函数:
- 先依据 + 右边 type 调用不同类似如下的 String:: operator+( )
StringSumHelper & operator + (const StringSumHelper &lhs, float num)
{
StringSumHelper &a =const_cast<StringSumHelper&>(lhs);
if (!a.concat(num)) a.invalidate();
return a;
}
- 里面你看到了, 它调用 a.concat(num)
- 再来看看 String::concat( )
unsigned char String::concat(float num)
{
char buf[20];
char* string = dtostrf(num, 4, 2, buf);
return concat(string, strlen(string));
}
-
看到了, 它先调用 dtostrf( ) 处理实数, 把 float 转为小数后两位的字符串, 处理 float 类似"%.2f" 格式化很耗费时间! 不过也没办法啰 😃
-
还有, 又调用一次 concat(string, strlen(string));
-
请注意, 在这句里面调用一次 strlen(string) 计算字符串长度 ! 看过 strlen( ) 源代码你就会发现它有多笨,
它会逐字一直看, 边看边 +1, 直到看到一个 NULL 也就是一个 byte 内容是 0 为止! -
再来看看那 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;
}
- 看到没, 它去调用 reserve(newlen)
在该 reserve 内作法我大概说一下(程序代码就略去)- 看看 buffer 保留的空间是否 >= newlen,
是则立即返回 - 如果不够, 去调用 malloc( ) 要一块新的够大的 RAM buffer
这时如果要不到不会报错, 直接返回!(当然这样有问题, 可是又能怎样呢?)
调用 strcpy( )把原先 buffer 的都逐 char 复制过去
把原先 buffer 位置暂时记在 tmps
把 buffer 改指向新的 RAM buffer
把 tmps 指针指过去, 就是原先字符串占用的 RAM 还给系统
- 看看 buffer 保留的空间是否 >= newlen,
- 最后再次调用 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