8 任务调度与移植
基本所有在FreeRTOS的main函数中,都是在任务创建之后,后面紧接着调用一个vTaskStartScheduler()函数。这个函数就是用来开启任务调度器的,在tasks.c文件中有函数的源码定义。
-
8.1 任务调度开启函数
void vTaskStartScheduler( void ) { BaseType_t xReturn; xReturn = xTaskCreate( prvIdleTask, "IDLE", usIdleTaskStackSize, ( void * ) NULL, (tskIDLE_PRIORITY|portPRIVILEGE_BIT), &xIdleTaskHandle ); ················· ① #if ( configUSE_TIMERS == 1 ) { if( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask();················② } else { mtCOVERAGE_TEST_MARKER(); } } if( xReturn == pdPASS ) { portDISABLE_INTERRUPTS();·······················③ #if ( configUSE_NEWLIB_REENTRANT == 1 ) { _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } xNextTaskUnblockTime = portMAX_DELAY; xSchedulerRunning = pdTRUE; ························④ xTickCount = ( TickType_t ) 0U; if( xPortStartScheduler() != pdFALSE )···················⑤ { //如果调度器启动成功的话,是不会运行到这里的,函数 不会有返回值的 } else { //除非调用vTaskEndScheduler(),否则不会运行到这里 } } else { //内核没有启动成功才会运行到这里, //往往是因为创建空闲任务或者定时器任务的时候 //没有足够的内存configASSERT( xReturn ); } //防止编译报错 ( void ) xIdleTaskHandle; }
①创建空闲任务(动态创建)
②创建定时器服务
③关闭中断
④ xSchedulerRunning默认为pdFALSE,这里设为pdTRUE表示调度开始运行,别的地方会以此变量当做标志位来进行操作
⑤调用函数xPortStartScheduler()来初始化跟调度器启动有关的硬件
- 8.2 FreeRTOS移植相关函数
特别要注意的是:
xPortStartScheduler() 的函数所涉及的都是移植相关的东西,要移植的文件port.c很多也都是为它服务的。
xPortStartScheduler函数原型:
prvSetupTimerInterrupt() 函数是用来设置滴答定时器的定时周期,并且使能滴答定时器的中断。我们所移植的代码中,这个函数提供的是一个滴答定时器的初始化接口函数,也就是vPortSystick_Init()。
uxCriticalNesting 表示临界区的嵌套层数,在初始化的时候将它的值设置为0,没有什么代码进入临界区。
vPortStartFirstTask() 就是触发一次TRAP 1中断,TRAP 1中断也就是执行一个汇编函数portRESTORE_CONTEXT();
这里先来看看vPortSystick_Init函数的使用, vPortSystick_Init函数中的代码就是进行3个初始化,两个TRAP和滴答定时器的初始化。裁剪后的代码,如下图所示:
-
8.2.1 滴答定时器的初始化
vPortSystick_Init() 函数本身就是为了初始化滴答定时器,在初始化的时候调用了systick_init()函数,同时传递了两个参数ulClkLoadCnt、xPortSysTickHandler。看到systick_init()函数就会知道这两个值的作用:
ulClkLoadCnt是滴答定时器每次重新加载的值,由这个值减到会是一个周期,触发一次滴答中断,运行xPortSysTickHandler函数。ulClkLoadCnt在vPortSystick_Init函数中计算出来的值,正好是每1ms进入一次中断,也就是说系统滴答的周期是1ms。在xPortSysTickHandler函数中会将滴答的计数值进行加一。 -
8.2.2 TRAP函数的使用
在vPortSystick_Ini() 函数中还初始化了两个TRAP,在移植中这两个TRAP的使用也是非常关键的。TRAP1中断就是调用portRESTORE_CONTEXT这个宏函数来进行恢复下文。触发TRAP0中断最终会调用vPortYieldHandler函数,这是一个进行上下文切换的函数,代码如下:
这个函数一共就三行,功能很明确。保存上文,选择要切换的下文,进行切换。
在进入中断函数的时候,就会将一部分寄存去入栈,这里通过一些减法操作将剩下的保存uxCriticalNesting、EPSR、EPC的寄存器也入栈,在我们切换的时候,需要将之前任务这些寄存器都保存起来。
在执行完vTaskSwitchContext函数之后,下一个要执行任务的TCB会被保存在pxCurrentTCB中。这里的代码将要运行的下一个任务的堆栈进行出栈操作,实现的是将下个要运行的任务进行出栈的操作。
vTaskSwitchContext函数会将处于就绪状态的最高优先级的任务选中,将它的TCB保存在pxCurrentTCB中,代码简化后如下:
这里的if判断语句,因为如果调度器挂起就不能进行切换。调用函数taskSELECT_HIGHEST_PRIORITY_TASK()获得下一个要运行的任务。
选择完要切换的任务,在这里进行切换:
这里进行的主要是将下一个要运行的任务进行一些出栈的操作,将相应的寄存器出栈,切换到下文。
- 8.2.3 任务栈的初始化
我们在进行任务创建的时候,会在我们的移植层对创建的任务进行栈的初始化:
1:栈顶为空
2:然后是R15存放我们的PC指针,
3:将R13-R1都初始化为0,这里主要是为了将这些寄存器预留出来,以方便后面我们进行使用,
4:R0初始化为pvParameters,作为任务的输入参数,通常函数会调用R0~R3作为输入参数,R0也可以用作返回结果,如果返回值是64位,则R1也会用作返回结果。
5:然后是临界嵌套的计数值
6:寄存器xPSR,这里是用作EPSR寄存器在遇到异常情况时被用来保存当前处理器执行的内容。
7:最后是EPC,异常保留程序计数器的存储