在上一章节中,任务体内的延时使用的是软件延时,即还是让CPU 空等来达到延时的效果。使用RTOS 的很大优势就是榨干CPU 的性能,永远不能让它闲着,任务如果需要延时也就不能再让CPU 空等来实现延时的效果。
1、什么是阻塞延时?
阻塞延时就是当任务需要延时的时候,任务会放弃CPU 的使用权,CPU 可以去干其他的事情,当任务延时时间到,重新获取 CPU 使用权,任务继续运行,这样就充分地利用了CPU 的资源,而不是干等着。
2、什么是空闲任务?
当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?如果没有其他任务可以运行,RTOS 通常会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。在 μC/OS-III 中,空闲任务是系统在初始化的时候创建的优先级最低的任务,空闲任务主体很简单,只是对一个全局变量进行计数。鉴于空闲任务的这种特性,在实际应用中,当系统进入空闲任务的时候,可在空闲任务中让单片机进入休眠或者低功耗等操作。
μC/OS中的两个系统任务分别是空闲任务和统计任务。
3、实现空闲任务
1、定义空闲任务栈
空闲任务栈在 os_cfg_app.c(os_cfg_app.c 第一次使用需要自行在文件夹 μC/OS-IIISource 中新建并添加到工程的μC/OS-III Source 组)文件中定义。
CPU_STK OSCfg_IdleTaskStk [OS_CFG_IDLE_TASK_STK_SIZE];
声明在 os_cfg_app.h 中。
#ifndef OS_CFG_APP_H
#define OS_CFG_APP_H
#define OS_CFG_IDLE_TASK_STK_SIZE 128u
#endif
空闲任务的栈是一个定义好的数组, 大小由OS_CFG_IDLE_TASK_STK_SIZE 这个宏来控制。OS_CFG_IDLE_TASK_STK_SIZE 在os_cfg_app.h 这个头文件定义,大小为128字节。
这里有与普通任务不一样的一点是 空闲任务的栈的起始地址和大小均被定义成一个常量,不能被修改。 在 os_cfg_app.c 文件中进行定义。
CPU_STK * const OSCfg_IdleTaskStkBasePtr = (CPU_STK *)&OSCfg_IdleTaskStk[0];
CPU_STK_SIZE const OSCfg_IdleTaskStkSize = (CPU_STK_SIZE)OS_CFG_IDLE_TASK_STK_SIZE;
此外,变量 OSCfg_IdleTaskStkBasePtr 和 OSCfg_IdleTaskStkSize 同时还在 os.h 中声明,这样就具有全局属性,可以在其他文件里面被使用。
extern CPU_STK * const OSCfg_IdleTaskStkBasePtr;
extern CPU_STK_SIZE const OSCfg_IdleTaskStkSize;
2、定义空闲任务函数
空闲任务正如其名,空闲,任务体里面只是对全局变量 OSIdleTaskCtr ++ 操作。 空闲任务函数定义在 os_core.c 文件中。
void OS_IdleTask (void *p_arg)
{
p_arg = p_arg;
for(;;)
{
OSIdleTaskCtr++;
}
}
全局变量 OSIdleTaskCtr 称为空闲任务计数变量。在 os.h 中定义。
OS_EXT OS_IDLE_CTR OSIdleTaskCtr;
其中的 OS_IDLE_CTR 是在 os_type.h 中重新定义的数据类型。
typedef CPU_INT32U OS_IDLE_CTR;
3、定义空闲任务控制块
OS_EXT OS_TCB OSIdleTaskTCB;
需要特别注意的是:这里的 OS_TCB 中多了一个成员 TaskDelayTicks,它的作用在后面的 实现阻塞延时 中会被提到。
typedef struct os_tcb OS_TCB;
struct os_tcb
{
CPU_STK *StkPtr;
CPU_STK_SIZE StkSize;
OS_TICK TaskDelayTicks;
};
OS_TICK是在os_type.h中重定义的变量。
typedef CPU_INT32U OS_TICK;
4、空闲任务初始化 – 包括空闲任务创建函数
该函数在 os_core.c 中定义。
void OS_IdleTaskInit(OS_ERR *p_err)
{
OSIdleTaskCtr = (OS_IDLE_CTR)0;
OSTaskCreate( (OS_TCB *)&OSIdleTaskTCB,
(OS_TASK_PTR )OS_IdleTask,
(void *)0,
(CPU_STK *)OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
(OS_ERR *)p_err );
}
5、在 OSInit 函数中调用空闲任务初始化函数 OS_IdleTaskInit
在前面,普通任务的任务创建函数是在 main 函数中被调用的。这里是 OSInit 中被调用。这么做的目的是表明在系统还没有启动之前空闲任务就已经创建好。
OSInit 函数也在 os_core.c 中定义。
void OSInit (OS_ERR *p_err)
{
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
OS_RdyListInit();
OS_IdleTaskInit(p_err);
if (*p_err != OS_ERR_NONE)
{
return;
}
}
6、实现阻塞延时
阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU 可以去执行其他的任务,如果其他的任务也在延时状态,那么 CPU 就将运行空闲任务。
阻塞延时函数在 os_time.c 中定义并实现。
void OSTimeDly(OS_TICK dly)
{
OSTCBCurPtr->TaskDelayTicks = dly;
OSSched();
}
TaskDelayTicks 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期。比如当SysTick 的中断周期为 10 ms,调用 OSTimeDly(2) 则完成 2*10ms 的延时。
函数 OSSched() 需要重新实现,在 os_core.c 中。
void OSSched(void)
{
if( OSTCBCurPtr == &OSIdleTaskTCB )
{
if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else
{
return;
}
}
else
{
if(OSTCBCurPtr == OSRdyList[0].HeadPtr)
{
if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else if(OSTCBCurPtr->TaskDelayTicks != 0)
{
OSTCBHighRdyPtr = &OSIdleTaskTCB;
}
else
{
return;
}
}
else if(OSTCBCurPtr == OSRdyList[1].HeadPtr)
{
if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else if(OSTCBCurPtr->TaskDelayTicks != 0)
{
OSTCBHighRdyPtr = &OSIdleTaskTCB;
}
else
{
return;
}
}
}
OS_TASK_SW();
}
主要操作时通过当前控制块的指针 OSTCBCurPtr (指向当前正在运行的一个任务)和最高优先级控制块的指针 OSTCBHighRdyPtr (指向下一个要运行的任务)来进行控制和判断的。
画图来理解下上述代码。
7、编写 main 函数
int main(void)
{
OS_ERR err;
CPU_IntDis();
OS_CPU_SysTickInit (10);
OSInit(&err);
OSTaskCreate ((OS_TCB*) &Task1TCB,
(OS_TASK_PTR ) Task1,
(void *) 0,
(CPU_STK*) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR ) Task2,
(void *) 0,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_ERR *) &err);
OSRdyList[0].HeadPtr = &Task1TCB;
OSRdyList[1].HeadPtr = &Task2TCB;
OSStart(&err);
}
需要注意:空闲任务初始化函数在OSInint 中调用,在系统启动之前创建好空闲任务。
8、修改 任务一 和 任务二 函数
void Task1( void *p_arg )
{
for( ;; )
{
flag1 = 1;
OSTimeDly(2);
flag1 = 0;
OSTimeDly(2);
}
}
void Task2( void *p_arg )
{
for( ;; )
{
flag2 = 1;
OSTimeDly(2);
flag2 = 0;
OSTimeDly(2);
}
}
9、这一切最为关键的主线其实还是 时基 SysTick
在 SysTick 中断服务函数中
void SysTick_Handler(void)
{
OSTimeTick();
}
而 OSTimeTick();
void OSTimeTick (void)
{
unsigned int i;
for(i=0; i<OS_CFG_PRIO_MAX; i++)
{
if(OSRdyList[i].HeadPtr->TaskDelayTicks > 0)
{
OSRdyList[i].HeadPtr->TaskDelayTicks --;
}
}
OSSched();
}
需要注意这个任务调度是非常有必要的,如果不进行任务调度,执行完 for 循环之后就结束了,直到发生下一次的SysTick中断,再来执行 for 循环,以此往复,但是就是不切换回到任务。所以说这个任务调度函数 OSSched() 是非常重要的。
总结
为了方便下次看到能迅速反应过来,我把自己理解的程序运行流程再详细描述如下。
1、程序总是从main函数开始执行,在main函数中配置了 SysTick 以及初始化了 OSInit(&err);以后,启动OS,程序将不再返回。(注意:当 SysTick 初始化完成之后,计时就开始了,只不过在程序一开始的时候 SysTick 中断被关闭了,在 系统启动 OSStart() 函数里面的 OSStartHighRdy() 中才被重新开启。)
2、程序首先执行任务一,当执行完 flag1 = 1;开始执行 OSTimeDly(2); 在这个程序中设置了任务一的延时时间以及为了实现阻塞延时做了任务切换到了任务二,然后任务二开始执行,任务一失去 CPU ,同样的, 当任务二执行完 flag2 = 1;开始执行 OSTimeDly(2); 在这个程序中设置了任务二的延时时间以及为了实现阻塞延时做了任务切换到了空闲任务(因为任务一仍然还处在阻塞状态),随着第一次 SysTick 中断产生(10ms时间到),系统执行中断服务程序 OSTimeTick,将每个任务的 TaskDelayTicks 减去 1 ,之后,通过任务调度函数 OSSched() 才切换到空闲任务(因为此时任务一和任务二依然处于阻塞状态)。当第二次 SysTick 中断产生(又一个10ms时间到),系统再次执行中断服务程序 OSTimeTick,将每个任务的 TaskDelayTicks 再减去 1 ,之后通过任务切换发现,任务一的TaskDelayTicks等于0(其实任务二的 TaskDelayTicks 也等于 0 了,但是在程序中的 if 条件语句中优先处理任务一了,通过颠倒顺序,仿真结果验证了,我在最后贴图。)之后,任务一再次往下执行,执行到 flag1 = 0 后,开始执行 OSTimeDly(2); 再次进入延时阻塞状态,才切换到任务二 …
好了,程序过程描述至此。
仿真验证疑问
if( OSTCBCurPtr == &OSIdleTaskTCB )
{
if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else
{
return;
}
}
调整顺序
if( OSTCBCurPtr == &OSIdleTaskTCB )
{
if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else
{
return;
}
}
需要注意的是,
1、从宏观上来看,任务一和任务二看似是同时运行的,但是实际上任务间还是有先后之分的。
2、在实际使用 μC/OS 时,我们并不会这样使用延时阻塞。我们会插入一个 延时列表 来进行维护。
|