本章是对上一张的补充,具体的代码也没有细看。
目录
前言
之前任务体内的延时使用的的是软件延时,即让CPU空等达到延时的效果,但RTOS的很大优势就是榨干CPU的性能,任务需要延时的时候也不能让CPU空等来实现延时的效果,所以任务需要延时的时候,任务会放弃CPU的使用权,CPU可以去干其他的事情,当任务延时时间到,重新获取CPU使用权,这称作阻塞延时。
当任务需要延时进入阻塞状态,CPU如果没有其他任务可以运行,RTOS会为CPU创建一个空闲任务。在FreeRETOS中空闲任务是在启动调度器的时候创建的优先级最低的任务,空闲任务主体主要做一些系统内存的清理工作。
本章实现的空闲任务是一个对全局变量进行计数。
在实际应用中,可以在空闲任务中让单片机进入休眠或者低功耗等操作。
1、实现空闲任务
目前在创建任务时使用的栈和TCB都是用的是静态内存,即需要预先定义好的内存。在main.c中定义。
1.1、定义空闲任务的栈
/*定义空闲任务的栈*/
#define configMINIMAL_STACK_SIZE (( unsigned short ) 128 )
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
空闲任务的栈是一个定义好的数组,大小由FreeRTOSConfig.h中定义的宏configMINIMAL_STACK_SIZE控制,默认128字即512字节。
1.2、定义空闲任务的任务控制块
定义在main.c中
/*定义空闲任务的任务控制块*/
TCB_t IdleTaskTCB;
1.3、创建空闲任务
主要是要实现创建一个任务这样的过逞。
空闲任务在调度器启动函数vTaskStartScheduler()中创建
extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory(
TCB_t **ppxIdleTaskStackBuffer,
StackType_t **ppIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize
);
void TaskStartScheduler(void)
{
/*==================创建空闲函数====================*/
TCB_t *pxIdleTaskTCBBuffer = NULL; //用于指向空闲任务控制块
StackType_t *pxIdleTaskStackBuffer = NULL; //用于指向空闲任务栈起始地址
uint32_t ulIdleTaskStackSize;
/*获取空闲任务的内存,任务栈,任务TCB*/
vApplicationGetIdleTaskMemory(
&pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize
);
/*创建空闲任务*/
xIdleTaskHandle =
xCreateTaskStatic( (TaskFunction_t) prvIdleTask, /*任务入口*/
(char 8)”IDLE“, /*任务名称,字符串形式*/
(uint32_t)ulIdleTaskStackSize, /*任务栈大小,大小为字*/
(void *)NULL, /*任务形参*/
(StackType_t *)pxIdleTaskStackBuffer, /*任务栈起始地址*/
(TCB_t *)pxIdleTaskTCBBuffer /*任务控制块*/
);
/*将任务添加到就绪列表*/
VListInertEnd(&(pxReadyTaskLists[0]),&((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem));
/*=====================创建空闲任务===================== */
/*手动指定第一个运行的任务*/
pxCurrentTCB = &Task1TCB;
/*启动调度器*/
if( xPortStartScheduler() != pdFALSE )
{
/*调度器启动成功,则不会来到这里*/
}
}
获取空闲任务的内存,即将pxIdleTaskTCBBuffer和pxIdleTaskStackBuffer作为形参传到xTaskCreateStatic()函数的指针分别指向空闲任务的TCB和栈的起始地址,这个操作由vApplicationGetIdleTaskMemory()实现。在main.c中实现。
void vApplicationGetIdleTaskMemory(
TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize
)
{
*ppxIdleTaskTCBBuffer = &IdleTaskTCB;
*ppxIdleTaskStackBuffer = IdleTaskStack;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
2、实现阻塞延时
目的是要起到一个延时的效果,需要延时时当前任务失去CPU使用权,CPU转向执行其他任务,没有其他任务就执行空闲任务,演示结束后CPU转向执行当前任务。实际上是一个任务切换的过程,所以要实现当前任务向其他任务或空闲任务切换的过程。
2.1、vTaskDelay()函数
阻塞延时的阻塞是指任务调用该延时函数后,任务会被玻璃CPU使用权,然后进入阻塞状态,直到延时结束,任务重新获取CPU使用权才可以继续运行,在任务阻塞这段时期,CPU可以去执行其他任务,如果其他的任务也在延时状态,那么CPU就将运行空闲任务。在task.c中定义。
void vTaskDelay(const TickType_t xTicksToDelay)
{
TCB_t *pxTCB = NULL;
/*获取当前任务的TCB*/
pxTCB = pxCurrentTCB;
/*设置延时时间*/
pxTCB->xTicksToDelay = xTicksToDelay;
/*任务切换*/
taskYIELD();
}
pxCurrentTCB是一个全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。
xTicksToDelay是任务控制块的一个成员,用于记录任务需要延时的时间,单位为SysTick的中断周期,本书中SysTick中断周期为10ms.
xTicksToDelay定义
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /*栈顶*/
ListItem_t xStateListItem /*任务节点*/
StackType_t *pxStack; /*任务栈起始地址*/
char pcTaskName[ configMAX_TASK_NAME_LEN ];/*任务名称,字符串形式*/
TickType_t xTicksToDelay /*用于延时*/
}tskTCB;
2.2、修改vTaskSwitchContext()函数
调用taskYIELD()会产生PendSV中断,在PendSV中断服务函数中会调用上下文切换函数vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新pxCurrentTCB。
本章多增加了一个空闲任务,则需要让pxCurrentTCB在三个任务中切换。
#if 0
void vTaskSwitchContext(void)
{
/*两个任务轮流切换*/
if(pxCurrentTCB == &Task1TCB )
{
pxCurrentTCB = &Task2TCB;
}
else
{
pxCurrentTCB = &Task1TCB;
}
}
#else
void vTaskSwitchContext( void )
{
/*如果当前任务是空闲任务,那么就尝试执行任务1或者任务2,
看看他们的延时时间是否结束,如果任务的延时时间均没有到期,
那就返回继续执行空闲任务*/
if(pxCurrentTCB == &IdleTaskTCB)
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else
{
return;//任务延时均没有到期则返回,继续执行空闲任务
}
}
else //当前任务不是空闲任务则会执行到这里
{
/*如果当前任务是任务1或者任务2的话,检查下另外一个任务,
如果另外的任务不在延时中,就切换到该任务
否则,判断下当前任务是否应该进入延时状态
如果是的话就切换到空闲任务,否则就不进行任何切换*/
if(pxCurrentTCB == &Task1TCB)
{
if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不进行切换,因为两个任务都处于延时中 */
}
}
else if(pxCurrentTCB == &Task2TCB)
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不进行切换,因为两个任务都处于延时中 */
}
}
}
}
#endif
3、SysTick中断服务函数
在任务上下文切换函数vTaskSwitchContext()中,会判断xTicksToDelay的值是否为零,如果为零就要将对应的任务就绪,如果不为零就继续延时。如果一个任务要延时,开始xTicksToDelay不为零,当xTicksToDelay变为0的时候表示延时结束。xTicksToDelay周期递减时的周期有SysTick中断提供,SysTick中断服务函数在port.c中实现
void xPortSysTickHandler(void)
{
/*关中断*/
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 开中断 */
vPortClearBASEPRIFromISR();
}
更新系统时基函数xTaskIncrementTick()
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
/* 更新系统时基计数器 xTickCount, xTickCount 是一个在 port.c 中定义的全局变量 */
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();
}
4、SysTick初始化函数
SysTick的中断服务函数要想被顺利执行,则SysTick必须先初始化。
void vPortSetUpTimerInterrupt(void)
vPortSetupTimerInterrupt()在xPortStartScheduler()中被调用。
总结
本节空闲任务与阻塞延时的实现就是在上节任务的创建与任务的切换的基础上,将软件延时换为了阻塞延时,阻塞延时就是当要进行延时的时候,当前任务失去CPU的使用权,CPU转向去执行其他任务,当没有其他任务看完执行的时候执行空闲任务,延时结束后返回到当前任务。
空闲任务与阻塞延时的实现还是对一个新任务的创建与三个任务的切换,所以在上一节的基础上,在启动调度器的部分加上了创建空闲任务,新建了阻塞延时函数,同时修改了上下文切换函数TaskSwitchContext()的内容并定义了SysTick中断服务函数来提供中断周期。