Arduino 单片机程序中处理时间戳、时间溢出和延时问题

这篇具有很好参考价值的文章主要介绍了Arduino 单片机程序中处理时间戳、时间溢出和延时问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

这个话题对其他单片机也适用,就是用来计时的变量万一溢出了该怎么整,类似那个经典的千年虫问题。实际上这个问题在日常生活中也很常见,比如,时钟上的小时最大值为23,从0 开始,每过24 小时归零一次,只按时钟上的小时数来记录时间,最多只能计24 小时。时间超过最大值后归零就是所谓的溢出问题。

一种常见的设计是用比较时间戳的方法来预约一个延时任务,比如在1 点整的时候开始一个2 小时的延时任务,1 + 2 = 3,所以任务预约在3 点执行,当前时间大于3 时这个任务就该执行了。但由于计时会溢出,如果在22 点想开始2 小时的延时任务,22 + 2 = 0 (溢出后归零),22 + 2 的结果反而比22 小,任务立即就执行了,发生BUG。最简单万能的处理方法自然是增大计时变量的尺寸,就像IPV4 扩展成IPV6,二进制数值每增加一位,最大值就增加一倍,按这个指数增长,很快就能把溢出周期增大到天文数字。但在单片机上,32 位变量全面增大到64 位的话还是有一些代价的,尤其是乘除法的处理。以下介绍两种常见的处理思路,顺便摘录一个Arduino 社区关于这个问题的回答。

1. 只比较时间间隔

原理很简单,虽然22 + 2 变成了0,但是按无符号整数的算法,0 - 22 还等于2,这也是加减法运算律的要求。当然实际的计算不是这样的,只是拿24 小时制简单说明一下,可以用如下C 代码测试:

#include <stdio.h>
#include <stdint.h>

int main()
{
    uint8_t a = 255;
    uint8_t b = a + 5;
    uint8_t c = b - a;

    printf("a = %u, b = %u, c = %u", a, b, c);
    
    return 0;
}

结果:a = 255, b = 4, c = 5。所以规避时间溢出BUG 的方法就是对时间戳做减法,把预约延时任务时的时间戳T0 = 22 和延时间隔ΔT = 2 都保存下来,然后持续获取当前时间T。延时过程中,当前时间T 会从T0 一直增加,直到两小时后,T 溢出变成0。此时做减法T - T0 = 0 - 22 = 2,和延时间隔ΔT 相等,预约完成。用这种思路实现的延时函数大概是下面这样:

void delay(int Δt) {
	int t0 = get_time();
	int t = t0;
	while((t - t0) < Δt) {
		t = get_time();
	}
}

只要算出来当前时间和T0 的间隔小于延时时间就持续死循环等待。这种方法必须额外保存一个t0 ,只是一个延时倒还好,如果要同时处理多个延时任务就会比较麻烦。比如,想实现定时执行任务的功能,每个任务在指定时间后调用一个回调函数,一般用结构体保存任务的相关信息,类似这样:

struct Task {
	int (*callback)();
	int t0;
	int Δt;
};

然后对每个任务都用上面延时函数的方式处理,每次更新任务状态时,用当前时间减去t0,再和Δt 比较。可以用,只不过有多个任务时,每个结构体里都多一个t0,会多占用一些RAM,效率不算高。

2. 倒计时器

想提高任务结构体的存储效率,只能把t0 去掉,然后用其他方式处理溢出问题。比如,可以把结构体里存储的延时时间换成一个倒计时器,结构体改写成这样:

struct Task {
	int (*callback)();
	int 倒计时;
};

每次更新任务状态时,把所有任务的倒计时变量减去一段时间,倒计时减完了就到执行任务的时候了。要减去的时间是两次更新任务状态的时间间隔,更新任务的函数大概是这样:

void update() {
	static int 第一次运行 = 1;
	static int 上次时间 = 0;
	int 这次时间 = get_time();
	int 间隔 = 上次时间 - 这次时间;
	上次时间 = 这次时间;
	
	if(第一次运行) {
		间隔 = 0;
		第一次运行 = 0;	
	}
	
	// 遍历所有任务
	for( /* ... */ ) {
		if(task.倒计时 <= 间隔) {
			// 说明倒计时再减一次就变成0 或负数,那就不用减了,直接执行回调函数
			task.callback();
			continue;
		}
		
		task.倒计时 -= 间隔;
	}
}

整体第一次运行时,计算出来的两次时间间隔是没有意义的,可以把间隔清零,或者额外设计别的初始化机制,比如,任务列表为空就不需要计时,所以不妨把第一个任务添加进来的时间作为计时初始值。只用一个倒计时变量的方式提高了存储效率,相比上一种方式,也没降低运行效率,因为上一种方式也要每个任务做一次减法然后比较时间。缺点在于,倒计时运行后,添加任务时的初始延时参数就没了,如果想实现能自动重复运行的任务,还得额外准备一个变量存储初始参数,这就和上一种方式没区别了。

摘录 - 如何处理millis() 函数归零问题

源地址:How can I handle the millis() rollover?

注:millis() 是Arduino 框架里返回时间毫秒值的函数,这个应该足够众所周知。下面把时间的溢出问题称作“翻转“ 问题(rollover)

时刻,时间戳和持续时间

在处理时间时,我们必须至少区分两个不同的概念:时刻持续时间。时刻是时间轴上的一个点。持续时间是时间间隔的长度,即定义间隔开始和结束的时刻之间的时间距离。在日常语言中,这些概念之间的区别并不总是非常明显。例如,如果我说“我五分钟后回来”,那么“五分钟”是我缺席的预计持续时间,而“五分钟后”是我预计回来的时刻。牢记这种区别很重要,因为这是完全避免翻转问题的最简单方法。

millis() 的返回值可以解释为持续时间:从程序开始到现在经过的时间。然而,一旦 millis() 溢出,这种解释就失效了。通常更有用的定义是认为 millis() 返回一个时间戳,即标识特定时刻的“标签”。这种解释的缺点是,因为时间戳每 49.7 天就会被重用(millis() 的溢出周期是49.7天),因此时间戳并不具有唯一性。然而,这很少是一个问题:在大多数嵌入式应用程序中,49.7 天前发生的任何事情都是我们不关心的古老历史。因此,回收旧标签不应该是一个问题。

不要比较时间戳

试图找出两个时间戳中哪一个大于另一个是没有意义的。例如:

unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }

天真的人们会认为 if () 的条件总是为真。但实际上,如果 millis()delay(3000) 期间溢出,它实际上会是假的。将 t1t2 视为可回收的标签是避免错误的最简单方法:时间戳 t1 明显被分配给在 t2 之前的某个时刻,但在 49.7 天后它将被重新分配给未来的时刻。因此,t2 的之前之后都存在t1。这应该清楚地表明了表达式 t2 > t1 没有意义。

但是,如果这些只是标签,那么显然的问题是:我们如何用它们进行有用的时间计算?答案是:仅使用对时间戳有意义的两种计算:

  1. 后来的时间戳 - 较早的时间戳 等于一段持续时间,即较早时刻和较晚时刻之间经过的时间量。这是涉及时间戳的最有用的算术运算。
  2. 时间戳 ± 持续时间 产生一个时间戳,该时间戳在初始时间戳之后(如果使用 +)或之前(如果 −)。听起来不像那么有用,因为结果时间戳只能用于两种计算中的一种…

由于取模运算的原理,这两者都保证在millis() 翻转时正常工作,只要涉及的延迟不超过 49.7 天。

注:这里提到取模运算是因为时间溢出和取模是相似的,若取溢出周期为T,取线性无限增长的真实时间为t,那么计时变量的实际值 v = t % T,也就是每过T 时间,计时变量就回归初值

比较持续时间是可以的

持续时间只是在某个时间间隔内经过的毫秒数。只要我们不需要处理超过 49.7 天的持续时间,任何在物理上有意义的操作在计算上也应该有意义。例如,我们可以将持续时间乘以频率得到周期数。或者我们可以比较两个持续时间,以了解哪一个更长。例如,这里是 delay() 的两种实现。首先,有缺陷的版本:

void myDelay(unsigned long ms) {          // ms: 持续时间
    unsigned long start = millis();       // start: 时间戳
    unsigned long finished = start + ms;  // finished: 时间戳
    for (;;) {
        unsigned long now = millis();     // now: 时间戳
        if (now >= finished)              // 比较时间戳:错误!
            return;
    }
}

这是正确的版本:

void myDelay(unsigned long ms) {              // ms: 持续时间
    unsigned long start = millis();           // start: 时间戳
    for (;;) {
        unsigned long now = millis();         // now: 时间戳
        unsigned long elapsed = now - start;  // elapsed: 持续时间
        if (elapsed >= ms)                    // 比较持续时间:正确
            return;
    }
}

大多数 C 程序员会以更简洁的形式编写上面的循环,例如

while (millis() < start + ms) ;  // 有缺陷的版本

while (millis() - start < ms) ;  // 正确的版本

尽管它们看起来骗人地相似,但时间戳/持续时间区别应该清楚地表明哪一个有缺陷,哪一个是正确的。文章来源地址https://www.toymoban.com/news/detail-691601.html

到了这里,关于Arduino 单片机程序中处理时间戳、时间溢出和延时问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Proteus仿真】【Arduino单片机】简易电子琴

    本项目使用Proteus8仿真Arduino单片机控制器,使用无源蜂鸣器、按键等。 主要功能: 系统运行后,按下K1-K7键发出不同音调。 B站演示视频:https://space.bilibili.com/444388619 视频地址:https://space.bilibili.com/444388619/video 专注于51单片机、STM32、国产32、DSP、Proteus、arduino、ESP32、物联网

    2024年02月06日
    浏览(36)
  • 【Proteus仿真】【Arduino单片机】路灯控制系统

    本项目使用Proteus8仿真Arduino单片机控制器,使用LCD1602显示模块、人体红外传感器、光线检测模块、路灯继电器控制等。 主要功能: 系统运行后,LCD1602显示时间、工作模式,光线强度及路灯工作状态。 如果晚上11点到凌晨4点,通过红外感应方式控制路灯; 当感应有人,路灯

    2024年01月16日
    浏览(36)
  • 基于Arduino单片机超声波测距仪设计

    文章目录 摘  要 1.课程设计任务 1.1课程设计题目 1.2设计的要求 2.设计总体方案 2.1初步设计方案 2.2各个单元电路的设计要求 2.3主要性能指标 2.4总体方案 3.单元模块设计 3.1显示模块 3.2超声波测距模块 3.3蜂鸣器模块 3.4电机模块 3.5 LED二极管模块 4.软件流程图 5.设计代码 5.1核

    2024年02月11日
    浏览(32)
  • 【Proteus仿真】【Arduino单片机】水箱液位监控系统

    本项目使用Proteus8仿真Arduino单片机控制器,使用LCD1602液晶、按键、蜂鸣器、液位传感器、ADC转换器、水泵等。 主要功能: 系统运行后,LCD1602显示当前水位、上下限阈值和工作模式,系统默认处于自动模式。 若检测水位高于上限,声光报警,开启抽水。若检测水位低于下限

    2024年02月20日
    浏览(36)
  • 【Proteus仿真】【Arduino单片机】HC05蓝牙通信

    本项目使用Proteus8仿真Arduino单片机控制器,使用PCF8574、LCD1602液晶、HC05蓝牙传感器等。 主要功能: 系统运行后,LCD1602显示蓝牙接收数据。 B站演示视频:https://space.bilibili.com/444388619 视频地址:https://space.bilibili.com/444388619/video 专注于51单片机、STM32、国产32、DSP、Proteus、ardu

    2024年04月16日
    浏览(39)
  • 【Proteus仿真】【Arduino单片机】汽车车窗除霜系统设计

    本项目使用Proteus8仿真Arduino单片机控制器,使用LCD1602显示模块、光线传感器、DS18B20温度传感器、PCF8691 ADC模块、继电器加热模块等。 主要功能: 系统运行后,LCD1602显示温度和光线强度值; 当车窗光线强度低于一定值,且车窗温度低于一定值,车窗开启加热;当光线强度达

    2024年01月16日
    浏览(38)
  • 【Proteus仿真】【Arduino单片机】SG90舵机控制

    本项目使用Proteus8仿真Arduino单片机控制器,使用SG90舵机等。 主要功能: 系统运行后,舵机开始运行。 B站演示视频:https://space.bilibili.com/444388619 视频地址:https://space.bilibili.com/444388619/video 专注于51单片机、STM32、国产32、DSP、Proteus、arduino、ESP32、物联网软件开发,PCB设计,

    2024年02月07日
    浏览(33)
  • esp32单片机在arduino环境下,WIF联网

    文章目录 系列文章目录 前言 一、WIFI是什么? 二、使用步骤 1.引入库 2.读入数据 总结 随着人工智能的不断发展,物联网这门技术也越来越重要,很多人都开启了物联网学习,本文就介绍了物联网WIFI模块的基础内容。 WIFI在中文里又称作\\\"行动热点\\\",是 Wi-Fi联盟 制造商的商标

    2024年02月15日
    浏览(38)
  • esp32单片机在arduino环境下,串口接收解码

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一、pandas是什么? 二、使用步骤 1.引入库 2.读入数据 总结 当主控芯片发送一串数据,但此数据为一串字符串,需要将此字符串翻译成整型数组,以形成指令,控制串口屏。 例如:esp32单

    2024年02月09日
    浏览(27)
  • ARM、树莓派、Arduino、单片机四者有什么不同

    一张图展示它们之间的关系,如下所示: 什么是arm? 其实ARM是一种微处理器架构,但它和“手臂”(Arm)毫无关系,哪怕在拼写上一样。虽然两者都十分强壮,但ARM更像一个小而精干的智能脑袋,可以帮助各种设备实现高效、可靠的计算和控制。 ARM,全称为Advanced RISC Mach

    2024年04月16日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包