定时任务的溢出问题

引子

先看一段代码:

static void led_loop(void)
{
    static uint32_t next_tick = 0;

    if(HAL_GetTick() >= next_tick)
    {
        next_tick = HAL_GetTick() + 100;
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

int main(void)
{
    // 此处省略一万字

    while (1)
    {
        // other loop
		
        led_loop();
    }
}

这是stm32裸机平台的LED闪烁代码,每100毫秒翻转一次LED电平。笔者用它来演示定时任务的实现,即每隔一段时间执行一个任务。

问题

这段代码有一个小问题,不知各位同学有没有发现。如果没有,那你看这篇文章就看对了,哈哈。

HAL_GetTick返回的是从系统启动到现在所经过的时间uwTick,单位是1毫秒。uwTick是32位无符号整型变量,它最大能记录多少天的时间呢?

2^32 / 1000 / 3600 / 24 = 49.71.....

uwTick在系统一直运行49天后将溢出,溢出后重新从0开始累加。在即将溢出时,上面这个定时程序将出现问题。为了方便描述,要使用到一个宏UINT32_MAX。UINT32_MAX表示32位无符号整型变量的最大值,即0xffffffff。

时间溢出会引发两种问题:

  1. 如果HAL_GetTick()溢出而next_tick尚未溢出,则长时间内HAL_GetTick()<next_tick,定时任务“永”不执行。为什么HAL_GetTick()会先溢出呢?比如next_tick为UINT32_MAX。当HAL_GetTick()为UINT32_MAX-1时调用了一次led_loop,此时HAL_GetTick()<next_tick,不执行任务。而之后执行其他任务时,花费了一些时间,当再调用led_loop时,经过了2ms的时间。此时HAL_GetTick()为UINT32_MAX - 1 + 2 = UINT32_MAX + 1 = 0,发生了溢出。
  2. 如果HAL_GetTick()尚未溢出而next_tick溢出,则在HAL_GetTick()溢出之前,HAL_GetTick()>next_tick,定时任务将不停执行。

问题1不是必现的,如果led_loop每毫秒都执行一次,就没问题。问题2是必现的,next_tick不断累加,49天后必然溢出。

解决

49天是一个并不算短的时间,对于一个一直运转的设备来说,溢出都是必然的。所以我们得想办法解决这个问题。

修改如下:

static void led_loop(void)
{
    static uint32_t next_tick = 0;

    if(HAL_GetTick() - next_tick < UINT32_MAX / 2)
    {
        next_tick = HAL_GetTick() + 100;
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

只是把判断条件改了下

if(HAL_GetTick() - next_tick < UINT32_MAX / 2)

HAL_GetTick()与next_tick的差值与UINT32_MAX / 2有什么关系呢?我们知道,它的功能就是if (HAL_GetTick() >= next_tick),只不过它不怕时间溢出,无论是HAL_GetTick()还是next_tick。我们先用几个具体的数字,来验证一下这个功能。

在举例前,需要明确一点。HAL_GetTick()和next_tick都是无符号的,无符号减无符号的结果还是无符号,uint32减uint32还是uint32。如果(int32_t)HAL_GetTick() - (int32_t)next_tick = -1,则HAL_GetTick() - next_tick = UINT32_MAX。int32类型的负数对应的uint32的正数有如下的关系:

int32_t类型的变量x为负数,(uint32_t)x = UINT32_MAX + 1 - |x|。

  1. HAL_GetTick() = 1, next_tick = 2, HAL_GetTick() - next_tick = 1 - 2 = -1 = UINT32_MAX > UINT32_MAX / 2,if判断不成立。
  2. HAL_GetTick() = 2, next_tick = 1, HAL_GetTick() - next_tick = 2 - 1 = 1 < UINT32_MAX / 2,if判断成立。
  3. HAL_GetTick() = UINT32_MAX, next_tick = 1(next_tick溢出,所以实际上当时时间HAL_GetTick()还没达到超时时间next_tick ), HAL_GetTick() - next_tick = UINT32_MAX - 1 > UINT32_MAX / 2, if判断不成立。
  4. HAL_GetTick() = 1, next_tick = UINT32_MAX(这描述的场景是,HAL_GetTick()原先是接近UINT32_MAX的,只不过由于检查的间隔过长,再次检查时HAL_GetTick()溢出了,),HAL_GetTick() - next_tick = 1 - UINT32_MAX = UINT32_MAX + 1 - |1 - UINT32_MAX| = 2 < UINT32_MAX / 2, if判断成立。

例1和例2是普通场景,其结果与if (HAL_GetTick() >= next_tick)一致。例3和例4是我们之前所讨论的两种时间溢出场景,例3是next_tick溢出,例4是HAL_GetTick()溢出。例3和例4的判断结果也满足实际的情况,比如例3中,next_tick溢出,HAL_GetTick()尚未溢出,所以尚未满足超时条件,例3的结果也是if判断不成立。

原理

if ((HAL_GetTick() - next_tick) < UINT32_MAX / 2)为什么能实现超时判断而又不怕溢出呢?

原理还是在无符号整型的减法运算上面。之前提到无符号减无符号的结果还是无符号,即还是正数(或0),并且提了正负数转换的规律。有的同学可能看的云里雾里,那么现在,笔者将告诉你两个无符号数相减的终极含义:两个数的距离。准确说,是被减数在坐标轴上向左走到减数所经过的距离。下面画图说明B-A的两种情况:

在这里插入图片描述

  1. B1 > A1,B1向左直接能走到A1。
  2. B2 < A2,B2向左走到A2得分为三个步骤:B2向左走到0,从0跳到UINT32_MAX(算走1步),从UINT32_MAX走到A2。

现在再想想“解决”那节里面的例4,HAL_GetTick() = 1, next_tick = UINT32_MAX,从HAL_GetTick()走到next_tick的距离就是1 + 1 + 0 = 2

将A和B替换为代表时间的next_tick和HAL_GetTick(),则向右代表未来,而向左代表过去。HAL_GetTick() - next_tick就是HAL_GetTick()向过去走到next_tick的距离。由前面B-A的叙述可知,无论HAL_GetTick()是不是在next_tick前面(前面表未来),HAL_GetTick()向左总能走到next_tick,因为这是一个环路,从0向左会走到UINT32_MAX。

我们可以通过比较HAL_GetTick() - next_tick这个距离的大小,从而判断HAL_GetTick()是否能从过去走到next_tick。

我们细想下实际的使用场景的特点:

  • 规划的肯定是未来的任务,而不是过去的任务。
  • 会定期去检查有没有超时,检测的间隔不会很长,比如每次时间更新时都检查。

基于如上特点,我们可以得出结论:如果检查超时的频率比较高,当检查到HAL_GetTick()超过next_tick时,HAL_GetTick()不会超太多。可能超了1,10,100,反正是一个较小的值。进一步推论得出:若HAL_GetTick()确实在next_tick前面的话,那么HAL_GetTick()往回走到next_tick的距离不会太长。如果HAL_GetTick()要走很长的距离才能到达next_tick,那它到达的应该是未来的next_tick,HAL_GetTick()自己穿越时空了。什么样的距离算很长的距离呢,uint32的最大值为UINT32_MAX,那就以一半为界,因此就有了之前看到的判断式:

if ((HAL_GetTick() - next_tick) < UINT32_MAX / 2)

再重复叙述下:

  1. 如果HAL_GetTick()向左走较短的距离(小于UINT32_MAX / 2)就到了next_tick,说明HAL_GetTick()往过去走能达到next_tick,从而判断HAL_GetTick()比next_tick更前,即达到了超时条件。
  2. 如果HAL_GetTick()得走很长距离(大于等于UINT32_MAX / 2)才能走到next_tick,说明HAL_GetTick()往过去走不到next_tick,它走到了未来,从而判断next_tick比HAL_GetTick()更前,即尚未超时。

最后,让我们再用图来演示下4种场景:

为作图方便,用current表示HAL_GetTick()即当前时间,timeout表示next_tick即下次任务的时间。

这里用<<表示远小于,比如current << timeout表示:current < timeout - UINT32_MAX / 2。同理,用>>表示远大于。

  1. current< timeout,两者均未溢出,即未超时。
    在这里插入图片描述
  2. current > timeout,两者均未溢出,即已超时。
    在这里插入图片描述
  3. current << timeout,current溢出,即已超时。
    在这里插入图片描述
  4. current >> timeout, timeout溢出,即未超时。
    在这里插入图片描述

质疑

最后再让我们看一眼这个判断式,它是万能的吗,有没有限制条件呢?

if ((HAL_GetTick() - next_tick) < UINT32_MAX / 2)

其实是有的,那就是定时任务的周期必须小于UINT32_MAX / 2,否则就会得出相反的判断结果:

  • 实际未超时时认为超时。
  • 实际超时时认为尚未超时。

前面提到,UINT32_MAX对应的是49天,所以UINT32_MAX / 2是24天。长周期的定时任务,比如以周、月、季度、年为周期,本就不适合用基于Tick的机制,因为:

  1. UINT32_MAX也就49天,极限状态下也不满足季度周期。
  2. 月、季、年的天数是会变的,2月可能有28天也可能有29天,其他月有30天和31天。

对于长周期的定时任务,可以使用RTC模块,或者使用timer.h中的相关函数。

  • 18
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值