极客工坊

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 13341|回复: 7

【教程】关于 millis( ) 与其 timer0 中断的秘密

[复制链接]
发表于 2015-4-8 22:41:31 | 显示全部楼层 |阅读模式
本帖最后由 tsaiwn 于 2015-4-8 23:09 编辑


(1)到底 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( ) 都不会改变答案 !


(2)关於 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++;
}


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


(3)何时会执行上述的 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) !!

(4)每隔 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 这三个之一个,  多出来的误差要累计到下次的计算內。



(5)接著来看看相关的 micros( ) 这 function 是如何写的:
   不过以下这程序已经被我简化成比较容易看懂(依据16MHz clock),
   它会用到前面提及的 timer0_overflow_count;  

unsigned long micros() {
    unsigned long m;
    uint8_t oldSREG = SREG; // 状態寄存器(包括是否允许 Interrupt)
    uint8_t t;  // 临时变量
    cli();    // 禁止 Interrupt
    m = timer0_overflow_count;  // timer0 已经 overflow 几次 ?
    t = TCNT0;  // timer0 目前的值
    if ((TIFR0 & _BV(TOV0)) && (t & 255)) m++; // timer0 目前的TCNT0值不是 0且欠一次中断
    SREG = oldSREG;  // 恢復状態寄存器(注意不一定恢復中断喔 !)
    return ((m *256) + t) * 4;  // 最大只能代表约 71.58分钟
} // micros(

  你可以看到它只是短暂禁止中断, 然后读取两个整数到 m 和 t,
  並在恢復中断状態后回传 ((m *256) + t) * 4; 这答案。

(6)为何 micros( ) 回传的值都是 4 的倍数 ?

   其实从程序最后回传值就知道一定是 4 的倍数 !
   前面说过因为 timer0 的 clock cycle 是每个 tick 0.004ms = 4 us,
   在该函数內最后是回传 ((m *256) + t) * 4;
   所以你会发现 micros( ) 回传的值都是 4 的倍数 !

     回传的 ((m *256) + t) * 4 这答案用白话文说,
  就是  ((TCNT0 已Overflow次数) * 256 + TCNT0 ) * 4
  注意前面说过该 timer0 的 TCNT0 是每 4 us自动加 1,
  这也是为何最后要乘以 4 获得几个 micro seconds的答案 !

     因为 micros( ) 答案是用 unsigned long 表示,
所以 micros( ) 大约开机后每70分钟会Overflow 归零,

      4294967296 /1000/1000 /60 分钟 =  71.58 分钟
  还有, 请注意, 在进入 micros( ) 之前可能已经禁止中断,
  所以结束 micros( )  之前不是用 eni( ); 恢復中断,
  是用 SREG = oldSREG; 恢復原先的中断状態!

  前面说过,
  如果你在 ISR( ) 內连续调用 millis( ) 其答案是不会变的 !
  因为在 ISR( ) 內中断是被禁止的, 根本没机会进入SIGNAL(TIMER0_OVF_vect),
  所以 millis( ) 回传值不会变,
  但是, 在 ISR( ) 內连续调用 micros( ) 则其值是会变的 !



回复

使用道具 举报

 楼主| 发表于 2015-4-8 22:57:03 | 显示全部楼层


【补充】



(1)为什么在 ISR( ) 內中断已经被禁止但连续调用 micros( ) 仍会有变化 ?
      因为从 micros( ) 的源代码可以看到,
  它的答案是 ((m *256) + t) * 4;
  其中的 m 就是 timer0_overflow_count 中断禁止时当然不会变,
  可是其中的 t 是 TCNT0 是由 timer0 自己不断的加 1,
  不会受到 CPU 是否允许中断或禁止中断的影响 !
  只是如果中断被禁止太久,
  由於禁止中断期间 timer0_overflow_count 都不会变,
  这样得到的 micros( ) 还是会受到禁止中断的影响 !
  我这里只是强调在 ISR( ) 內连续调用 micros( ) 会得到不同的值 !

(2)调用 micros( )要花掉多少时间 ?
      別忘了调用 micros( ) 也要时间,
   写 unsigned long kk = micros( ); 意思是跑去 micros( ) 里面,
   然后回传的答案要复製到 kk,
   跳去 micros( ) 要四个 CPU Clock cycle,
   把答案放入 kk 要 8 个 CPU Clock cycle,
   在 micros( ) 里面共会用掉 17 CPU clock cycle,
   所以, kk = micros( ); 总共会花掉  4 + 17 + 8 = 29 个 CPU Clock cycle;
   在 16MHz 状况下, 每个 CPU Clock cycle (tick)是 0.0625 us
   於是kk = micros( ); 总共会用到 29 * 0.0625 us = 1.8125 us
   就是说调用 micros( ) 本身会花掉將近 2 us,
   *** 注意不要把 CPU 的 tick (clock cycle) 与 timer 的 tick 搞混了 !
------------------
  关於 AVR 指令集以及各指令 clock 请看:
    http://www.atmel.com/images/doc0856.pdf

回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-8 23:02:14 | 显示全部楼层
tsaiwn 发表于 2015-4-8 22:57
【补充】


【再补充】

Q: 到底 delayMicroseconds( ) 会不会禁止中断 ? 有的说会, 有的说不会 ?

  来看 delayMicroseconds( ) 这 function !

     以下这是新版的 delayMicroseconds( ) 源代码,
(在2010/01/29之前的版本执行 delayMicroseconds( ) 之时会禁止中断 !!)
  新版本已经不再禁止中断,
  这样万一在 delayMicroseconds( ); 中途產生中断,
  由於一个中断处理程序即使甚么事都不做就中断返回也要42cycles大约 2.625us,
  ( 参看 http://www.gammon.com.au/forum/?id=11488 )
  於是实际 delay 的时间会比预期的更长一些 !

以下就是新版的 delayMicroseconds( )  的源代码:

void delayMicroseconds( unsigned long us ){
   if (--us == 0) return;  // 表示 us 是 1
   us <<= 2; // us = us*4, 因等下每Loop一次是 0.25us
   us -= 2;  // 修正这句以及前两句耗掉约 0.5us
   __asm__ __volatile__ (
      "1: sbiw %0,1" "\n\t"   // 2 cycles
      "brne 1b" : "=w" (us) : "0" (us) // 2 cycles
    );
} // delayMicroseconds(


     上面那两句用 __asm__ __volatile__ ( ) 夹住看起来像天书的,
  是组合语言写的小 Loop,
  共会依据参数 us 做 Loop  (unsigned int)( 4 * (us-1) -2) 次,
  每 Loop 一次是 0.25us;
  还有,
  由於进入 delayMicroseconds( ) 与离开 delayMicroseconds( )
  合起来就大约 1.125 us,
  因此delayMicroseconds( )会有大约 1us 的误差 !
  实际上 delayMicroseconds(1)会花掉约 1.4375 us;
  delayMicroseconds(2)则会花大约 2.25 us;
  可是因为 micros( ) 回传的值必定是 4 的倍数,
  如果你用 micros( )检查是看不出来的 !!

  注意因为考虑准確度的问题, 虽然参数是 unsigned long,
  但是官方网站建议 us 最大只可以到  16383 不然会很不准: (因为目前版本其实只用unsigned int, 且把参数乘以4)
     http://arduino.cc/en/Reference/delayMicroseconds

  ** 其实 16383 就是 65535 除以 4 的整数 (unsigned int 的最大就是 65535) !

  ** 如果 us 超过 16383 则delayMicroseconds(us); 会很不准確,
      主要原因是那会相当於 delayMicroseconds(us % 16384);
      因为从delayMicroseconds( ) 源代码可看到它做了  us = (us -1)*4 -2 然后只用us右边两个 bytes.

请注意, 在 delayMicroseconds( ) 的源代码中所夹的组合语言,

     由於它是使用 SBIW 指令对新的 us 做减一並检查是否还没减到 0 (brne)就继续Loop,
     可是注意 sbiw 是 16 bit 指令,  这表示虽然参数 us 是 unsigned long,

     但其实目前版本的 delayMicroseconds( us ) 只用 us 变量(减去1后乘以四再减去2)右边的 16 bits,
    所以, 如果你的 us 大於 16383,
    则其真正 delay 的时间將会变成延迟大约 (us % 16384) micro second.
    换句话说, 写 delayMicroseconds( 16483 );
    將会变成与 delayMicroseconds( 99 ); 几乎相同 !!!

        还有, 因代码內立即对 us 减去 1才判断是否为 0,
    所以, delayMicroseconds(0); 反而是delayMicroseconds()延迟最久的,
    將会比 delayMicroseconds(16383); 还久一点点(大约多 1 us) !

   关於 Arduino 所用 CPU 的指令可以看:
        http://en.wikipedia.org/wiki/Atmel_AVR_instruction_set
   或看 AVR 指令集 (Instruction Set)
        http://www.atmel.com/images/doc0856.pdf
   或是看 ATmega328 的 datasheet:
        http://www.atmel.com/Images/doc8161.pdf  (P.427-)
   或看 AVR 组合语言入门
        http://www.avr-asm-tutorial.net/avr_cn/beginner/

回复 支持 反对

使用道具 举报

发表于 2015-4-9 09:43:38 | 显示全部楼层
寫得真好,謝謝教導!
回复 支持 反对

使用道具 举报

发表于 2015-4-9 12:41:26 | 显示全部楼层
谢谢分享学习一下
回复 支持 反对

使用道具 举报

发表于 2015-5-6 21:15:27 | 显示全部楼层
谢谢分享~学到了很多知识。
回复 支持 反对

使用道具 举报

发表于 2015-5-9 16:46:00 | 显示全部楼层
{:soso_e179:}{:soso_e179:}{:soso_e179:}  不错的分析
回复 支持 反对

使用道具 举报

发表于 2015-5-26 16:09:21 | 显示全部楼层
{:soso_e179:}{:soso_e179:}{:soso_e179:}{:soso_e179:}很好
回复 支持 反对

使用道具 举报

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

本版积分规则 需要先绑定手机号

Archiver|联系我们|极客工坊

GMT+8, 2024-3-29 05:09 , Processed in 0.041412 second(s), 22 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2021, Tencent Cloud.

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