查看: 2573|回复: 4

【神秘知識】為何 delay(1000); 前後只有 999ms (milli second)?

[复制链接]
  • TA的每日心情
    擦汗
    2016-9-23 12:33
  • 签到天数: 170 天

    [LV.7]常住居民III

    发表于 2015-4-12 11:17 | 显示全部楼层 |阅读模式

        首先感謝 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( ) 又多跳了一 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 的誤差 !!
    這以前我有寫過一篇跟大家分享 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 的中斷與其處理程序 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 這三個之一個,
      多出來的誤差要累計到下次的計算內。


    评分

    参与人数 1贡献 +1 收起 理由
    奈何col + 1

    查看全部评分

  • TA的每日心情
    奋斗
    2015-8-28 10:57
  • 签到天数: 146 天

    [LV.7]常住居民III

    发表于 2015-4-12 11:19 | 显示全部楼层
    点个赞,还真没注意过这个问题,没有用过精确定时
  • TA的每日心情
    开心
    2015-8-4 08:02
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2016-2-16 21:31 | 显示全部楼层
    太精细了,这也能想得到啊

    该用户从未签到

    发表于 2016-2-17 08:29 | 显示全部楼层
    都看了,没明白,有时间再看一遍吧,谢谢楼主

    该用户从未签到

    发表于 2016-6-19 16:22 | 显示全部楼层
    不得不说,楼主真大神。刷了楼主的几个帖子,受益匪浅。确实把arduino研究的很透,对于想深度开发arduino的人来说能少走太多弯路了!
    您需要登录后才可以回帖 登录 | 立即注册  

    本版积分规则

    热门推荐

    体验OneNET,登录就送好礼
    体验OneNET,登录就送好礼
    OneNET--中国移动物联网开发平台,解决协议适配、海量连接、数据存储、设备管理等物
    初学arduino求助
    初学arduino求助
    大佬们,我想问一下,我现在想做一个程序:按下开关持续时间t,延迟7.77t后点亮led0.
    Arduino模拟USB鼠标
    Arduino模拟USB鼠标
    [md]### 模拟鼠标控制 下面将使用摇杆模块和Arduino Leonardo模拟USB鼠标。 [/md] [s
    ATK-esp8266WiFi模块
    ATK-esp8266WiFi模块
    我的esp8266模块以前烧进去过一次,半年没玩了。重新烧就提示错误了[/backcolor] 这是
    Arduino MEGA 与UNO 通过nRF24L模块通讯
    Arduino MEGA 与UNO 通过n
    之前在深水宝很“实惠”的店铺买了一些原件,随手砍了esp8266以及nRF24L*3 因为缺
    Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   ( 蜀ICP备14017632号-3 )
    快速回复 返回顶部 返回列表