简短的答案:不要试图“处理”毫秒转换,而是编写安全转换代码。您的教程示例代码很好。如果尝试检测翻转以实施纠正措施,则可能是您做错了什么。大多数Arduino程序仅需要管理持续时间相对较短的事件,例如将按钮弹跳50毫秒或打开加热器12个小时...然后,即使该程序打算一次运行数年,毫不担心侧翻。
管理(或避免管理)过渡问题的正确方法是根据模块化算法考虑unsigned long
返回的数字
。对于数学上的偏爱,在编程时熟悉此概念非常有用。您可以在尼克· 加蒙( Nick Gammon)的文章millis()溢出中看到运行中的数学……一件坏事吗?。对于那些不想了解计算细节的人,我在这里提供了一种替代的方法(希望更简单)。它基于瞬时和持续时间之间的简单区别。只要您的测试仅涉及比较持续时间,就可以了。millis()
关于micros()的注释:此处所说的所有内容millis()
同样适用micros()
,除了micros()
每71.6分钟翻转一次,并且setMillis()
下面提供的功能不影响micros()
。
瞬间,时间戳和持续时间
在处理时间时,我们必须区分至少两个不同的概念:瞬间和持续时间。瞬间是时间轴上的一个点。持续时间是时间间隔的长度,即定义间隔开始和结束的瞬间之间的时间距离。在日常语言中,这些概念之间的区别并不总是很明显。例如,如果我说“ 我将在五分钟之内回来 ”,则“ 五分钟 ”是我缺席的估计
持续时间,而“ 五分钟 ”是瞬间
我预测的回来。记住区别很重要,因为这是完全避免过渡问题的最简单方法。
的返回值millis()
可以解释为持续时间:从程序开始到现在的时间。但是,这种解释一旦毫力士溢出就会中断。通常millis()
,将其视为返回
时间戳(即标识特定时刻的“标签” )通常有用得多。可以争辩说,由于这些标签每49.7天被重复使用,因此这些标签含糊不清。但是,这很少有问题:在大多数嵌入式应用程序中,49.7天前发生的任何事情都是我们不关心的古老历史。因此,回收旧标签应该不是问题。
不比较时间戳
试图找出两个时间戳中哪个大于另一个时间戳是没有意义的。例:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
天真地,人们希望条件if ()
永远是正确的。但是如果在比赛期间Millis溢出的话,这实际上是错误的
delay(3000)
。将t1和t2视为可回收标签是避免该错误的最简单方法:标签t1显然已分配给t2之前的一个瞬间,但在49.7天内它将被重新分配给将来的一个瞬间。因此,t1发生在 t2 之前和之后。这应该明确表示该表达式t2 > t1
没有任何意义。
但是,如果这些仅仅是标签,那么显而易见的问题是:我们如何使用它们进行任何有用的时间计算?答案是:将自己限制在只有两个对时间戳有意义的计算中:
later_timestamp - earlier_timestamp
产生持续时间,即在较早的时刻和较晚的时刻之间经过的时间量。这是涉及时间戳的最有用的算术运算。
timestamp ± duration
产生一个时间戳,该时间戳在初始时间戳之后(如果使用+)或之前(如果-)。听起来不那么有用,因为生成的时间戳只能用于两种计算中...
由于采用了模块化算法,因此至少在涉及的延迟短于49.7天的情况下,这两种方法都可以保证在毫秒转换中正常工作。
比较持续时间很好
持续时间只是在某个时间间隔内经过的毫秒数。只要我们不需要处理超过49.7天的持续时间,任何在物理上有意义的操作都应该在计算上有意义。例如,我们可以将持续时间乘以频率来获得多个周期。或者我们可以比较两个持续时间,以了解哪个更长。例如,这是的两个替代实现delay()
。首先,越野车:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
unsigned long finished = start + ms; // finished: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
if (now >= finished) // comparing timestamps: BUG!
return;
}
}
这是正确的:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
unsigned long elapsed = now - start; // elapsed: duration
if (elapsed >= ms) // comparing durations: OK
return;
}
}
大多数C程序员会以简短的形式编写上述循环,例如
while (millis() < start + ms) ; // BUGGY version
和
while (millis() - start < ms) ; // CORRECT version
尽管它们看起来看似相似,但时间戳/持续时间的区别应该明确哪个是错误的,哪个是正确的。
如果我真的需要比较时间戳怎么办?
最好尽量避免这种情况。如果不可避免,仍然有希望知道相应时刻是否足够接近:比24.85天短。是的,我们最大的可管理延迟时间为49.7天,减少了一半。
显而易见的解决方案是将时间戳比较问题转换为持续时间比较问题。假设我们需要知道时刻t1是在t2之前还是之后。我们选择它们共同的过去中的某个参考时刻,并比较从该参考直到t1和t2的持续时间。通过从t1或t2中减去足够长的持续时间来获得参考时刻:
unsigned long reference_instant = t2 - LONG_ENOUGH_DURATION;
unsigned long from_reference_until_t1 = t1 - reference_instant;
unsigned long from_reference_until_t2 = t2 - reference_instant;
if (from_reference_until_t1 < from_reference_until_t2)
// t1 is before t2
可以简化为:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
倾向于进一步简化为if (t1 - t2 < 0)
。显然,这是行不通的,因为t1 - t2
作为无符号数计算的不能为负。但是,这虽然不是便携式的,但确实可以工作:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
signed
上面的关键字是多余的(平原long
总是带符号的),但是它有助于使意图更清晰。转换为有符号长等于设置为LONG_ENOUGH_DURATION
等于24.85天。该技巧不可移植,因为根据C标准,结果是实现定义的。但是由于gcc编译器承诺会做正确的事,因此它可以在Arduino上可靠地工作。如果我们希望避免实现定义的行为,则上述签名的比较在数学上等效于此:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
唯一的问题是比较向后看。只要long为32位,它也等同于此一位测试:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
最后三个测试实际上是由gcc编译成完全相同的机器代码。
我如何针对米利斯翻转测试我的草图
如果您遵循上述规则,那么您应该一切都很好。如果仍然要测试,请将此功能添加到草图中:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
现在,您可以通过调用来遍历您的程序
setMillis(destination)
。如果您希望它一遍又一遍地经历毫氏溢出,就像Phil Connors放荡土拨鼠日一样,可以将其放入其中loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
上面的负时间戳(-3000)由编译器隐式转换为无符号长,对应于翻转前的3000毫秒(转换为4294964296)。
如果我真的需要追踪很长的时间怎么办?
如果您需要在三个月后打开并关闭继电器,那么您确实需要跟踪毫秒的溢出。有很多方法可以做到这一点。最直接的解决方案可能是简单地扩展millis()
到64位:
uint64_t millis64() {
static uint32_t low32, high32;
uint32_t new_low32 = millis();
if (new_low32 < low32) high32++;
low32 = new_low32;
return (uint64_t) high32 << 32 | low32;
}
这实际上是对翻转事件进行计数,并将此计数用作64位毫秒计数中的32个最高有效位。为了使此计数正常运行,需要至少每49.7天调用一次该函数。但是,如果每49.7天仅调用一次,则在某些情况下,检查可能会(new_low32 < low32)
失败,并且代码会丢失计数high32
。根据时间框架的排列方式,使用millis()决定何时仅在一次“包装” millis(特定的49.7天窗口)中仅对该代码进行调用是非常危险的。为了安全起见,如果使用millis()确定何时仅对millis64()进行调用,则每个49.7天的窗口中应该至少有两个调用。
但是请记住,在Arduino上64位算术运算昂贵。降低时间分辨率以保持在32位可能是值得的。