前文指出了基于系统滴答计数实现的毫秒级延时的问题。
uint32_t comm_get_ms(void)
{
return sys_tick_get();
}
void comm_delay(uint32_t ms)
{
uint32_t timeout = comm_get_ms() + ms;
while(comm_get_ms() 《 timeout);
}
comm_get_ms返回当前系统时间(系统滴答计数),即系统从启动到现在经过了多少毫秒。comm_delay先获取当前时间,加上延时时间以计算出到期时间timeout,之后循环等待当前时间超过timeout以完成延时。
系统时间使用uint32_t变量来记录,经过49.71天后将达到最大值UINT32_MAX(0xffffffff),溢出后回到0重新累加。不仅是当前时间会溢出,在接近49.71天时,计算的timeout将会更先一步溢出,从而使延时判断失效。
前文在结尾给出了解决方案:
void comm_delay(uint32_t ms)
{
uint32_t timeout = comm_get_ms() + ms;
while(comm_get_ms() - timeout 》 UINT32_MAX / 2);
}
其实改动很小,仅仅修改了判断超时的条件。为什么要用两个时间差去与UINT32_MAX / 2比较?判断条件为什么是大于?
了解其中的原理是有必要的。因为延时的条件如上,而如果想实现定时的话,条件就会倒过来。知其所以然,方能灵活运用。
定时任务:
uint32_t timeout = 0;
while (1)
{
if (comm_get_ms() - timeout 《 UINT32_MAX / 2)
{
printf(“hello
”);
timeout = comm_get_ms() + 1000;
}
}
主要矛盾
无论是延时还是定时,我们都是在进行时间的比较。先根据延时或定时时长计算出到期时间timeout,之后不停的判断当时时间有没有超过这个timeout。
所有的时间变量都是uint32_t,由于它的最大值非常大,为了方便讲解,我们假设所有的变量都是uint8_t,即8位无符号整型,取值范围为0-255。同样为方便叙述,以cur_time表示当前时间,以timeout表示目标到期时间。
现在的任务也非常清楚了,在各种场景下比较cur_time是否超过了timeout。比如:
起始cur_time为10,延时目标为5,则timeout为10 + 5 = 15。判断依据非常简单,cur_time 《 15时视为未超过timeout,或者说cur_time 《 timeout视为未超过timeout。
起始cur_time为250,延时目标为10,则timeout为250 + 10 = 260 = 4。此时cur_time 《 timeout不再适用。
张三和李四谁跑的快
既然时间溢出问题让我们头疼,那我们先来看一个简单的问题,一个任何人都可以不假思索得出答案的问题:判断跑道上的张三和李四谁跑的快,或者说谁跑在前面。
如下图,张三(A)和李四(B)在跑道上跑步,沿逆时针方向跑。蓝色是起跑线,不过他们并不只跑一圈,假设跑三圈。并且我们知道,张三和李四的水平相差不大,短短的三圈不足以让他们拉快过长的距离,更不可能出现套圈。
假设这个跑道长256米,从起点开始沿逆时针方向(即跑步的方向)标注坐标。那么A和B在坐标轴的位置大致如下:
假设A为10,B为240,A 《 B,但是从跑道的图中大家不假思索就得出A跑在前面。这是为什么呢?
大家在判断谁在前面时,其实根本没去管那根蓝色的线(起点或终点)。因为跑道首尾相连,而且张三和李四要跑好几圈,必将多次经过起终点,所以起终点没有任何判断价值。
人脑是怎么判断的
笔者反复自我剖析,觉得可能是这样判断的:
人脑会做两种假设,张三(A)快,或者李四(B)快。最终选择一个最合理的假设。
假设张三(A)快,那么A沿顺时针跑回B(逆时针是前进方向,往回跑就是顺时针)的距离即为A超前B的距离,如下图的红色箭头,相对于一圈的长度而言是一个较小的距离。假设李四(B)快,则B沿顺时针方向需要跑大半圈才能遇到张三(A)。如果李四确实比张三快的话,那么快了不只一点点,而是超前大半圈。先前说过,张三和李四的水平相差不大,短短的三圈不足以让他们拉快过长的距离。所以我们更愿意相信第一种假设成立,即张三(A)比李四(B)跑的快。
人脑做上述判断的时候,并没有给跑道建立坐标系,也不是判断张三和李四的坐标值哪个大,而是判断张三和李四的距离。这个距离是有方向性的。
假设张三(A)快,则目测A跑回B的距离L(A-B)。这个距离比较小,所以判断成立,A确实在B前面。
假设李四(B)快,则目测B跑回A的距离L(B-A)。这个距离比较大,所以判断不成立,B其实在A的后面。
其实根本不需要验证两种假设,只需要验证一个就行了,因为它们是对立的。
回归代码
人脑通过视觉来估测张三与李四的距离,但是计算机不行,它需要一个明确的方法,还是需要坐标系的。
还是假设这个跑道长256米,从起点开始沿逆时针方向(即跑步的方向)标注坐标。
简单情况
先看简单的情况,即A和B在起点的同侧。对应到坐标系上为:
A在40米处(记为Xa),B在20米处(记为Xb)。A返回到B的距离为
L = Xa - Xb = 40 - 20 = 20
这个距离远小于256,所以A在B的前面。
溢出情况
再来看看复杂的溢出情况,即A和B在起点两侧。
对应在坐标系上时,为方便绘制,将A、B与起终点的距离拉远一点。Xa=30,Xb=220。A返回到B的距离为:
L = L1 + L2 = (Xa - 0) + (256 - Xb) = 30 + (256 - 220) = 66
66也是远小于256的,所以A还是在B的前面。
归一
有没有发现什么不对?刚才讨论区分简单情况和溢出情况,在计算L时的公式是不同的,这可有点小麻烦。如果有统一的公式就好了。
让我们再看一眼溢出情况的公式:
L = L1 + L2
= (Xa - 0) + (256 - Xb)
= Xa - Xb + 256 = Xa - Xb
这么一调整就和简单情况一样了吧。为什么把加256给去掉了?因为我们讨论Xa和Xb是uint8_t,加256和没加是一样的。
验证
还是上一个例子的场景,我们来假设B在A前面。B返回到A的距离为:
L = Xb - Xa = 220 - 30 = 190
190比较接近256,所以假设不成立,B并不在A前面,而是A在B前面。
我们在判断距离时,用了两种标准:
L远小于256
L比较接近256
对于计算机而言,这是无法实现的,它需要一个明确的标准。那是什么呢?就一刀切吧,以256 / 2为阈值。
L 《 256 / 2:假设成立
L 》 256 / 2:假设失败至于L == 256 / 2的情况,随便归入哪个都行。
再看时间判断
void comm_delay(uint32_t ms)
{
uint32_t timeout = comm_get_ms() + ms;
while(comm_get_ms() - timeout 》 UINT32_MAX / 2);
}
再看这时间判断,有没有豁然开朗呢?comm_get_ms()是张三,timeout是李四,变量范围由uint8_t变成了uint32_t,仅此而已。
后记
这种超时判断方法并非由笔者想出,是笔者在阅读RT-Thead操作系统的timer源码时发现的。rt_timer是RT-Thread的定时器模块,提供基于系统滴答计数的定时功能,其计数值就是32位无符号整型uint32_t,时间久了必然溢出。
笔者之前也为溢出问题感到头疼,而RT-Thread号称不惧溢出,于是笔者怀着好奇的心态挖掘了其解决方法。在rt_timer中,有多处这样的判断,现在看起来是不是感觉很亲切呢?
/*
* It supposes that the new tick shall less than the half duration of
* tick max.
*/if ((current_tick - t-》timeout_tick) 《 RT_TICK_MAX / 2)
编辑:jq
-
延时
+关注
关注
0文章
107浏览量
25507
原文标题:从rtthread timer模块中找到裸机定时问题的解决方案
文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论