极客工坊

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 18602|回复: 11

【你不知的秘密】为何 delay(1000); 前后只有 999ms (milli second)?

[复制链接]
发表于 2015-4-12 18:26:17 | 显示全部楼层 |阅读模式
首先感谢 xvb*** 坛友 帮忙发现这个问题 !

什么问题 ? 就是 delay( ) "看起来" 好像不准的问题, 请看:
  unsigned long st = millis( );
  delay(1000);
  unsigned long et = millis( );
  Serial.println(et - st);  // 差距多少个 ms

结果印出 999,

你不相信对不对 !? 一开始我也不相信 !
如果印出的是 1000 或 1001 应该大家都会相信,
但是, 999 怎么可能呢 ?!

    要感谢坛友问了这个问题 :-)
一开始打死我也不相信会有这种事,
因为我明明看过 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( ) 看看时间到了没?
时间没到就不返回,
一点学问都没吧 :-)

     这样看来 delay(1000)前后一定至少差 1000 啊, 偶而差 1001 也是正常,
因可能被中断导致 loop 做完回到我们调用 millis( ) 又多跳了一 ms,
但是, 说要得到 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 的误差 !!
回复

使用道具 举报

 楼主| 发表于 2015-4-12 18:32:11 | 显示全部楼层
补充

假设 delay( ) 前抓到 st = millis( ); 是 5801,
在新版本的 delay(1000); 延迟 1000 ms 之后,
很有机会是刚好 millis( ) 在 6800快要变 6801  之时(但还没变),
於是你delay后的 et = millis( ); 很可能抓到 6800;
变很奇怪的前后只差999 ms;
如果是以前的 delay( )  版本就不会有这种问题;
这样好像现在的 delay( )比以前的 delay( )不准確,
错了,
其实现在的 delay( )是比较准確的,
它只是偶而"看起来"不准 !
注意以前写法的 delay( )本身会有最多 1ms 的误差延迟,
但从 delay( ) 前后抓 millis( ) 是看不出来的!
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-12 18:38:15 | 显示全部楼层
tsaiwn 发表于 2015-4-12 18:32
补充

假设 delay( ) 前抓到 st = millis( ); 是 5801,

再补充

再来看看 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

注意虽然你在 ISR( ) 內可以调用 millis( ),
但是在 ISR( ) 內因为中断请求被禁止,
所以连续调用 millis( ) 得到的答案都不会变喔 !
因此千万不要在 ISR( ) 中断程序內写如下:
   while( millis( ) < timeUP ) {
     //.. do nothing 或 do something
   }
这样这 while Loop 会陷入永不停止的 LOOP !!!
因为 millis( ) 都不会改变答案 !
Why ?
因为 timer0_millis 这个变量(Variable) 是靠 TIMER0_OVF_vect 中断改变的,
中断被禁止了,  timer0_millis  就不会改变 !

回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-12 18:44:46 | 显示全部楼层
本帖最后由 tsaiwn 于 2015-4-12 18:52 编辑
tsaiwn 发表于 2015-4-12 18:38
再补充

再来看看 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( ) 其答案是不会变的 !

何时会发生 TIMER0_OVF_vect 中断 ?
就是何时会执行上述的 SIGNAL(TIMER0_OVF_vect) 这 ISR( ) ?
下回分解 :-)
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-12 18:48:48 | 显示全部楼层
tsaiwn 发表于 2015-4-12 18:44
再补充

到底 timer0_millis 这个变量(Variable) 何时会被改变 ?


到底何时会执行上述的 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) 处理 。

每隔 1.024ms 把 millis 加 1 岂不是有误差 0.024ms 那要如何修正 ?
先让大家想一想, 明天再告诉大家
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-12 18:51:25 | 显示全部楼层
tsaiwn 发表于 2015-4-12 18:48
到底何时会执行上述的 SIGNAL(TIMER0_OVF_vect) 这 ISR( ) ? 为什么 ?
     好了, 剩下的问题是何时会 ...

其实答案已经在该中断程序码内,
应该大部分坛友看得懂吧
明天再补充解释
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-12 18:59:50 | 显示全部楼层
tsaiwn 发表于 2015-4-12 18:48
到底何时会执行上述的 SIGNAL(TIMER0_OVF_vect) 这 ISR( ) ? 为什么 ?
     好了, 剩下的问题是何时会 ...


补充

请不要把 timer0 的 clock cycle (tick) 与 CPU 的 clock cycle 搞混了喔 !
这里是以 Arduino UNO 为例,
使用 16MHz 的 Clock,
所以一个 CPU clock cycle 是 1秒/16M =  0.0625 us;
也就是每 16 clock 是一个 us (micro second)
要至于这 timer0 的一个 clock cycle 是:
   ( timer0 的 Prescaler 是被Arduino设定为 64)
  1 秒 / (16 000 000 / 64) = 1/250000 =  0.000004 sec = 0.004 ms
所以给 timer0 的 tick 是每个 tick  0.004ms = 4 us (micro second)。


回复 支持 反对

使用道具 举报

发表于 2015-4-12 19:20:03 | 显示全部楼层
nice work,分析的很细致
回复 支持 反对

使用道具 举报

发表于 2015-4-12 23:27:00 | 显示全部楼层
这才是专家!
回复 支持 反对

使用道具 举报

发表于 2015-4-13 11:19:25 | 显示全部楼层
这内容好像看到过好多次了。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-13 14:40:50 | 显示全部楼层
ofourme 发表于 2015-4-13 11:19
这内容好像看到过好多次了。

当然啊..都是我写的
但是主题不同, 强调的重点不同
回复 支持 反对

使用道具 举报

发表于 2015-4-13 15:28:45 | 显示全部楼层
谢谢分享学习一下
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|联系我们|极客工坊

GMT+8, 2026-6-19 02:00 , Processed in 0.036572 second(s), 21 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表