8.1 调度器开启过程分析
8.2.1 任务调度器开启函数分析
前面的所有例程中我们都是在 main()函数中先创建一个开始任务 start_task,后面紧接着调用函数 vTaskStartScheduler()。这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c中有定义,缩减后的函数代码如下:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
xReturn = xTaskCreate( prvIdleTask, (1)
"IDLE", configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
#if ( configUSE_TIMERS == 1 ) //使用软件定时器使能
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask(); (2)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS ) //空闲任务和定时器任务创建成功。
{
portDISABLE_INTERRUPTS(); (3)
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE; (4)
xTickCount = ( TickType_t ) 0U;
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5)
if( xPortStartScheduler() != pdFALSE ) (6)
{
//如果调度器启动成功的话就不会运行到这里,函数不会有返回值的
}
else
{
//不会运行到这里,除非调用函数 xTaskEndScheduler()
}
}
else
{
//程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
//空闲任务或者定时器任务的时候没有足够的内存。
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
//防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提
//示 xIdleTaskHandle 未使用。
( void ) xIdleTaskHandle;
}
(1)、创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最低。
(2)、如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程 是在函数 xTimerCreateTimerTask()中完成的,这个函数很简单,大家就 自行查阅一下。
(3)、关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。
(4)、变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。
(5)、当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。
(6)、调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等等。
8.2.2 内核相关硬件初始化函数分析
FreeRTOS 系统时钟是由滴答定时器来提供的, 而且任务切换也会用到 PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成,缩减后的函数代码如下:
BaseType_t xPortStartScheduler( void )
{
/******************************************************************/
/****************此处省略一大堆的条件编译代码**********************/
/*****************************************************************/
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; (1)
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; (2)
vPortSetupTimerInterrupt(); (3)
uxCriticalNesting = 0; (4)
prvStartFirstTask(); (5)
//代码正常执行的话是不会到这里的!
return 0;
}
(1)、设置 PendSV 的中断优先级,为最低优先级。
(2)、设置滴答定时器的中断优先级,为最低优先级。
(3)、调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断,函数比较简单,大家自行查 阅分析。
(4)、初始化临界区嵌套计数器。
(5)、调用函数 prvStartFirstTask()开启第一个任务
8.2.3 启动第一个任务
经过上面的操作以后我们就可以启动第一个任务了,函数 prvStartFirstTask()用于启动第一个任务,这是一个汇编函数,函数源码如下:
__asm void prvStartFirstTask( void )
{
PRESERVE8
ldr r0, =0xE000ED08 ;R0=0XE000ED08 (1)
ldr r0, [r0] ;取 R0 所保存的地址处的值赋给 R0 (2)
ldr r0, [r0] ;获取 MSP 初始值 (3)
msr msp, r0 ;复位 MSP (4)
cpsie I ;使能中断(清除 PRIMASK) (5)
cpsie f ;使能中断(清除 FAULTMASK) (6)
dsb ;数据同步屏障 (7)
isb ;指令同步屏障 (8)
svc 0 ;触发 SVC 中断(异常) (9)
nop
nop
}
8.2.4 SVC 中断服务函数
8.2.5 空闲任务
创建一个名为“IDLE”的任务,这个任务叫做空闲任务。顾名思义,空闲任务就是空闲的时候运行的任务,也就是系统中其他的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是 FreeRTOS 系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!但是空闲任务不仅仅是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他的事情,如下:
1、判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。
2、运行用户设置的空闲任务钩子函数。
3、判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理
空闲任务的任务优先级是最低的,为 0,任务函数为 prvIdleTask(),有关空闲任务的详细内容我们后面会有专门的章节讲解,这里大家只要知道有这个任务就行了
8.3任务创建过程分析
8.3.1 任务创建函数分析
(1)、 使用函数 pvPortMalloc()给任务的任务堆栈申请内存,申请内存的时候会做字节对齐处理。
(2)、如果堆栈的内存申请成功的话就接着给任务控制块申请内存,同样使用函数pvPortMalloc()。
(3)、 任务控制块内存申请成功的话就初始化内存控制块中的任务堆栈字段 pxStack,使用(1)中申请到的任务堆栈。
(4)、如果任务控制块内存申请失败的话就释放前面已经申请成功的任务堆栈的内存。
(5)、标记任务堆栈和任务控制块是使用动态内存分配方法得到的。
(6)、使用函数 prvInitialiseNewTask()初始化任务,这个函数完成对任务控制块中各个字段的初始化工作!
(7)、使用函数 prvAddNewTaskToReadyList()将新创建的任务加入到就绪列表中。
8.3.2 任务初始化函数分析
(1) 、 如 果 使 能 了 堆 栈 溢 出 检 测 功 能 或 者 追 踪 功 能 的 话 就 使 用 一 个 定 值tskSTACK_FILL_BYTE 来填充任务堆栈, 这个值为 0xa5U。
(2)、计算堆栈栈顶 pxTopOfStack,后面初始化堆栈的时候需要用到。
(3)、 保存任务的任务名。
(4)、 任务名数组添加字符串结束符’\0’。
(5)、判断任务优先级是否合法,如果设置的任务优先级大于 configMAX_PRIORITIES,则将优先级修改为 configMAX_PRIORITIES-1。
(6)、 初始化任务控制块的优先级字段 uxPriority。
(7)、使能了互斥信号量功能,需要初始化相应的字段。
(8)和(9)、初始化列表项 xStateListItem 和 xEventListItem,任务控制块结构体中有两个列表项,这里对这两个列表项做初始化。
(10)和(12)、设置列表项 xStateListItem 和 xEventListItem 属于当前任务的任务控制块,也就是设置这两个列表项的字段 pvOwner 为新创建的任务的任务控制块。
(11)、设置列表项xEventListItem的字段xItemValue为configMAX_PRIORITIES- uxPriority,比如当前任务优先级 3,最大优先级为 32,那么 xItemValue 就为 32-3=29,这就意味着 xItemValue值越大,优先级就越小。上一章学习列表和列表项的时候我们说过,列表的插入是按照xItemValue 的值升序排列的。
(12)、初始化线程本地存储指针,如果使能了这个功能的话。
(13)、调用函数 pxPortInitialiseStack()初始化任务堆栈。
(14)、生成任务句柄,返回给参数 pxCreatedTask,从这里可以看出任务句柄其实就是任务控制块。