开启任务调度器
vTaskStartScheduler()作用:
用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度
该函数内部实现,如下:
- 创建空闲任务:prvIdleTask
#else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */ { /* The Idle task is being created using dynamically allocated RAM. */ xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL, portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */ &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */ } #endif /* configSUPPORT_STATIC_ALLOCATION */
- 如果使能软件定时器,则创建定时器任务。创建软件定时器任务:xTimerCreateTimerTask
#if ( configUSE_TIMERS == 1 ) { if( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_TIMERS */
- 关中断(在启动第一个任务时开启)防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
/* Interrupts are turned off here, to ensure a tick does not occur * before or during the call to xPortStartScheduler(). The stacks of * the created tasks contain a status word with interrupts switched on * so interrupts will automatically get re-enabled when the first task * starts to run. */ portDISABLE_INTERRUPTS();
- 初始化全局变量,并将任务调度器的运行标志设置为已运行
xNextTaskUnblockTime = portMAX_DELAY; xSchedulerRunning = pdTRUE; xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
- 初始化任务运行时间统计功能的时基定时器
/* If configGENERATE_RUN_TIME_STATS is defined then the following * macro must be defined to configure the timer/counter used to generate * the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS * is set to 0 and the following line fails to build then ensure you do not * have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your * FreeRTOSConfig.h file. */ portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); traceTASK_SWITCHED_IN();
- 调用函数 xPortStartScheduler()完成启动任务调度器
/* Setting up the timer tick is hardware specific and thus in the * portable interface. */ if( xPortStartScheduler() != pdFALSE ) { /* Should not reach here as if the scheduler is running the * function will not return. */ } else { /* Should only reach here if a task calls xTaskEndScheduler(). */ }
xPortStartScheduler() 作用:
该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务
该函数内部实现,如下:
- 检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
- 配置 PendSV 和 SysTick 的中断优先级为最低优先级
/* Make PendSV and SysTick the lowest priority interrupts. */ portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
- 调用函数 vPortSetupTimerInterrupt ( ) 配置 SysTick
/* Start the timer that generates the tick ISR. Interrupts are disabled * here already. */ vPortSetupTimerInterrupt();
- 初始化临界区嵌套计数器为 0
/* Initialise the critical nesting count ready for the first task. */ uxCriticalNesting = 0;
- 调用函数 prvEnableVFP() 使能 FPU(F1系列没有该功能)
- 调用函数 prvStartFirstTask() 启动第一个任务
/* Start the first task. */ prvStartFirstTask();
启动第一个任务
prvStartFirstTask ()作用:
开启第一个任务,用于初始化启动第一个任务前的环境,主要是重新设置MSP 指针,并使能全局中断
MSP指针
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时, MCU 会自动更新 SP 指针,ARM Cortex-M 内核提供了两个栈空间:
- 主堆栈指针(MSP):它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用
- 进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。
在FreeRTOS中,中断使用MSP(主堆栈),中断以外使用PSP(进程堆栈)
取 MSP 的初始值的思路是先根据向量表的位置寄存器 VTOR (0xE000ED08) 来获取向量表存储的地址; 在根据向量表存储的地址,来访问第一个元素,也就是初始的 MSP
/*--------------------汇编语言------------------------------*/
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
PRESERVE8 //8字节对齐
/* 使用NVIC偏移寄存器定位堆栈 */
ldr r0, =0xE000ED08
//将地址0xE000ED08加载到寄存器r0中。这个地址是NVIC中的一个寄存器,用于存储主堆栈指针(MSP)的当前值。
ldr r0, [ r0 ]
//从r0指向的地址(即0xE000ED08)加载值到r0。r0中存储的是另一个地址,这个地址指向实际的MSP值。
ldr r0, [ r0 ]
//再次从r0指向的地址加载值到r0。现在,r0中存储的是MSP的值。
/* 设置MSP回到堆栈的开始 */
msr msp, r0
//将r0中的值(即MSP的值)设置回MSP寄存器。这通常是在系统启动或任务切换时进行的,以确保堆栈指针指向正确的位置。
/* 全局启用中断 */
cpsie i //启用IRQ中断。
cpsie f //启用FIQ中断。
dsb //确保之前的指令完成执行。
isb //清理指令流水线,确保之前的指令更改立即生效。
/* 调用SVC启动第一个任务 */
svc 0
//通过SVC指令调用系统服务,0是传递给服务的参数。这通常用于请求操作系统服务,如启动第一个任务。
nop //无操作指令,用于确保svc指令有足够的时间执行。
nop
/* *INDENT-ON* */
}
vPortSVCHandler ( )作用:
- 通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务是系统将要运行的任务
- 通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已初始化,然后设置 PSP 指针 。
- 通过往 BASEPRI 寄存器中写 0,允许中断。
- R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN
而EXC_RETURN 只有 6 个合法的值(M4、M7)(M3系列只有3个),如下表所示:
描述 |
使用浮点单元 |
未使用浮点单元 |
中断返回后进入Hamdler模式,并使用MSP |
0xFFFFFFE1 |
0xFFFFFFF1 |
中断返回后进入线程模式,并使用 MSP |
0xFFFFFFE9 |
0xFFFFFFF9 |
中断返回后进入线程模式,并使用 PSP |
0xFFFFFFED |
0xFFFFFFFD |
注意:SVC中断只在启动第一次任务时会调用一次,以后均不调用
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
PRESERVE8
ldr r3, = pxCurrentTCB
//将pxCurrentTCB的地址加载到寄存器r3中。pxCurrentTCB是一个指向当前任务控制块(TCB)的指针。
ldr r1, [ r3 ]
//从r3指向的地址(即pxCurrentTCB)加载值到r1,这样r1就指向了当前任务的TCB。
ldr r0, [ r1 ]
//从r1指向的地址(即当前任务的TCB)加载值到r0,r0现在包含了当前任务的栈顶地址。
ldmia r0 !, { r4 - r11 }
//从r0指向的地址(即当前任务的栈顶)加载一系列寄存器(r4到r11),!表示更新r0的值,即栈指针向上移动。
msr psp, r0
//将r0的值(更新后的栈指针)设置为主栈指针(PSP)。
isb
//指令同步屏障,确保之前的指令执行完成。
mov r0, # 0
//将0移动到r0寄存器,准备设置基本优先级寄存器。
msr basepri, r0
//将r0的值(0)设置为基本优先级寄存器(BASEPRI),这样可以允许所有优先级的中断。
orr r14, # 0xd
//将r14寄存器的值与0xd进行或操作,这通常用于设置返回地址或模式。
bx r14
//从r14寄存器指向的地址返回,继续执行被SVC异常打断的代码。
/* *INDENT-ON* */
}
出栈
出栈(恢复现场),方向:从下往上(低地址往高地址):假设r0地址为0x04汇编指令示例:
ldmia r0!, {r4-r6}
任务栈r0地址由低到高,将r0存储地址里面的内容手动加载到 CPU寄存器r4、r5、
压栈
压栈(保存现场),方向:从上往下(高地址往低地址):假设r0地址为0x10汇编指令示例:
stmdb r0!, {r4-r6}
r0的存储地址由高到低递减,将r4、r5、r6里的内容存储到r0的任务栈里面。
任务切换
任务切换的本质:就是CPU寄存器的切换。(任务切换的过程在PendSV中断服务函数里边完成)
假设当由任务A切换到任务B时,主要分为两步:
第一步:需暂停任务A的执行,并将此时任务A的寄存器保存到任务堆栈,这个过程叫做保存现场;
第二步:将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换
PendSV中断服务函数
本质:通过向中断控制和状态寄存器 ICSR 的bit28 写入 1 挂起 PendSV 来启动 PendSV 中断
- 滴答定时器中断调用
- 执行FreeRTOS提供的相关API函数:portYIELD()
/*-------------------------------------------------------------------*/
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
//外部变量声明
PRESERVE8
/*-------------------保存当前任务的上下文------------------------------*/
mrs r0, psp
//使用mrs指令将程序状态寄存器(PSP)的值读取到寄存器r0中。PSP用于存储任务级别的堆栈指针。
isb
//禁用中断
ldr r3, =pxCurrentTCB
ldr r2, [ r3 ]
//将当前任务的TCB地址(存储在r3中)和TCB本身(存储在r2中)加载到寄存器中。
stmdb r0 !, { r4 - r11 }
//使用stmdb指令将寄存器r4到r11的值保存到当前任务的堆栈中
str r0, [ r2 ]
//将PSP的值存储到TCB中,以便在任务恢复时能够找到其堆栈。
/*----------------------准备任务切换-----------------------------------*/
stmdb sp !, { r3, r14 }
//将r3(和r14压入系统堆栈(使用sp)。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
//设置basepri寄存器为configMAX_SYSCALL_INTERRUPT_PRIORITY,
//以确保在任务切换期间不会被更高优先级的中断打断。
dsb
isb
/*---------------------执行任务切换-------------------------------------*/
bl vTaskSwitchContext
//调用vTaskSwitchContext函数进行任务切换。
mov r0, #0
msr basepri, r0
//将basepri寄存器清零,允许所有中断。
ldmia sp !, { r3, r14 }
//从系统堆栈中恢复r3和r14寄存器的值。
/*-------------------恢复新任务的上下文-----------------------------------*/
ldr r1, [ r3 ]
ldr r0, [ r1 ]
//从新任务的TCB中加载堆栈指针(PSP)到r0。
ldmia r0 !, { r4 - r11 }
//使用ldmia指令从堆栈中恢复r4到r11寄存器的值。
msr psp, r0
//将PSP寄存器的值更新为新任务的堆栈指针。
isb
//禁用中断
bx r14
//使用bx指令跳转到链接寄存器(r14)中存储的地址,即新任务的执行点。
nop
/*-----------------------------------------------------------*/
vTaskSwitchContext( )
查找最高优先级任务
通过这个函数在vTaskSwitchContext( )中taskSELECT_HIGHEST_PRIORITY_TASK( ) 函数完成
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{
UBaseType_t uxTopPriority; /* Find the highest priority list that contains ready tasks. */
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
}
/* taskSELECT_HIGHEST_PRIORITY_TASK() */
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
前导置零指令(__clz( ( uxReadyPriorities ) )
可以简单理解为计算一个 32位数,头部 0 的个数 (通过前导置零指令获得最高优先级)
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
通过该函数获取当前最高优先级任务的任务控制块
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{
List_t * const pxConstList = ( pxList );
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )
{
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
}
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
}