剖析毫秒级延时防溢出的原理

描述

前文指出了基于系统滴答计数实现的毫秒级延时的问题。

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

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分