阻塞延时
阻塞延时是当任务进入延时后,该任务的CPU使用权被剥夺进入阻塞状态(阻塞状态可以理解为保持状态不变,ps:惯性)。此时CPU可以进行其他任务的调度等,这样一来大大提升了CPU的使用效率。
而当所有任务都进入阻塞状态时,此时CPU就调度空闲任务执行。
阻塞延时
阻塞延时和普通CPU延时不同,普通CPU延时是CPU进行设置好的时长等待,此时下面要进行的所有任务执行都将一并延时,直到延时结束,CPU再继续执行,这样子的一个系统称不上一个实时操作系统,因为他无法很好实现“实时”效果;而阻塞延时的核心,就是当上一个任务进入延时后,CPU就“放开”对其的控制,让其自己保持状态不变,CPU进而对下面的指令进行执行,在rtos中,这种方式使得延时对于任务的切换几乎没有影响,进而实现“实时”效果。
我们定义阻塞延时TaskDelay,它将完成两件事情:1. 设置延时时长; 2. 切换任务
1. 设置延时时长
首先要明白,阻塞延时的时长如何实现? 是通过SysTick中断进行计数值的定时递减!
首先先确定系统的时钟源频率是多少,知道了频率我们便知道系统SYS一秒数多少下,那么我们设置给他数一定的次数中断一次,就可以控制SYS每多长时间中断一次,然后,我们设置一个值-任务的延时计数值,当SYS中断一次,该值递减一次,直到值为0,则进行任务阻塞状态结束,这样一来,就可以实现控制任务阻塞时间。
SysTick中断服务函数为SysTick_Handler,我们宏定义为xPortSysTickHandler,当产生SysTick中断时进入这个函数。按照上面的理论,我们知道,首先要确定SYS一秒数多少下,要数多少下中断一次,数完是否真的要产生中断(所以你确实可以选择数完就数完,啥事没有)等,对SysTick进行初始化,而初始化其实就是通过两个寄存器进行这几个问题进行确定,如图
图1 两个寄存器的位段说明
根据图片,可以写代码如下,初始化函数为vPortSetupTimerInterrupt
#define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *)0xe000e010)) //SYS控制及状态寄存器 #define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *)0xe000e014)) //重装载值,数完后中断一次 #ifndef configSYSTICK_CLOCK_HZ //SYSTICK时钟频率 #define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ //假如定为系统时钟↓ #define portNVIC_SYSTICK_CLK_BIT (1UL << 2UL) //控制寄存器第2位段-选择时钟源:1为内核时钟 #else #define portNVIC_SYSTICK_CLK_BIT (0) //控制寄存器第2位段-选择时钟源:0为外部时钟 #endif #define portNVIC_SYSTICK_INT_BIT (1UL << 1UL) //控制寄存器第1位段-SYS倒数到0时是否产生异常 #define portNVIC_SYSTICK_ENABLE_BIT (1UL << 0UL) //控制寄存器第0位段-SYS使能 void vPortSetupTimerInterrupt(void) { /* 初始化SYSTICK,即初始化两个寄存器 */ portNVIC_SYSTICK_LOAD_REG = (configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ) - 1UL; portNVIC_SYSTICK_CTRL_REG = (portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT); }
确定完SysTick的一些参数后,我们就可以定义一下,产生中断后,我们要做些什么,同样由上面理论知,我们需要设置,中断发生时,任务延时计数值减1,并判断这个值是否为0,如果为0,表示任务阻塞状态结束,则切换到这个任务。所以代码如下
void xPortSysTickHandler(void) { /* SysTick中断服务函数 */ vPortRaiseBASEPRI(); //进临界区 xTaskIncrementTick(); //进入计数器切换 vPortClearBASEPRIFromISR(); //出临界区 } void xTaskIncrementTick(void) { /* 进入计数器递减,并在每一次递减后判断是否有超时任务,进行切换 */ TCB_t *pxTCB = NULL; BaseType_t i = 0; /*注释码段暂时用不到*/ //extern TickType_t xTickCount; //const TickType_t xConstTickCount = xTickCount + 1; //xTickCount = xConstTickCount; /* 扫描就绪列表中所有优先级链表的第一个任务的xTicksToDelay,如不为0,则减1 */ for(i=0;i<configMAX_PRIORITIES;i++) { pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY((&pxReadyTasksLists[i])); if(pxTCB->xTicksToDelay > 0) { pxTCB->xTicksToDelay--; } } portYIELD(); //任务切换 }
通过上面我们知道了给定某个任务他自己的一个任务延时计数值,就可以让它进行我们想要的延时,那么剩下的就是这个任务延时计数值应该怎么赋予任务?什么时候赋予?
第一个问题简单,和任务相关的,一律被我们集合在任务的TCB中,于是在原有的TCB结构中,加入任务延时计数值xTicksToDelay
typedef struct tasTaskControlerBlock { volatile StackType_t *pxTopOfStack; //栈顶 ListItem_t xStateListItem; //任务节点(链表项) StackType_t *pxStack; //任务栈起始地址 char pcTaskName[configMAX_TASK_NAME_LEN]; //任务名称 TickType_t xTicksToDelay; //任务延时计数值 }tskTCB; typedef tskTCB TCB_t;
第二个问题,文章的一开头就已经说明了,阻塞延时函数TaskDelay的第一件事:设置延时
void vTaskDelay(const TickType_t xTicksToDelay) { TCB_t *pxTCB = NULL; //设置当前任务的延时计数 pxTCB = pxCurrentTCB; pxTCB->xTicksToDelay = xTicksToDelay; //任务切换 taskYIELD(); }
2. 切换任务
由上面的vTaskDelay函数的代码可知,第二件事就是切换任务,通过前面文章的调度器的实现(https://www.cnblogs.com/toriyung/p/16837371.html)我们知道,任务切换是通过触发PendSV中断,进入PendSVHandler中断服务函数,里面有着任务切换函数vTaskSwitchContext,但之前的文章并没有涉及延时,所以需要加入对任务当下延时计数值(xTicksToDelay)的判断
void vTaskSwitchContext(void) { /* 上下文切换(任务切换)*/ if(pxCurrentTCB == &IdleTaskTCB) //当前任务为空闲任务 { if(Task1TCB.xTicksToDelay == 0) //Task1延迟结束,切换为Task1 { pxCurrentTCB =&Task1TCB; } else if(Task2TCB.xTicksToDelay == 0) //Task2延迟结束,切换为Task2 { pxCurrentTCB =&Task2TCB; } else //Task1和Task2都在延迟,不进行切换 { return; } } else { if(pxCurrentTCB == &Task1TCB) //当前任务为Task1 { if(Task2TCB.xTicksToDelay == 0) //Task2延迟结束,切换为Task2 { pxCurrentTCB =&Task2TCB; } else if(pxCurrentTCB->xTicksToDelay != 0) //Task1和Task2都在延迟,切换为空闲任务 { pxCurrentTCB = &IdleTaskTCB; } else { return; //Task1没有延迟,保持Task1 } } if(pxCurrentTCB == &Task2TCB) //当前任务为Task2 { if(Task1TCB.xTicksToDelay == 0) //Task1延迟结束,切换为Task1 { pxCurrentTCB =&Task1TCB; } else if(pxCurrentTCB->xTicksToDelay != 0) //Task1和Task2都在延迟,切换为空闲任务 { pxCurrentTCB = &IdleTaskTCB; } else { return; //Task2没有延迟,保持Task2 } } } }
至此,阻塞延时的实现搞定!
结构框图
图2 结构框图
仿真效果
图3 普通延迟
图4 阻塞延迟
可以看到,普通延迟的情况下,当任务1拉高电平进入延迟和拉低电平进入延迟期间,CPU根本没有空出手来去调度任务2,所以任务2一直保持低电平不变,直到任务1第二次延迟结束,CPU才进行任务2的调度;而阻塞延迟的情况下,当任务1拉高电平后立马进入阻塞状态,CPU停止对任务1控制,立马调度任务2,所以任务2以十分快的速度追上任务1的步伐拉高电平,肉眼看起来几乎是同步的。