精妙的单片机非阻塞延时程序设计


来源:http://blog.chinaunix.net/uid-29673749-id-4425603.html


  对于每个单片机爱好者及工程开发设计人员,在刚接触单片机的那最初的青葱岁月里,都有过点亮跑马灯的经历。从看到那一排排小灯按着我们的想法在跳动时激动心情。到随着经验越多,越来又会感觉到这个小灯是个好东西,尤其是在调试资源有限的环境中,有时会帮上大忙。


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


  1. while(1)

  2. {

  3.     LED =OFF;

  4.     Delay_ms(500);

  5.     LED = ON;

  6.     Delay_ms(500);

  7. }

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

这时我们就会慢慢意识到,第一种(阻塞延时)方法效率很低,让芯片在那儿空运行几百毫米,什么也不做,真是莫大的浪费,尤其在芯片频率较高,任务又很多时,这样做就像在平坦宽阔的高速公路上挖了一大坑,出现事故可想而知。

但一个单片机中的定时器毕竟有限,如果我需要几十个或者更多不同时间的定时中断,每一个时间到都完成不同的处理动作,如何去做呢。一般我们会想到在一个定时中断函数中再定义static 变量继续定时,到了所需时间,做不同的动作。而这样又会导致在一个中断里做了很多不同的事情,会抢占主轮询更多时间,有时甚至喧宾夺主,并也不是很如的思维逻辑。

那么有没有更好的方法来实现呢,答案是肯定的。下面介绍我在一个项目中偶遇,一个精妙设计的非阻塞定时延时软件的设计(此设计主要针对于无操作系统的裸机程序)。

在上篇文章中有对systick的介绍,比如我要设置其10ms中断一次,如何实现呢?
也很简单,只需调用 core_cm3.h文件中 SysTick_Config 函数 ,当系统时钟为72MHZ,则设置成如下即可 SysTick_Config(720000 ); (递减计数720000次后中断一次) 。此时SysTick_Handler中断函数就会10ms进入一次;

任务定时用软件是如何设计的呢 ? 
且先看其数据结构,这也是精妙所在之处,在此作自顶向下的介绍:

其定义结构体类型如:

  1. typedef struct

  2. {

  3.     uint8_t Tick10Msec;

  4.     Char_Field Status;

  5. } Timer_Struct;

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

  1. typedef union

  2. {

  3.     unsigned char byte;

  4.     Timer_Bit field;

  5. } Char_Field

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

  1. typedef struct

  2. {

  3.     unsigned char bit0: 1;

  4.     unsigned char bit1: 1;

  5.     unsigned char bit2: 1;

  6.     unsigned char bit3: 1;

  7.     unsigned char bit4: 1;

  8.     unsigned char bit5: 1;

  9.     unsigned char bit6: 1;

  10.     unsigned char bit7: 1;

  11. } Timer_Bit

此联合体的这样设计的目的将在后面的代码中体现出来。
如此结构体的设计就完成了。

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

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

  1. #define bSystem10Msec        gTimer.Status.field.bit0

  2. #define bSystem50Msec        gTimer.Status.field.bit1

  3. #define bSystem100Msec       gTimer.Status.field.bit2

  4. #define bSystem1Sec          gTimer.Status.field.bit3

  5. #define bTemp10Msec          gTimer.Status.field.bit4

  6. #define bTemp50Msec          gTimer.Status.field.bit5

  7. #define bTemp100Msec         gTimer.Status.field.bit6

  8. #define bTemp1Sec            gTimer.Status.field.bit

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

  1. typedef enum

  2. {

  3.     TIMER_RESET = 0,

  4.     TIMER_SET = 1,

  5. } TimerStatus;

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

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

  1. void SysTick_Handler(void)

  2. {

  3.          

  4.         bTemp10Msec = TIMER_SET;

  5.         

  6.         ++gTimer.Tick10Msec;

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

  8.         {

  9.             bTemp50Msec = TIMER_SET;

  10.         }

  11.         

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

  13.         {

  14.             bTemp100Msec = TIMER_SET;

  15.         }

  16.         

  17.         if (100 == gTimer.Tick10Msec)

  18.         {

  19.             gTimer.Tick10Msec = 0;

  20.             bTemp1Sec = TIMER_SET;

  21.         }

  22. }

而这又有什么用呢 ?

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

  1. void SysTimer _Process(void)

    就仅对单片机需要实现的功能来说,如果你连功能都实现不了,那我还要你做什么--那只是基础工作。其实,往往上下游提供的环境对你的设计就是一种考验,也有你需要变换N种方案才能比较好的实现的可能。很明显,这样的上下游环境,对你就是一种很好的资源,不过有些人可能就会视而不见,让你更加熟悉自己开发的东西,在这个行业中的使用和价值情况,对自己的今后发展都是一个很好的参考。不像C、C++的人员到了没有发挥平台的时候,就很郁闷了。相反,对单片机来说,我们可以为自己建立那样的可以供自己发挥和发展的平台。

  2. {

  3.     gTimer.Status.byte &= 0xF0;

  4.     

  5.     if (bTemp10Msec)

  6.     {

  7.         bSystem10Msec = TIMER_SET;

  8.     }

  9.     

  10.     if (bTemp50Msec)

  11.     {

  12.         bSystem50Msec = TIMER_SET;

  13.     }

  14.     

  15.     if (bTemp100Msec)

  16.     {

  17.         bSystem100Msec = TIMER_SET;

  18.     }

  19.     

  20.     if (bTemp1Sec)

  21.     {

  22.         bSystem1Sec = TIMER_SET;

  23.     }

  24.     

  25.     gTimer.Status.byte &= 0x0F;

  26. }

此函数开头与结尾两句

  1. gTimer.Status.byte &= 0xF0;

  2. gTimer.Status.byte &= 0x0F

就分别巧妙的实现了bSystemXXX (低4位) 和 bTempXXX(高4位)的清零工作,不用再等定时到达后还需手动把计数值清零。此处清零工作用到了联合体中的变量共用一个起始存储空间的特性。

但要保证while(1)轮询时间要远小于10ms,否则将导致定时延时不准确。这样,在每轮询一次,就先把bSystemXXX ,再根据bTempXXX判断是否时间到达,并把对应的bSystemXXX 置1,而后面所有的任务就都可以通过bSystemXXX 来进行定时延时,在最后函数退出时,又会把bTempXXX清零,为下一次时间到达后查询判断作好了准备。

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

  1. void Task_A_Processing(void)

  2. {

  3.     if(TIMER_SET == bSystem50Msec){

  4.         //do something

  5.     }

  6. }

  7. void Task_B_Processing(void)

  8. {

  9.     if(TIMER_SET == bSystem100Msec){

  10.         //do something

  11.     }

  12. }

  13. void Task_C_Processing(void)

  14. {

  15.     static uint8_t ticks = 0;

  16.     if(TIMER_SET == bSystem100Msec){

  17.        ticks ++ ;

  18.     }

  19.     if(== ticks){

  20.         ticks = 0;

  21.          //do something

  22.     }

  23. }

  24. void Task_D_Processing(void)

  25. {

  26.     if(TIMER_SET == bSystem1Sec){

  27.         //do something

  28.     }

  29. }

以上示例四个任务进程,

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

  1. int main(void)

  2. {

  3.     while(1)

  4.     {

  5.         SysTimer _Process();

  6.         Task_A_Processing();

  7.         Task_B_Processing();

  8.         Task_C_Processing();

  9.         Task_D_Processing();

  10.     }

  11. }

这样,就可以轻松且清晰实现了多个任务,不同时间内处理不同事件。(但注意,每个任务处理中不要有阻塞延时,也不要处理过多的事情,以致处理时间较长。可设计成状态机来处理不同任务。)

单片机

每天更新
单片机
各种知识,电子制作DIY,及
电子行业
最新资讯,关注我们,棒棒哒!

在更复杂的系统中,经常使用分布式多计算机系统。多机系统通常由许多具有不同功能的单片机组成,每个单片机执行特定的任务,并且它们通过串行通信相互通信和协调。在这种系统中,单片机通常用作终端机,并安装在系统的某些节点上,以对现场信息进行实时测量和控制。MCU的高可靠性和强大的抗干扰能力使其可以在恶劣环境的前端工作。

生成海报
点赞 0

Abin

我还没有学会写个人说明!

相关推荐

基于8051单片机实现电子时钟+数字秒表设计

概述 电子时钟是一种利用数字电路来显示秒、分、时的计时装置,与传统的机械钟相比,它具有走时准确、显 示直观、无机械传动装置等优点,因而得到广泛应用。随着人们生活环境的不断改善和美化,在许

stm32进不去串口中断的问题

今天遇到了特别棘手的问题,程序进入串口中断 原本应该进入这段程序: void USART1_IRQHandler(void) {if(USART_GetITStatus(USART1,USART_IT_RXNE)

立创EDA从零开始教学

结合自己为公司设计的产品的经验上,推出一套立创EDA的硬件教学设计课程。或者说是一系列文章。# 系列文章目录第一章 前言和软件介绍# 前言 大家可以叫我小刘,我大学是自动化专业,在大二开始接触单片机&#

2022寒假自学蓝桥杯嵌入式

2022寒假自学蓝桥杯嵌入式 欢近似从0开始准备2022年4月份省赛 学习记录分享 2022/1/22晚 硬件部分的认识(国信长天) 液晶屏,芯片在液晶屏下 电源选择跳线 下载调试文件接口