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

  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)
  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. }
这样,就可以轻松且清晰实现了多个任务,不同时间内处理不同事件。(但注意,每个任务处理中不要有阻塞延时,也不要处理过多的事情,以致处理时间较长。可设计成状态机来处理不同任务。)
### 单片机实现非阻塞延时的方法 在单片机开发中,非阻塞延时是一种高效的编程方式,它允许系统在等待某个事件发生的同时还能执行其他任务。这种方法通常通过硬件定时器配合中断机制来实现。 #### 使用硬件定时器和中断的非阻塞延时 一种常见的做法是利用硬件定时器触发周期性中断,在中断服务程序中更新状态变量或标志位。以下是具体实现方法: 1. **初始化定时器** 配置一个硬件定时器(如TIM3),使其按照设定的时间间隔触发中断。例如,设置定时器每10毫秒触发一次中断[^2]。 2. **编写中断服务程序** 在中断服务程序中,可以通过修改全局变量或其他标志位来记录当前的状态变化。以下是一个简单的示例代码: ```c // 定义全局计数器 volatile uint32_t globalCounter = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 判断是否为TIM3中断 globalCounter++; // 增加计数器值 } } ``` 在此基础上,可以根据`globalCounter`的值判断是否达到所需的延迟时间。例如,如果希望实现500毫秒的延时,则可以检测`globalCounter >= 50`。 --- #### 软件定时器的设计 另一种更灵活的方式是基于软件定时器的设计。这种设计的核心在于维护一组定时任务的数据结构,并定期检查这些任务是否到期。以下是一个典型的例子: 1. **定义数据结构** 创建一个结构体数组,用于存储各个定时任务的信息。例如: ```c typedef struct { uint32_t tickStart; // 记录任务启动时刻 uint32_t durationMs; // 设置的任务持续时间(单位:毫秒) bool isRunning; // 表示任务是否正在运行 } TimerTask; ``` 2. **初始化任务列表** 初始化多个定时任务并将其加入到任务队列中。例如: ```c #define MAX_TASKS 10 TimerTask tasks[MAX_TASKS]; void initTasks() { for (int i = 0; i < MAX_TASKS; ++i) { tasks[i].isRunning = false; } } void startTask(int index, uint32_t durationMs) { if (!tasks[index].isRunning && index < MAX_TASKS) { tasks[index].tickStart = globalCounter; // 当前计数值作为起始点 tasks[index].durationMs = durationMs / 10; // 将毫秒转换为10ms刻度 tasks[index].isRunning = true; } } ``` 3. **轮询任务状态** 在主循环中定期检查每个任务的状态,判断是否有任务已经超时。例如: ```c void checkTasks() { for (int i = 0; i < MAX_TASKS; ++i) { if (tasks[i].isRunning) { if ((globalCounter - tasks[i].tickStart) >= tasks[i].durationMs) { tasks[i].isRunning = false; // 结束该任务 handleTaskCompletion(i); // 处理任务完成后的逻辑 } } } } ``` 4. **处理任务完成的动作** 编写函数`handleTaskCompletion()`,用来指定当某项任务完成后应采取的具体行动。例如点亮LED、发送串口消息等。 --- #### DWT寄存器的应用 除了传统的硬件定时器外,还可以使用ARM Cortex-M系列微控制器内置的DWT(Data Watchpoint and Trace)模块来获取高精度的时间戳。这种方式无需额外配置定时器资源,适合轻量级场景。 1. **启用DWT功能** 启用DWT和CYCCNT寄存器以访问CPU核心时钟计数器: ```c #include "core_cm3.h" void enableDWT() { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 开启跟踪单元 DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 启用CYCCNT计数器 } ``` 2. **计算延迟时间** 使用`DWT->CYCCNT`读取当前的CPU时钟计数值,并据此推算经过的时间。例如: ```c uint32_t startTime = DWT->CYCCNT; while (((DWT->CYCCNT - startTime) * 1000) / SystemCoreClock < 500) { // 循环直到超过500毫秒 } ``` 注意:此方法依赖于精确的系统时钟频率(SystemCoreClock),因此需确保其值被正确定义。 --- ### 总结 以上介绍了三种实现单片机非阻塞延时的主要方法: - 硬件定时器结合中断; - 软件定时器管理多任务; - ARM Cortex-M平台下DWT寄存器的高效运用。 无论采用哪种方案,都需要合理规划系统架构以及优化资源分配,从而最大化性能表现。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值