FreeRTOS学习之路三:任务调度器
前面两节讲了任务栈、任务内容、任务列表的联系,当我们创建好多个任务并把它添加到任务列表时,系统是如何来进行任务间的切换与调度。就靠freertos系统里面的调度器进行这项工作。
先看看freertos一般创建任务开始调度的代码流程。
流程就是创建任务,任何启动调度器。xTaskCreateStatic是一个静态内存任务创建函数,在前面已经说过,来看看这个函数的参数。
1.任务入口:也就是函数名,函数名也就代表任务起始地址。
2.任务名称:一段字符串。
3.任务栈大小:也就是自己定义的一个静态数组的大小。
4.任务形参:传进函数里面的参数
5.任务优先级:数字越大优先级越高。
6.任务栈起始地址:数组名。
7.任务控制块:指向任务TCB的指针。
返回值:pdPass代表创建任务成功,也就是为1.
然后将任务添加的任务列表上,直接调用vListInsertEnd函数。
最后启动调度器。
调度器
vTaskStartScheduler();
这个函数被称为调度器,可以说是freertos的核心。
函数原型:void vTaskStartScheduler( void ),无返回值,无参数。
函数实现:
为了方便学习,这里可以用野火简化的调度器函数,原函数有许多宏,这里列出一些必要的实现过程。
void vTaskStartScheduler( void )
{
/*======================================创建空闲任务start==============================================*/
/*空闲任务用于不造成CPU资源的浪费,当没有比空闲任务优先级高的任务时,系统就会一直运行此函数,此函数可以用来做一些资源清理等功能。*/
TCB_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
(char *)"IDLE", /* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(UBaseType_t) tskIDLE_PRIORITY, /* 任务优先级,数值越大,优先级越高 */
(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
(TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */
/*======================================创建空闲任务end================================================*/
/*下面两句主要是关于系统心跳时钟,暂时不管*/
xNextTaskUnblockTime = portMAX_DELAY;
xTickCount = ( TickType_t ) 0U;
//xTickCount = ( TickType_t ) 0xfffffffcUL;
/* 启动调度器 ,重要的是此函数*/
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
xPortStartScheduler函数,初始化时钟和系统悬挂的优先级为最低,为了任务在调度和被挂起时不被影响。
seType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化SysTick */
vPortSetupTimerInterrupt();
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
prvStartFirstTask(),此函数开始真正的调度,这是一段汇编代码,需要了解ARM架构,总而言之,开始会从中断向量表中找到中断服务子程序,
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
里面存放的是向量表的起始地址,即MSP的地址 */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* 设置主堆栈指针msp的值 */
msr msp, r0
/* 使能全局中断 */
cpsie i
cpsie f
dsb
isb
/* 调用SVC去启动第一个任务 */
svc 0
nop
nop
}
当代码执行到svc这一行时,在单步执行的时候会去指向svc中断服务子程序。在这一段中任务会找到第一个优先级高的TCB任务指针,将里面任务栈里面的内容分加载到寄存器中,其中有一个就是任务的入口地址,最后bx r14是中断返回,然后将任务栈里面的内容出栈进行任务执行。
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB;
PRESERVE8
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
ldr r1, [r3] /* 加载pxCurrentTCB到r1 */
ldr r0, [r1] /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
ldmia r0!, {r4-r11} /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
msr psp, r0 /* 将r0的值,即任务的栈指针更新到psp */
isb
mov r0, #0 /* 设置r0的值为0 */
msr basepri, r0 /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
orr r14, #0xd /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
bx r14 /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
同时PSP的值也将更新,即指向任务栈的栈顶 */
}
[1]: 参考《FreeRTOS 内核实现与应用开发实战指南