为何定时做事的ISR或中断程序内不可用Serial.print(定时器相关)
https://www.arduino.cn/thread-12546-1-1.html
问题:中断或定时器中看不到串口打印的内容
常常有人用到定时器的中断(或外部中断),然后想要使用 Serial.print 或 Serial.println 送数据到串口监视器查看,可是却常常有人发现加了一些 Serial.println 之后可能很快就不动了 !?
官方网站也建议在 ISR( ) 内或是在用 attachInterrupt 连接的中断处理程序内, 最好不要使用 Serial.print 或 Serial.println 这类函数 (function) !
那到底在 中断处理程序内 可不可以用 Serial.print 和 Serial.println 呢 ? 其实答案是勉强可以用 Serial.print( ) 与 Serial.println( ) 的 ! 为何说勉强可以, 不是说不可以, 也不说可以 ?
因为, 只要你的中断请求的间隔时间不是太短(即两次中断的时间间隔), 那使用 Serial.print 其实并不会有问题 !
怎样才叫做太短呢? 就是短到 Serial.print 来不及或快要来不及也可能会有问题!如果你的中断一秒才来一次或更久才来一次, 那用 Serial.println应该没问题
如果你的中断请求是 1ms 来一次,那肯定来不及 !!原因很简单, 来不及打印 Serial.println( ) 要求送出的字符串。
奇怪!?
不是说 Arduino CPU 在 16MHz 的频率 (Clock)之下, 最快 1us 可以做 16 个指令,那 1ms 就可以做 16000 个指令了,怎会小小 Serial.print 一下就来不及呢?原因当然就是 Serial.print 很慢很慢 !
其实, 所谓 1us (micro second)可以做 16 个指令是指最简单的机器指令,
对于 C/C++ 写的每句指令因为相当于数个到数十个机器指令,
所以 C/C++ 每句指令大多数要 0.5 us 到 1 us甚至更多的时间(例如调用 millis( ) 或调用 micros( ) 就大约要 2 us)。
聪明的你已经算出那这样 1ms 还是可以做大约数百甚至多到两千个 C/C++ 指令 !
没错, 可是, 在 Baud rate (波特率) 9600 的情况下, 打印一个 char 就要大约 1ms = 1000us 的时间。
这是可以算的, 9600bps 意思就是每秒 9600 bit, 一个 char 要用 10 bit(包括内容 8 bit,开始(Start) 1 bit,结束(STOP) 1 bit), 所以,9600bps 意思就是每秒大约 9600/10 = 960 char (Byte);
这意思就是 1 char 大约要 1ms = 1000 us 的时间 (1秒/960 = 1.04ms) !
Arduino 程序来测试串口输出时间
// measure Serial.print cost
unsigned long begt, endt; // for record micros( )
int baud = 9600;
char ggyy[ ] = "-2-4567890ABCD56"; // 字符串用来测 60..66 char
void setup( ) {
Serial.begin(baud);
Serial.println(String("Test Baud Rate = ") + baud);
Serial.println("----------\nNow test print 50 chars immediate return");
test1( );
Serial.println("\n\n==========\nNow test with .flush( )");
test2( );
test3( );
} // setup(
void loop( ) {
;
}
void test1( ) {
Serial.flush( );
delay(258);
begt = micros( );
//Serial.print(ggyy); // 用来测 60 .. 66 char
Serial.print("123456789x123456789X");
Serial.print("..34567890-x-4567890F234F678\r\n");
endt = micros( );
Serial.println(String("Run time = ") + (endt - begt) + "micro seconds = " +
(endt - begt) / 1000.0 + "milli seconds");
}
void test2( ) {
Serial.flush( );
delay(258);
begt = micros( );
//Serial.print(ggyy); // 用来测 60 .. 66 char
Serial.print("123456789x123456789X");
Serial.print("..34567890-x-4567890F234F678\r\n");
Serial.flush( ); // 确定都已经打印出去
endt = micros( );
Serial.println(String("Run time = ") + (endt - begt) + "micro seconds = " +
(endt - begt) / 1000.0 + "milli seconds");
}
void test3( ) {
Serial.println(String("\n\nPrint 100 char with flush at Baud rate ") + baud);
Serial.flush( );
delay(258);
begt = micros( );
//Serial.print(ggyy); // 用来测 60 .. 66 char
Serial.print("123456789x123456789X");
Serial.print("..34567890-x-4567890F234F678\r\n");
///
Serial.print("123456789x123456789X");
Serial.print("..34567890-x-4567890F234F678\r\n");
Serial.flush( ); // 确定都已经打印出去
endt = micros( );
Serial.println(String("Run time = ") + (endt - begt) + "micro seconds = " +
(endt - begt) / 1000.0 + "milli seconds");
}
大家可以自己测试看看, 以下我把我测的各种结果跟大家分享:
- 测试在 9600 bps 打印 50, 60, 61, … 68 char, 100 char, 200char, 300char, 500char, 600char, 1000char;
以下两栏结果分别是在 Serial.print 之后立即抓时间, 以及 test2( ) 是用 .flush( ) 之后再抓时间:
打印字符数 | test1( ) | test2( ) |
---|---|---|
50 char | 0.51 ms | 52.03ms (相差 51.52) |
60 char | 0.61 ms | 62.43ms (相差 61.82) |
61 char | 0.62 ms | 63.47ms (相差 62.85) |
62 char | 0.62 ms | 64.51ms (相差 63.89) |
63 char | 0.64 ms | 65.55ms (相差 64.91) |
64 char | 0.64 ms | 66.59ms (相差 65.95) |
65 char | 0.66 ms | 67.63ms (相差 66.97) |
66 char | 1.08 ms | 68.67ms (相差 67.59) |
67 char | 2.12 ms | 69.72ms (相差 67.6) |
68 char | 3.16 ms | 70.76ms (相差 67.6) |
100 char | 36.44 ms | 104.03ms (相差 67.59) |
200 char | 140.44 ms | 208.03ms (相差 67.59) |
300 char | 244.44 ms | 312.03ms (相差 67.59) |
500 char | 452.44 ms | 520.03ms (相差 67.59) |
600 char | 556.44 ms | 624.03ms (相差 67.59) |
1000 char | 972.44 ms | 1040.03ms (相差 67.59) |
请注意, 0.64ms 可能是 0.636ms 或 0.640ms 或 0.644ms, 因印出时自动四舍五入!
- 改用 Baud Rate 19200 bps, 发现所需时间就是大约一半, 很合理 !
- 改用 Baud Rate 57600 bps, 发现所用时间就是 9600bps 的大约七分之一,
- 改用 Baud Rate 115200 bps, 时间又是 57600bps 的大约一半 !
- 回头解释刚刚(1)的结果, 以下先讨论 test1( )的方式,
你会发现, 如果不使用 Serial.flush( ); 把打印数据冲刷出去.
则印 50char 要 0.51ms (且这答案在各种 Baud Rate 几乎都一样 !!),
印 60 char 要 0.61ms, 印 64 char 要 0.64ms, 印 65 char 要 0.66ms (因为只印到小数点后两位, 是把小数点后第三位做四舍五入)
意思是大约每个 char 要大约 0.1ms (不是 1msㄟ, 怎会这么快 !?),
别高兴, 事实上这时间不是真正打印出去的时间, 是复制字符串到 Serial 的输出缓存区(Buffer)的时间,
所以, 你会发现, 即使提高 Baud Rate 到 57600, 打印 50 char 也是要 0.51 ms
那为何 65 char 是 0.66ms, 可是 66 char 却是 1.08ms, 67 char 要 2.12ms, 68 char 要 3.16ms ???
这是因为输出缓存区(Output buffer)只有 64 bytes (chars),
当缓存区满的时后, Serial.print 必须等有空位才可以把剩下的字符串复制进去缓存区,
必须都已经把参数的字符串复制进去缓存区了, 然后 Serial.print 才能返回!
因此, 你可以发现, 打印 50 char 要 0.51ms, 但是打印 500char, 却要高达 452.44ms, 不是 5.1ms !!!
如果你把系统 Serial 的输出缓存区改为 500 bytes 则打印 500char 就会是大约 5.1ms, 就是复制到缓存区的时间 !! - 再来讨论(1)的结果, 但重点放在用 test2( ), 也就是 Serial.flush( ); 之后再做 endt = micros( ); 取时间:
如果没使用 Serial.flush( ); 则表示可能还会有最多 63 bytes 在输出缓存区还没真的被送出去!!
现在如test2( ) 内的做法, 等 Serial.flush( ); 确定都送出之后再取得 micros( ) 时间, 结果 …
你会发现 50char 要 52.03ms, 60char 要 62.43ms, 64 char 要 66.59ms, 印 65 char 要 67.64ms, 印 66 char 要 68.67ms, 印 67 char 要 69.72ms,
印 100 char 要 104.03ms, 印 200 char 要 208.04ms, 印 300 char 要 312.06ms, 印 500 char 要 520.08ms,
看到没, 几乎就是 1 char 要 1 ms 啦 !!
那为何是比 1ms 略大呢?
首先 Serial.print 本身要花时间且传送要做一点准备工作也要一点时间,
还有, 1秒 / 960 char 这样 1 char 是 1.04ms, 所以印 100 char 要大约 104.03 ms 完全合理 !!!
另外, 打印 66 char 以上, 不论是 67 char, 100 char, 200char, 300 char, 500 char,甚至1000char,
你会发现, 没有 Serial.flush( )比有Serial.flush( )少了大约 67.5 ms, 且 66 char 以上后几乎都是这样!
这是因为缓存区只有 64 bytes, 所以在打印字数小于 65 char 时,
因第一 char 立即送出, 剩下的字符都可复制到缓存区然后就可从 print 或 println 立即返回!
但是打印太多字在缓存区满了之后, Serial.print 必须等到有一个 char 被抓走空出一byte的位置才可再把一个 char 填进去缓存区,
这大约要 1ms 的时间, 必须把要 print( ) 的所有字符都填入缓冲区之后 Serial.print 才会结束;
而此时缓存区内就是还有 64 char 还没送出, 这是有 .flush( ) 和没有 .flush( ) 差异时间的最主要来源 !!
所以, 打印 1000 char, 没有 .flush( )时间是 972.44ms, 有 .flush( ) 确定打印完毕则是 1040.03ms, 相差大约67.5ms !
,,,。。。,,,。。。,,,。。。! - 补充一下, 虽然 int 只有两个 byte, 但是:
int k = 12345;
Serial.println(k); 这句会送出 7 个 char:
包括 “12345” 以及 “\r\n” 这两个字符;
但请注意: Serial.print( (char)123); 这样只有送出 1 个 char !!
还有:
float ggyy = 1.25612345;
Serial.println(ggyy); 这句会送出 6 个 char:
包括 “1.26” 以及 “\r\n” 这两个字符;
注意 float 和 double 的数值打印时, 小数点之后预设(默认)只印两位(但会从没印出的第一位四舍五入到有印的最后一位):
http://arduino.cc/en/serial/print
关于 print/println 的源代码请参考:
(它们也在你Arduino IDE 目录的 hardware\arduino\cores\arduino\ 里面!)
(IDE 1.0.6)
https://github.com/arduino/Arduino/blob/ide-1.0.x/hardware/arduino/cores/arduino/Print.cpp#L59
https://github.com/arduino/Arduino/blob/ide-1.0.x/hardware/arduino/cores/arduino/HardwareSerial.cpp#L61
(IDE 1.5.x / IDE 1.6.x)
https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/Print.cpp#L228
https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/HardwareSerial.cpp#L225
修改串口缓存区大小
如果你想把缓存区改大一些(包括输出缓存区与输入的缓存区),
或只是想看看缓存区大小的定义 SERIAL_BUFFER_SIZE 真的是 64,
IDE 1.0.6 请看 HardwareSerial.cpp
https://github.com/arduino/Arduino/blob/ide-1.0.x/hardware/arduino/cores/arduino/HardwareSerial.cpp#L61
IDE 1.5.x 或 1.6.x 请看 HardwareSerial.h https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/HardwareSerial.h#L42
(它们也在你Arduino IDE 目录的 hardware\arduino\cores\arduino\ 里面!)