一、调度器开启过程分析
1.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 中断等等。
1.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)
prvEnableVFP(); (5)
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; (6)
prvStartFirstTask(); (7)
//代码正常执行的话是不会到这里的!
return 0;
}
(1)、设置 PendSV 的中断优先级,为最低优先级。
(2)、设置滴答定时器的中断优先级,为最低优先级。
(3)、调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时
器的中断,函数比较简单,大家自行查阅分析。
(4)、初始化临界区嵌套计数器。
(5)、调用函数 prvEnableVFP()使能 FPU。
(6)、设置寄存器 FPCCR 的 bit31 和 bit30 都为 1,这样 S0~S15 和 FPSCR 寄存器在异常入
口和退出时的壮态自动保存和恢复。并且异常流程使用惰性压栈的特性以保证中断等待。关于
FPCCR 寄存器和惰性压栈的知识请参考《权威指南》的“第 12 章 浮点运算”。
1.3 使能 FPU 函数分析
在函数 xPortStartScheduler()中会通过调用 prvEnableVFP()来使能 FPU,这个函数是汇编形
式的,在文件 port.c 中有定义,函数如下:
__asm void prvEnableVFP( void )
{
PRESERVE8
ldr.w r0, =0xE000ED88 ;R0=0XE000ED88 (1)
ldr r1, [r0] ;从 R0 的地址读取数据赋给 R1 (2)
orr r1, r1, #( 0xf << 20 ) ;R1=R1|(0xf<<20) (3)
str r1, [r0] ;R1 中的值写入 R0 保存的地址中 (4)
bx r14 (5)
nop
}
(1)、利用寄存器 CPACR 可以使能或禁止 FPU,此寄存器的地址为 0XE000ED88(具体参考
《权威指南》“第 13 章 浮点运算”13.2.3 章节),此寄存器的 CP10(bit20 和 bit21)和 CP11(bit22
和 bit23)用于控制 FPU。这 4 个 bit 的具体含义请参考《权威指南》,通常将这 4 个 bit 都设置为
1 来开启 FPU,表示全访问。此行代码将地址 0XE000ED88 保存在寄存器 R0 中。
(2)、读取 R0 中保存的存储地址处的数据,也就是 CPACR 寄存器的值,并将结果保存在
R1 寄存器中。
(3)、R1 中的值与(0xf<<20)进行按位或运算,也就是 R1=R1|0X00F00000。此时 R1 所保存
的值的 bit20~bit23 就都为 1 了,将这个值写入寄存器 CPACR 中就可开启 FPU。
(4)、将 R1 中的值写入 R0 中保存的地址处,也就是寄存器 CPACR 中。
(5)、函数返回。bx 为间接跳转指令,一般为 BX <Rm>,也就是跳转到存放在 Rm 中的地
址处,此处是跳转到 R14 存放的地址处。R14 寄存器也叫做链接寄存(LR),也可以用 LR 表示。
这个寄存器用于函数或子程序调用时返回地址的保存。关于此寄存器的详细介绍请参考《权威
指南》“第 4 章 架构”的 4.2.2 章节。
1.4 启动第一个任务
经过上面的操作以后我们就可以启动第一个任务了,函数 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
}
(1)、将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地址(0X00000000)
开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提
供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器
(VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08,通过这个寄存器可以重新定
义向量表,比如在 STM32F429 的 ST 官方库中会通过函数 SystemInit()来设置 VTOR 寄存器,
代码如下:
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //VTOR=0x08000000+0X00
通过上面一行代码就将向量表开始地址重新定义到了 0X08000000,向量表的起始地址存储
的就是 MSP 初始值。关于向量表和向量表重定位的详细内容请参阅《权威指南》的“第 7 章
异常和中断”的 7.5 小节。
(2)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取寄存器 VTOR
中的值,并将其保存在 R0 寄存器中。这一行代码执行完就以后 R0 的值应该为 0X08000000。
(3)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取地址 0X08000000
处存储的数据,并将其保存在 R0 寄存器中。我们知道向量表的起始地址保存的就是主栈指针
MSP 的初始值,这一行代码执行完以后寄存器 R0 就存储 MSP 的初始值。现在来看(1)、(2)、
(3)这三步起始就是为了获取 MSP 的初始值而已!
(4)、复位 MSP,R0 中保存了 MSP 的初始值,将其赋值给 MSP 就相当于复位 MSP。
(5)和(6)、使能中断,关于这两个指令的详细内容请参考《权威指南》的“第 4 章 架构”
的第 4.2.3 小节。
(7)和(8)、数据同步和指令同步屏障,这两个指令的详细内容请参考《权威指南》的“第 5
章 指令集”的 5.6.13 小节。
(9),调用 SVC 指令触发 SVC 中断,SVC 也叫做请求管理调用,SVC 和 PendSV 异常对于
OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。关于 SVC 的详细内容请参考《权威指
南》的“第 10 章 OS 支持特性”的 10.3 小节。在 FreeRTOS 中仅仅使用 SVC 异常来启动第一
个任务,后面的程序中就再也用不到 SVC 了。
1.5 SVC 中断服务函数
在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就
是 在 SVC 中 断 服 务 函 数 中 完 成 的 , SVC 中 断 服 务 函 数 应 该 为 SVC_Handler() , 但 是
FreeRTOSConfig.h 中通过#define 的方式重新定义为了 xPortPendSVHandler(),如下:
#define xPortPendSVHandler PendSV_Handler
函数 vPortSVCHandler()在文件 port.c 中定义,这个函数也是用汇编写的,函数源码如下:
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB ;R3=pxCurrentTCB 的地址 (1)
ldr r1, [r3] ;取 R3 所保存的地址处的值赋给 R1 (2)
ldr r0, [r1] ;取 R1 所保存的地址处的值赋给 R0 (3)
ldmia r0!, {r4-r11, r14} ;出栈 ,R4~R11 和 R14 (4)
msr psp, r0 ;进程栈指针 PSP 设置为任务的堆栈 (5)
isb ;指令同步屏障
mov r0, #0 ;R0=0 (6)
msr basepri, r0 ;寄存器 basepri=0,开启中断 (7)
bx r14 (8)
}
(1)、获取 pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向 TCB_t 的指针,这个
指针永远指向正在运行的任务。
(2)、取 R3 所保存的地址处的值赋给 R1。通过这一步就获取到了当前任务的任务控制块的
存储地址。
(3)、取 R3 所保存的地址处的值赋给 R0,我们知道任务控制块的第一个字段就是任务堆栈
的栈顶指针 pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址(0X20001058)得到
的就是栈顶指针所指向的地址,当前我的程序中这个栈顶指针(pxTopOfStack)所指向的地址为
0X20001004
可以看出(1)、(2)和(3)的目的就是获取要切换到的这个任务的任务栈顶指针,因为任务所对
应的寄存器值,也就是现场都保存在任务的任务堆栈中,所以需要获取栈顶指针来恢复这些寄
存器值!
(4)、R4~R11,R14 这些寄存器出栈。这里使用了指令 LDMIA,LDMIA 指令是多加载/存
储指令,不过这里使用的是具有回写的多加载/存储访问指令,用法如下:
LDMIA Rn! , {reg list}
表示从 Rn 指定的存储器位置读取多个字,地址在每次读取后增加(IA),Rn 在传输完成以后
写回。对于 STM32 来说地址一次增加 4 字节,比如如下代码:
LDR R0, =0X800
LDMIA R0!, {R2~R4}
上面两行代码就是将 0X800 地址的数据赋值给寄存器 R2,0X804 地址的数据赋值给寄存
器 R3,0X8008 地址的数据赋值给 R4 寄存器,然后,重点来了!此时 R0 为 800A!
通过这一步我们就从任务堆栈中将 R4~R11,R14 这几个寄存器的值给恢复了,注意 R14 的
值为 0XFFFFFFFD,这个值就是我们在初始化任务堆栈的时候保存的 EXC_RETURN 的值!
这里有朋友就要问了,R0~R3,R12,PC,xPSR 这些寄存器怎么没有恢复?这是因为这些寄
存器会在退出中断的时候 MCU 自动出栈(恢复)的,而 R4~R11 需要由用户手动出栈。如果使用
FPU 的话还要考虑到 FPU 寄存器,这个我们在分析 PendSV 中断服务函数的时候会讲到。到这
步以后我们来看一下堆栈的栈顶指针指到哪里了?
可以看出恢复 R4~R11 和 R14 以后堆栈的栈顶指针应该指向地址 0X20001028,
也就是保存寄存器 R0 值的存储地址。退出中断服务函数以后进程栈指针 PSP 应该从这个地址
开始恢复其他的寄存器值。
(5)、设置进程栈指针 PSP,PSP=R0=0X20001028
(6)、设置寄存器 R0 为 0。
(7)、设置寄存器 BASEPRI 为 R0,也就是 0,打开中断!
(8)、执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使
用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开
始运行!