为何 delay(1000); 前后只有 999ms (milli second)?

为何 delay(1000); 前后只有 999ms (milli second)?

https://www.arduino.cn/thread-15132-1-1.html
首先感谢 xvb13135 帮忙发现这个问题 !
什么问题 ? 就是 delay( ) “看起来” 好像不准的问题, 请看:

  unsigned long st = millis( );
  delay(1000);
  unsigned long et = millis( );
  Serial.println(et - st);  // 差距多少个 ms

结果印出 999,
你不相信对不对 !? 一开始我也不相信 !
如果印出的是 1000 或 1001 应该大家都会相信,
但是, 999 怎么可能呢 ?!
要感谢 xvb13135 问了这个问题 ?
一开始打死我也不相信会有这种事,
因为我明明看过 delay( ) 的程序源码:
(注意 delay 是以 ms(millis second)为最小单位, 1 ms = 1000 us)

void delay( unsigned long ms ){
  unsigned long start = millis();
  while (millis() - start < ms) {
    // do nothing here
  } // while(
} // delay(

你会发现,这 delay( ) 的代码很简单,它就是不断的调用 millis( ) 看看时间到了没?时间没到就不返回, 一点学问都没吧 ?

请注意,因为 millis( ) 的值是靠 timer0 的中断帮忙每次加 1,如果中断请求被禁止,则每次调用 millis( ) 会得到一样的值。
为什么?
看过 millis( ) 以及它的相关函数就知道了 !

这样看来 delay(1000)前后一定至少差 1000 啊, 偶而差 1001 也是正常,
因可能被中断导致 loop 做完回到我们调用 millis( ) 又多跳了1ms,
但是, 说要得到 999 是绝对不可能的事 !
问题是, 事实摆在眼前, 由图片有证据 !
所以, 我重新去看源代码,
啥 !? 是我脑筋停留在四年前看的 ?
原来 delay( ) 早就改写了,
查看 Arduino Release Note:
http://arduino.cc/en/Main/ReleaseNotes
发现 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(1000); 之后却只有差 999ms 的情形,
不过好处是, 它这种新的写法误差在应该在 7us 以内, 以前旧版delay( ) 的误差可能高达1ms;

通常有好处就有坏处, 延迟的误差是变小了,
但是, 本来旧版本的写法不会有前后查看 millis( ) 发现结果怪怪的问题,
新写法却会有这种感觉似乎少 delay 1 milli second, 但其实并没有喔 !
会这样, 这也是没办法的事 !
delay( ms )前后各调用一次 millis( )相减竟然小于 delay( )的 ms 数!
怎会这样呢?
这其实是 millis( ) 本身的问题 !
因为 millis( ) 本身就会有 1 ms 的误差 !!
这以前我有写过一篇跟大家分享 millis( ) 是怎么写的 ?
现在再拿出来分析何以 delay(ms); 前后调用 millis( ) 竟然发生只有 ms-1的原因 !!
假设 delay前抓到 st = millis( ); 是 5801,在新版本的 delay 延迟 1000 之后,
很有机会是刚好在 6800快要变 6801 之时(但还没变),
于是你delay后的 et = millis( ); 很可能抓到 6800; 变很奇怪的只差999;
如果是以前的 delay( ) 版本就不会有这种问题;
这样好像现在的 delay( )比以前的 delay( )不准确,
错了, 其实现在的 delay( )是比较准确的, 它只是偶而"看起来"不准 !
注意以前写法的 delay( )本身会有最多 1ms 的误差延迟,
但从 delay( ) 前后抓 millis( ) 是看不出来的!

既然现在新的 delay( )比较准确,
那怎会"看起来"比较不准呢?
刚刚说了, 这是因为 millis( ) 本身的误差造成的 !
好吧, 再来看看 millis( ) 是怎么写的 :

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

啥?
原来它只是先用 cli( ) 把中断请求禁止,
然后读取 timer0_millis; 放到临时变量 m,
接着还原中断状态, 然后把 m 送回来 !
请注意我是说"还原中断状态",
不是说"恢复中断",
Why?
因为原本在进入 millis( ) 之前有可能已经是禁止中断的状态,
是否禁止中断被记录在 SREG 中的一个 bit,
在送回答案之前做 SREG = oldSREG; 还原中断状态,
因为在刚进入 millis( ) 时有把 SREG 先复制到 oldSREG 这临时变量中!
注意虽然你在 ISR( ) 内可以调用 millis( ),
但是在 ISR( ) 内因为中断请求被禁止,
所以连续调用 millis( ) 得到的答案都不会变喔 !
因此千万不要在 ISR( ) 中断程序内写如下:

   while( millis( ) < timeUP ) {
     //.. do nothing 或 do something
   }

这样这 while Loop 会陷入永不停止的 LOOP !!!
因为 millis( ) 都不会改变答案 !

关于timer0_millis全局变量

关于 timer0 的中断与其处理程序 SIGNAL(TIMER0_OVF_vect)

是谁负责计算 timer0_millis 这个变量(Variable, 变量) ?
既然 millis( ) 的答案来自 timer0_millis 这个变数(Variable), 那 timer0_millis 这是啥东西呢?
原来它是一个全局变量(Global variable), 意思是可被各 function 存取(访问)的 unsigned long 变量。
那又是谁负责计算这 timer0_millis 呢? 是一个中断程序负责, 如下:

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++;   // 这是给 micros( ) 计算用的
}

//P.S. SIGNAL(…){…} 是以前旧版中断的写法, 后来改为写 ISR( … ){ … }

看到这里, 我们发现 millis( ) 答案来自 timer0_millis;
而 timer0_millis 必须系统发现 TIMER0_OVF_vect 中断才会改变(稍后讨论),
所以在 ISR( ) 内连续调用 millis( ) 其答案是不会变的 !
因为在 ISR( ) 内中断是被禁止的, 根本没机会进入SIGNAL(TIMER0_OVF_vect),所以在 ISR( ) 内连续调用 millis( ) 回传值不会变 ! 所以千万不要在 ISR( ) 内企图用 millis( ) 判断过了多久 ! 因为在 ISR( ) 内执行期间 millis( ) 在静止状态 !!

何时会执行上述的 SIGNAL(TIMER0_OVF_vect) 这 ISR( ) ? 为什么 ?

 好了, 剩下的问题是何时会执行上述中断代码 SIGNAL(TIMER0_OVF_vect) ?

这代码的 TIMER0_OVF_vect 名称就已经说明了是当 timer0 发生 Overflow的中断, 也就是 timer0 的内部计数寄存器 TCNT0 算了一轮回(0,1,2…254, 255, 0), 从 255 加 1 又变为 0 之时(这时称 Overflow 溢位)会产生中断进入这处理程序 !

那么 timer0 的 TCNT0 每隔多久会加 1 呢? 就是每当 timer0 被 “踢” 一下的时候啦! 被 "踢"一下就是 timer0 的频率变化一下, 称作一个 tick 或一个 clock cycle;
由于 timer0 的 Prescaler 是被Arduino设定为 64, Arduino 大都使用 16 MHz 的频率,除频 64 之后给 timer0 用,
则每个 clock cycle (或称 tick) 时间为:

  1 秒 / (16 000 000 / 64) = 1/250000 =  0.000004 sec = 0.004 ms

所以给 timer0 的 tick 是每个 tick 0.004ms = 4 us (micro second)。
意思是每隔 0.004 milli sec 定时器(定时器)的频率电路会"踢" timer0 一下, 这使得 timer0 会自动把 TCNT0 加 1, (注意不是靠 CPU 喔!)
因为 TCNT0 只有 8 bit, 看作无符号整数 (unsigned char), 既然 TCNT0 每 0.004ms 会自动加 1, 总会加到 255, 然后 255 再加 1 变回 0 (即 Overflow), 共使用256 ticks, 共花了 0.004 ms * 256 = 1.024ms,
这时会对 CPU 产生中断一次, 要求 CPU 进入上述的中断处理程序SIGNAL(TIMER0_OVF_vect) 处理 。
*** 注意给 CPU 的 clock cycle 是 0.0625 us 喔(没有除以 64) !!

**每隔 1.024ms 把 millis 加 1 岂不是有误差 0.024ms 那要如何修正 ?
我们看到了在中断处理程序中主要是把 timer0_millis 加 1, 但请注意, 实际上这时是经过了 1.024 ms, 并不是 1ms, 也就是产生了误差, 长此以往, 这误差会越来越大 !
还好, Arduino 的工程师很聪明, 另外用一个变量 timer0_fract 纪录误差, 就是 timer0_fract += 3;
然后你会发现在 (timer0_fract >= 125) 时会做调整:

    if (timer0_fract >= 125) {
       timer0_fract -= 125;
       timer0_millis += 1;
    }

这个动作跟闰年(Leap year)原理类似,
因为地球绕太阳一圈的回归年其实是365.2421990741天,
不是 365天也不是 366天, 所以每四年要闰年一次多一天,
可是四年多一天等于算做一年是 365.25 天, 又不准了,
因此每一百年又把多算的一天取消(公元年/100整除不是闰年)做修正 !!
这里的算法是因每次误差 0.024 ms, 用 3 代表,
然后 125 就是代表 0.024ms * 125 = 1.000ms,
因此如果 (timer0_fract >= 125) 就要把 millis 加 1,
并且要做 timer0_fract -= 125;
注意不是设为 0 喔, 是减去 125,
因这时可能是125, 126, 127 这三个之一个,
多出来的误差要累计到下次的计算内。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值