文章目录
为何 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 这三个之一个,
多出来的误差要累计到下次的计算内。