单片机中的非阻塞延时程序设计
摘要:非阻塞延时程序通过替代传统delay()函数,实现单片机高效多任务处理。其核心是使用millis()记录时间差,结合定时中断和状态标志,允许多个任务在不同时间间隔下并行执行。相比阻塞延时会占用CPU空转,非阻塞方案显著提升系统响应效率。关键设计包括:1)利用SysTick定时器产生10ms基准;2)通过位域结构体管理多个定时标志;3)主循环轮询处理各任务状态。要注意轮询周期应短于最小定时单位
非阻塞延时程序是一种在不需要使用delay()函数(该函数会阻塞程序执行)的情况下实现延时功能的方法。这种方法允许单片机在等待一段时间的同时执行其他任务,从而提高程序的效率。通常,我们使用 millis() 函数来记录时间,并通过比较当前时间和上一次记录的时间来判断是否已经达到设定的延时。
一:阻塞式延时
但对于绝大多数人,我们在最初让灯闪烁起来时大约都会用到阻塞延时实现,会像如下代码的样子:
while(1) {
LED =OFF;
Delay_ms(500);
LED = ON;
Delay_ms(500);
}
第一种(阻塞延时)方法效率很低,让芯片在那儿空运行几百毫米,什么也不做,真是莫大的浪费,尤其在芯片频率较高,任务又很多时,这样做就像在平坦宽阔的高速公路上挖了一大坑,出现事故可想而知。
二:定时器延时
然后,在我们接触到定时器,我们会发现,原来用定时中断来处理会更好。比如我们可以500ms中断一次,让灯亮或灭,其余的时间系统还可以做非常之多的事情,效率一下提升了很多。
但定时器毕竟有限,如果我需要几十个或者更多不同时间的定时中断,每一个时间到都完成不同的处理动作,如何去做呢。一般我们会想到在一个定时中断函数中再定义static 变量继续定时,到了所需时间,做不同的动作。而这样又会导致在一个中断里做了很多不同的事情,会抢占主轮询更多时间,有时甚至喧宾夺主。
那么有没有更好的方法来实现呢,答案是肯定的。一个精妙设计的非阻塞定时延时软件的设计(此设计主要针对于无操作系统的裸机程序)。
三:非阻塞式延时设计
只需调用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.bit7
另外为了后面程序清晰,再定义一状态指示:
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();
}
}
这样,就可以轻松且清晰实现了多个任务,不同时间内处理不同事件。(但注意,每个任务处理中不要有阻塞延时,也不要处理过多的事情,以致处理时间较长。可设计成状态机来处理不同任务。
四、非阻塞延时介绍
非阻塞延时是一种编程技术,允许程序在执行延时操作时不阻塞主线程,而是继续执行其他任务。与传统的阻塞延时(如Arduino的delay()函数)不同,非阻塞延时通过记录时间戳和检查时间差来实现延时功能。
优势
-
提高程序响应性
-
允许多个任务并行执行
-
避免程序"卡死"
-
更适合实时应用
注意事项
-
时间溢出处理:当计时器溢出时需要进行特殊处理
-
性能考虑:避免在循环中频繁检查大量任务
-
精度限制:非阻塞延时的精度受系统时钟精度限制
-
资源管理:合理管理任务队列,避免内存泄漏
这种非阻塞延时方法在实时应用程序中非常常见,是实现多任务处理的基础技术之一。
更多推荐



所有评论(0)