传人记智能技术论坛

 找回密码
 立即注册
查看: 7527|回复: 0

[资料分享] 精妙的单片机非阻塞延时程序设计

[复制链接]

71

主题

72

帖子

897

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
897
发表于 2023-2-2 10:45:05 | 显示全部楼层 |阅读模式

对于每个单片机爱好者及工程开发设计人员,在刚接触单片机的那最初的青葱岁月里,都有过点

亮跑马灯的经历。从看到那一排排小灯按着我们的想法在跳动时激动心情。到随着经验越多,越

来又会感觉到这个小灯是个好东西,尤其是在调试资源有限的环境中,有时会帮上大忙。


但对于绝大多数人,我们在最最初让灯闪烁起来时大约都会用到阻塞延时实现,会像如下代码的样子:

while(1)

{

      LED =OFF;

      Delay_ms(500);

      LED = ON;

      Delay_ms(500);

}

然后,在我们接触到定时器,我们会发现,原来用定时中断来处理会更好。比如我们可以500ms中
断一次,让灯亮或灭,其余的时间系统还可以做非常之多的事情,效率一下提升了很多。

这时我们就会慢慢意识到,第一种(阻塞延时)方法效率很低,让芯片在那儿空运行几百毫米,什么

也不做,真是莫大的浪费,尤其在芯片频率较高,任务又很多时,这样做就像在平坦宽阔的高速公

路上挖了一大坑,出现事故可想而知。


但一个单片机中的定时器毕竟有限,如果我需要几十个或者更多不同时间的定时中断,每一个时间

到都完成不同的处理动作,如何去做呢。一般我们会想到在一个定时中断函数中再定义static 变量

继续定时,到了所需时间,做不同的动作。而这样又会导致在一个中断里做了很多不同的事情,会

抢占主轮询更多时间,有时甚至喧宾夺主,并也不是很如的思维逻辑。


那么有没有更好的方法来实现呢,答案是肯定的。下面介绍我在一个项目中偶遇,一个精妙设计的

非阻塞定时延时软件的设计(此设计主要针对于无操作系统的裸机程序)。


比如我要设置systick,其10ms中断一次,如何实现呢?


也很简单,只需调用core_cm3.h文件中 SysTick_Config函数 ,当系统时钟为72MHZ,则设置成如

下即可SysTick_Config(720000); (递减计数720000次后中断一次) 。此时SysTick_Handler中断函数

就会10ms进入一次;


任务定时用软件是如何设计的呢 ?


且先看其数据结构,这也是精妙所在之处,在此作自顶向下的介绍:


其定义结构体类型如:

typedef struct

{

      uint8_t Tick10Msec;

      Char_Field Status;

} Timer_Struct;

其中Char_Field 为一联合体,设计如下:

typedef union

{

      unsigned char byte;

      Timer_Bit field;

} Char_Field

而它内部的Timer_Bit是一个可按位访问的结构体:

typedef struct

{

      unsigned char bit0: 1;

      unsigned char bit1: 1;

      unsigned char bit2: 1;

      unsigned char bit3: 1;

      unsigned char bit4: 1;

      unsigned char bit5: 1;

      unsigned char bit6: 1;

      unsigned char bit7: 1;

} Timer_Bit

此联合体的这样设计的目的将在后面的代码中体现出来。


如此结构体的设计就完成了。


然后我们定义的一全局变量,Timer_Struct gTimer;


并在头文件中宏定义如下:

#define bSystem10Msec        gTimer.Status.field.bit0

#define bSystem50Msec        gTimer.Status.field.bit1

#define bSystem100Msec       gTimer.Status.field.bit2

#define bSystem1Sec            gTimer.Status.field.bit3

#define bTemp10Msec          gTimer.Status.field.bit4

#define bTemp50Msec          gTimer.Status.field.bit5

#define bTemp100Msec         gTimer.Status.field.bit6

#define bTemp1Sec              gTimer.Status.field.bit


另外为了后面程序清晰,再定义一状态指示:

typedef enum

{

      TIMER_RESET = 0,

      TIMER_SET = 1,

} TimerStatus;


至此,准备工作就完成了。下面我们就开始大显神通了!


首先,10ms定时中断处理函数如,可以看出,每到达10ms 将把bTemp10Msec置1,每50ms 将把

bTemp50Msec置1,每100ms 将把bTemp100Msec置1,每1s 将把bTemp1Sec置1,

void SysTick_Handler(void)

{

          bTemp10Msec = TIMER_SET;

          ++gTimer.Tick10Msec;

        if (0 == (gTimer.Tick10Msec % 5))

        {

              bTemp50Msec = TIMER_SET;

        }

        if (0 == (gTimer.Tick10Msec % 10))

        {

              bTemp100Msec = TIMER_SET;

        }

        if (100 == gTimer.Tick10Msec)

        {

              gTimer.Tick10Msec = 0;

              bTemp1Sec = TIMER_SET;

        }

}


而这又有什么用呢 ?


这时,我们需在主轮询while(1)内最开始调用一个定时处理函数如下:

void SysTimer _Process(void)

{

    gTimer.Status.byte &= 0xF0;


    if (bTemp10Msec)

    {

        bSystem10Msec = TIMER_SET;

    }


    if (bTemp50Msec)

    {

        bSystem50Msec = TIMER_SET;

    }


    if (bTemp100Msec)

    {

        bSystem100Msec = TIMER_SET;

    }


    if (bTemp1Sec)

    {

        bSystem1Sec = TIMER_SET;

    }


    gTimer.Status.byte &= 0x0F;

}


此函数开头与结尾两句:

gTimer.Status.byte &= 0xF0;

gTimer.Status.byte &= 0x0F;


就分别巧妙的实现了bSystemXXX (低4位) 和 bTempXXX(高4位)的清零工作,不用再等定时到达

后还需手动把计数值清零。此处清零工作用到了联合体中的变量共用一个起始存储空间的特性。


但要保证while(1)轮询时间要远小于10ms,否则将导致定时延时不准确。这样,在每轮询一次,

就先把bSystemXXX ,再根据bTempXXX判断是否时间到达,并把对应的bSystemXXX 置1,而后

面所有的任务就都可以通过bSystemXXX来进行定时延时,在最后函数退出时,又会把bTempXXX

清零,为下一次时间到达后查询判断作好了准备。


说了这么多,举例说明一下如何应用:

void Task_A_Processing(void)

{

    if(TIMER_SET == bSystem50Msec){

        //do something

    }

}


void Task_B_Processing(void)

{

    if(TIMER_SET == bSystem100Msec){

        //do something

    }

}


void Task_C_Processing(void)

{

    static uint8_t ticks = 0;

    if(TIMER_SET == bSystem100Msec){

       ticks ++ ;

    }


    if(5 == ticks){

        ticks = 0;

         //do something

    }


}


void Task_D_Processing(void)

{

    if(TIMER_SET == bSystem1Sec){

        //do something

    }


}


以上示例四个任务进程,

在主轮询里可进行如下处理:

int main(void)

{

    while(1)

    {

        SysTimer _Process();

        Task_A_Processing();

        Task_B_Processing();

        Task_C_Processing();

        Task_D_Processing();

    }

}

这样,就可以轻松且清晰实现了多个任务,不同时间内处理不同事件。(但注意,每个任务处

理中不要有阻塞延时,也不要处理过多的事情,以致处理时间较长。可设计成状态机来处理不

同任务。)


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|传人记设备云管理系统 ( 浙ICP备18019984号 )

GMT+8, 2025-5-4 18:56 , Processed in 0.081781 second(s), 18 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表