rollover:millis() 溢出(overflow)归零(rollover)有沒问题? 2024-04-05 00:16:44 0 0 文章目录那当 millis() 溢出(Overflow)归零时到底有没有问题?先来看看 millis( ) 何时会溢出?为何说原则上millis()溢出归零是没问题,1. 正确--Arduino 的 delay( ) 代码:2. 错误--Arduino 的 delay( ) 代码:3. 原因分析3.1 用c语言测试3.1 用arduino测试更详细的解释原因:(a)先考虑第一种写法: (正确写法)delay()新旧版本比较旧版本2. 新版本新版本的 delay( ) 也可以在ISR( )中断程序内使用! https://www.arduino.cn/thread-12506-1-1.html 在 Arduino, 我们常常使用 millis( )做检查时间或计时(定时)的工作: http://www.arduino.cn/thread-12408-1-1.html http://www.arduino.cn/thread-12468-1-1.html http://arduino.cc/en/Reference/Millis 那当 millis() 溢出(Overflow)归零时到底有没有问题? 答案是原则上millis()溢出归零本身没有问题, 但是如果程序码没写好则有问题 ! ( micros( ) 也是使用 unsigned long, 所以类似的问题也会发生在 micros( ) 的使用) (差别只是 millis( )要 49.71 天才归零, 但 micros( ) 则大约 71.58分钟归零) 先来看看 millis( ) 何时会溢出? 因为 millis( )是用 unsigned long (32 bits)表示开机到现在为止多少milli second, 用 32 bit 表示一个无符号整数, 32bit都是 1就是最大数, 等于 2 的 32 次方 -1 = 4294967295 从 0 数到 4294967295 共要 4294967296 milli seconds, 等于 4294967296 /1000 /60 /60 小时; 每天有24小时, 所以等于 4294967296 /1000 /60 /60 / 24 天 = 49.710 天 也就是说, 开机后大约快要 50 天之时 millis( )会归零 ! 那到底有没问题 ?! 为何说原则上millis()溢出归零是没问题, 但是如果程序码没写好则有问题 ! 因为问题并不是出在 millis( ) 归零本身 ! 1. 正确–Arduino 的 delay( ) 代码:void delay( unsigned long dtms ){ unsigned long start = millis(); while (millis() - start < dtms);;} // delay( 以上这程序完全正确, 且即使 millis( ) 溢出归零也不会有问题 !! 2. 错误–Arduino 的 delay( ) 代码:void delay( unsigned long dtms ){ unsigned long start = millis(); while (millis() < start + dtms);; // 有问题 !} // delay( 3. 原因分析 从数学观点看起来似乎相同, 因为 millis() - start < dtms,应该就相当于 millis() < start + dtms 这只在没有 Overflow 之时成立, 因为把左边的 减项移到右边变加项, 应该对啊 !? 如果 millis( ) 前进到 Overflow 之时就不相同 !!其实也不是 millis( ) 本身 Overflow 或 RollOver 归零(变成 0)之时出问题! 严格说是当 millis( ) 前进到 unsigned long 最大数的一半时大约24天半之后就会开始出问题!! 因为它的问题是出在 start + dtms 这运算 ! 并不是出在 millis( ) 本身是否 Overflow !!! 3.1 用c语言测试 先别管为什么 ! 直接测试最准啦, 可是如何测试呢? 总不能等到 millis( ) 快要 Overflow 吧, 那要将近 50天ㄟ! (或是等一半 24.8天也受不了!) 其实也不难, 写个一般的 C 语言程序在 PC 上测试即可, 我们用 milli 模拟 millis( ), 每次做 ++milli; 模 要 delay 7 milli seconds, 分别照着上述两种写法依序测试, 为了证明上述第二种方法有问题, 我们连续测二次, 第二次 Loop 前先做 ++milli; 模拟已经过了 1 milli second, 测试的结过放在以下程序的后面批注(注释), 请看以下程序码(不是 Arduino 程序喔! 是一般的 C 语言程序!)// yc.c --- to test millis( ) Rollover (Overflow); by tsaiwn@cs.nctu.edu.tw#include <stdio.h>int main( ) { unsigned long k5 = -5; // 4294967291, 再+5就归零 unsigned long every = 7; unsigned long milli = k5; // 4294967291 printf("every=%lu\n", every); printf("milli=k=%lu\n", milli);/// unsigned long start = milli; printf("First method.. milli = %lu, ", milli); printf("start=%lu\n", start); printf(" milli - start=%lu\n", milli - start); while(milli - start < every){ printf("milli=%lu, ", milli); printf(" milli - start=%lu\n", milli - start); ++milli; } printf("===1st method, NOW milli = %lu ==========\n", milli); milli = k5; printf("\nSecond method.. milli = %lu, ", milli); printf("start=%lu\n", start); printf(" start + every=%lu\n", start + every); while( milli < start + every){ printf("milli=%lu\n", milli); ++milli; } ++milli; printf(" after ++milli, check milli=%lu\n", milli); while( milli < start + every){ printf("milli=%lu\n", milli); ++milli; } printf("===2nd method, NOW milli = %lu ======\n", milli); printf("===start + every = %lu\n", start + every);} /***** **************** Result: ====================== D:\Users\tsaiwn\test>gcc yc.c D:\Users\tsaiwn\test>a every=7 milli=k=4294967291 First method… milli = 4294967291, start=4294967291 milli - start=0 milli=4294967291, milli - start=0 milli=4294967292, milli - start=1 milli=4294967293, milli - start=2 milli=4294967294, milli - start=3 milli=4294967295, milli - start=4 milli=0, milli - start=5 milli=1, milli - start=6 ===1st method, NOW milli = 2 ========== Second method… milli = 4294967291, start=4294967291 start + every=2 after ++milli, check milli=4294967292 ===2nd method, NOW milli = 4294967292 ====== ===start + every = 2 D:\Users\tsaiwn\test> **************************************/ 3.1 用arduino测试 啥? 模拟的答案你不太相信 ? 好, 那用 Arduino 实际测试 ! 要测 50 天吗? 当然不是, 哪有那种美国时间连续测 50天 ! 其实, millis( ) 的答案是可以偷改的, 所以只要测试几十秒就可以啦 ! 只要你宣告 extern unsigned long timer0_millis; 然后在程序码中把 timer0_millis 改掉, 瞬间 millis( ) 就变成你要的值了 ! 以下就这样来测试前面说的两种 delay( ) 的写法, test1( )使用正确写法的 delayAA( ); test2()则使用错误写法的 delayBB( ); 不相信的朋友就测试一下吧:extern unsigned long timer0_millis; // millis( )用的void setup( ) { Serial.begin(9600); Serial.print(" -1 === "); Serial.println( (unsigned long) - 1); cli( ); // 禁止中断 timer0_millis = -2988; // 让 millis( ) 在 2.988 秒后 Overflow sei( ); // 允许中断 test1( ); cli( ); // 禁止中断 timer0_millis = -2988; // 让 millis( ) 在 3.388 秒后 Overflow sei( ); // 允许中断 test2( ); Serial.println("=== 2nd Run for test2"); cli( ); // 禁止中断 timer0_millis = -3388; // 让 millis( ) 在 3.388 秒后 Overflow sei( ); // 允许中断 test2( );}void loop( ) { //...}void test1( ) { Serial.print("test1: time ="); Serial.println( millis( ) ); unsigned long start = millis( ); delayAA(2000); unsigned long tm2 = millis( ); delayAA(3000); unsigned long tm3 = millis( ); delay(2500); // library unsigned long endt = millis( ); Serial.print("AA: start ="); Serial.print( start ); Serial.print("=="); Serial.println((long)start); // 有正负 sign Serial.print("after 2000, now="); Serial.println(tm2); Serial.print("after total 5000 delay, now="); Serial.println(tm3); Serial.print("After delay(2500) endt="); Serial.print(endt); Serial.print(", run time = "); Serial.print(endt - start); Serial.println(", should be 7500");}void test2( ) { Serial.print("test2: time ="); Serial.println( millis( ) ); unsigned long start = millis( ); delayBB(2000); unsigned long tm2 = millis( ); delayBB(3000); unsigned long tm3 = millis( ); delay(2500); // library unsigned long endt = millis( ); Serial.print("BB: start ="); Serial.print( start ); Serial.print("=="); Serial.println((long)start); // 有正负 sign Serial.print("after 2000, now="); Serial.println(tm2); Serial.print("after total 5000 delay, now="); Serial.println(tm3); Serial.print("After delay(2500) endt="); Serial.print(endt); Serial.print(", run time = "); Serial.print(endt - start); Serial.println(", should be 7500");}void delayAA( unsigned long dtms ) { unsigned long start = millis(); while (millis() - start < dtms);;}void delayBB(unsigned long dtms ) { unsigned long start = millis(); while (millis() < start + dtms);;} Q: 到底第二种条件式写法在何时会出问题 ? A: 在millis( )看做signed有正负号的数是负数, 且 start + dtms 是正之时, 也就是当 start = millis( ) 已经超过 unsigned long 最大数的一半, 且该 start = millis( ) 拿来与 dtms 相加竟然真的是正数之时就会出问题 !! 请注意这里的 dtms 是指范例中你想要 delay 的几个 ms (毫秒) ! 例如假设 millis( ) 看作有正负是 -800 但你要 delay 805 ms, 则 -800 < -800 + 805 也就是 -800 < 5 是不成立的 ! 因为unsigned long 的 -800 其实是一个很大的正数, 怎会小于 5 呢?! (不过, 如果这时你是要 delay 799 秒或更少则仍不会有问题 ! Why ? 留给你自己想 ?) 更详细的解释原因: 虽然我们宣告 millis( ) 与范例中的 start 以及function的参数为 unsigned long, 但其实在计算机来说,它们还是一个有正负的数(严格说过一半之后就是负数了), 例如, 4294967295 其实就是 -1, (都是二进制 32 个 1) 那 4294967294 其实就是 -2, 4294967290 就是 -6; 假设这时millis( )就是 4294967290 也就是 -6; 写 unsigned long start = millis( ); 得到 4294967290 假设要 delay 的 dtms 是 8 表示要 delay 8 ms, (a)先考虑第一种写法: (正确写法)unsigned long start = millis( ); while( millis( ) - start < dtms );; 注意此时的 millis( ) 显然与 start 相等, 相减得到 0, 于是 0 < dtms 条件成立, 继续做 while LOOP; 过了 1ms, millis( )变为 4294967290 也就是 -5, 于是条件式 millis( ) - start < dtms 就是: -5 - (-6) < 8 也就是 1 < 8, 显然还是成立, 继续等 ! 有些人会说不是 unsigned long 吗?! OK, -5 是 4294967291, 而 -6 是 4294967290, 于是 4294967291 - (4294967290) < 8 有没有成立呢? 这时还是相当于 1 < 8 当然成立啊 ! 继续在过 1 ms, 变成: -4 - (-6) < 8 也就是 2 < 8 看作无号数就是 4294967292 - (4294967290) < 8 仍然是相当于 2 < 8 所以必须继续等, … 如果再过了 4ms 变为(此时 millis( ) 归零!) 0 - (-6) < 8 相当于 6 < 8 还是不成立 … 如果再过了 2ms 变为(此时 millis( ) 是 2) 2 - (-6) < 8 相当于 8 < 8 不成立 ! 因为条件不成立, 于是离开 while LOOP, 刚好 delay 了 8 ms (注意刚刚假设 dtms 是 8), 完全正确 ! (b)接着来考虑第二种写法: (错误写法) unsigned long start = millis( ); while( millis( ) < start + dtms );; 同样假设开始时 millis( ) 与 start 都是 4294967290 也就是 -6; 第一次进入 while 条件式为: -6 < -6 +8 也就是 -6 < 2 学数学的会认为 -6 < 2 当然成立, 应该继续做 while LOOP等待, 问题是因为我们已经宣告 unsigned long, 所以 -6 不是 -6, 是 4294967290, 那 4294967290 怎会小于 2 呢 ?! 于是一开始 while( ) 条件就不成立, 立即结束 while LOOP, 等于都没有 delay 就结束了 !!! 所以, 第二种写法问题出在 start + dtms 这个地方, 由于 dtms 是正的数, 而 start = millis( ) 虽然是 unsigned long, 但当 millis( ) 达到unsigned long 接近最大数之时其实就是负很少的负数, 这时 millis( ) 或说 start 看作signed如为负数且其绝对值如果小于 dtms 则会使 start + dtms 变成正数就会出问题 ! 这使得 millis( ) < start + dtms 就会立即不成立, 于是 while LOOP 会立即结束 ! ! 结论: 第二种写法 while( millis( ) < start + dtms );; 的出问题其实是在 millis( ) 超过 unsigned long 最大数一半之后, 且看作 signed 有正负的 millis( ) + dtms 结果是正的数, 这时就会出问题 !!! 为了方便大家容易理解 millis( ) 的使用, 我把写在另一篇的相关信息 copy 过来补充如下: Arduino 的 millis( ) 源代码(Source code):unsigned long millis( ) { unsigned long m; uint8_t oldSREG = SREG; //状态寄存器(包括是否允许 Interrupt); 1clock // disable interrupts while we read timer0_millis or we might get an // inconsistent value (e.g. in the middle of a write to timer0_millis) cli( ); // 禁止中断; 1 clock m = timer0_millis; // 读取内存的全局变量 timer0_millis;8 clock SREG = oldSREG; // 恢复状态寄存器(注意不一定恢复中断喔 !);1 clock return m; // 6 clocks} // millis( // total 17 clock cycles其中的 timer0_millis; 由以下 ISR 中断程序负责更新:unsigned long timer0_millis=0; // 开机到现在几个 millis ?unsigned char timer0_fract=0; // 调整误差用unsigned long timer0_overflow_count; // 给 micros( ) 用SIGNAL(TIMER0_OVF_vect) { timer0_millis += 1; timer0_fract += 3; if (timer0_fract >= 125) { timer0_fract -= 125; timer0_millis += 1; } timer0_overflow_count++;} 程序中 timer0_overflow_count 是给 micros( ) 使用的 !! 以上这 SIGNAL(TIMER0_OVF_vect) 的意思是: timer0_millis 必须系统发现 TIMER0_OVF_vect 中断才会改变, 所以在 ISR( ) 内连续调用 millis( ) 其答案是不会变的 ! 因为在 ISR( ) 内中断是被禁止的, 根本没机会进入SIGNAL(TIMER0_OVF_vect), 所以在 ISR( ) 内连续调用 millis( ) 回传值不会变 ! 所以千万不要在 ISR( ) 内企图用 millis( ) 判断过了多久 ! 因为在 ISR( ) 内执行期间 millis( ) 在静止状态 !! 关于何时会执行上述的 SIGNAL(TIMER0_OVF_vect) 这 ISR( ) 以及更多与以上相关的说明请看以下这: 关于delay(), millis(), micros(), delayMicroseconds与定时器(教程) http://www.arduino.cn/thread-12468-1-1.html testOverflow.png (66.32 KB, 下载次数: 9) 请注意第二种写法delay不够我们要的时间 delay()新旧版本比较 旧版本 虽然原先 Arduino 的 delay( )是正确的, 不受 unsigned long 溢出或 Rollover 归零影响: 请看以下这 Arduino 的 delay( ) 代码: (正确 ) (2010年9月以前 Arduino 版本用的)void delay( unsigned long dtms ){ unsigned long start = millis(); while (millis() - start < dtms);;} // delay( 以上这程序完全正确, 且即使 millis( ) 溢出归零也不会有问题 !! 但是, 因为这种不断调用 millis( ) 有两个缺点:误差最多会大约 1ms不可以在 ISR( ) 中断程序内使用, 否则因为 millis( ) 不会变就回不来了 ! 所以, Arduino后来把 delay 改为使用检查 micros( ) 拖延所需的延迟时间! 2. 新版本 Arduino 自 2010/09/03 开始的版本已经把 delay( ) 改写如下:void delay(unsigned long ms) { uint16_t start = (uint16_t)micros(); while (ms > 0) { if (((uint16_t)micros() - start) >= 1000){ ms--; start += 1000; } // if }//while(}// delay( 这种新写法是 delay 的比较准确, 误差应该在 7us 以内(旧版delay误差可能高达1ms); 但是却使得 delay( )前后各调用一次 millis( )相减可能不等于 delay( )的 ms 数! 因为 millis( ) 回传的值其实会有 1ms 的误差! 所以:unsigned long bb = millis( ); delay(1000); unsigned long ee = millis( ); 则在新版本的 delay( ), 可能出现 ee -bb 只有 999 不是期望的 1000 的情形 !! 因为假设 delay前抓到 millis 是 5801, 在新版本的 delay 延迟 1000 之后, 很有可能是刚好在 6800快要变 6801 之时, 于是你delay后的 ee = millis( ); 很可能抓到 6800; 变很奇怪的只差999; 如果是以前的 delay( ) 版本就不会有这种问题, 但是以前写法的 delay( )本身会有最多 1ms 的误差! 还有, 新版的 delay( ) 因为不是靠 millis( ), 改用与中断无关的 micros( ), 所以 新版本的 delay( ) 也可以在ISR( )中断程序内使用! 收藏(0)