【教程】关于 millis( ) 与其 timer0 中断的秘密
本帖最后由 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( ) 则其值是会变的 !
【补充】
(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
tsaiwn 发表于 2015-4-8 22:57 static/image/common/back.gif
【补充】
【再补充】
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/
寫得真好,謝謝教導! 谢谢分享学习一下 谢谢分享~学到了很多知识。 {:soso_e179:}{:soso_e179:}{:soso_e179:}不错的分析 {:soso_e179:}{:soso_e179:}{:soso_e179:}{:soso_e179:}很好
页:
[1]