前文指出了基于系统滴答计数实现的毫秒级延时的问题。
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)
精彩可期,SR SHOW 2017上海国际服务机器人展专业观众预登记火热进行中!
浅谈微型扬声器UV胶的优势
新品|PM2.5空气检测模块&可穿戴手表套件
日本打造仅使用锂离子电池的“e5”型船
现代全新途胜1.6T全方位测评
剖析毫秒级延时防溢出的原理
区块链技术将使未来的思想互联网成为可能
苹果与高通的专利战将影响苹果公司对于5G应用的进程
本田投资2万亿日元在加拿大建电动汽车厂
买家电,安装还得花钱?选卡萨帝,设计、拆装、局改都免费!
射频功率的测量和控制
OLEDCOMM将SatelLife®LiFi模块整合到了纳米卫星中
Linux设备树关键之一:根节点
紫光同芯大容量高性能安全芯片THD89介绍
奥迪威车载超声波传感器助力自动泊车朝行泊一体高阶迭代
硬核工控安全平台 打造车联网服务系统
电源和接口的EMC设计考虑
为什么联想和华为很多方面不具备可比性?
创纪录!2022年半导体总销售额将增长11%
大数据在冠状病毒中的四种作用