millis() 溢出(overflow)归零(rollover)有沒问题?

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;我们连续测二次, 第二次 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.2 用arduino测试

要测 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)
    ;
}

输出结果:

 -1   ===   4294967295
test1: time =4294964308

AA: start =4294964309==-2987
after 2000, now=4294966309
after total 5000 delay, now=2013
After delay(2500)  endt=4512, run time = 7499, should be 7500

test2: time =4294964323
BB: start =4294964335==-2961
after 2000, now=4294966335
after total 5000 delay, now=4294966335
After delay(2500)  endt=1539, run time = 4500, should be 7500

=== 2nd Run for test2
test2: time =4294963921
BB: start =4294963933==-3363
after 2000, now=4294965933
after total 5000 delay, now=4294965933
After delay(2500)  endt=1137, run time = 4500, should be 7500
Q: 到底第二种条件式写法在何时会出问题 ?

A: 就是当millis()得到的数表示成有正负号的正数时, start = millis( ) 超过 unsigned long 最大数的一半, 且该 start +dtms 结果是正数之时就会出问题!请注意这里的 dtms 是指范例中你想要 delay 的几个 ms (毫秒)!
例如假设 millis( ) 得到的数看作有正负数,比如start = millis() = 4294966496; start是signed类型,start就是 -800, 但你要 delay 805 ms,
millis() < -800 + 805 也就是4294966496< 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定时器(教程)

请注意第二种写法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( )有两个缺点:

  1. 误差最多会大约 1ms
  2. 不可以在 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( )中断程序内使用!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔚蓝慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值