目录
学习调度器之前的一些建议:
本讲的内容和Cortex-M处理器的内核架构密切联系,所以学习之前建议大家:
⭐①提前阅读《Cortex M3权威指南(中文)》和《Cortex M3与M4权威指南》
⭐②结合文档教程正点原子《FreeRTOS开发手册》第八章进行学习
1.开启任务调度器
函数:vTaskStartScheduler()
作用:创建好任务后,如果不开启任务调度器,任务将不能被执行。用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度。
vTaskStartScheduler()函数完整代码+中文注释步骤如下:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/* 判断动态创建还是静态创建,由于默认是动态创建任务,并不会进入到下面的if语句,而是进入到else语句 */
/* Add the idle task at the lowest priority. */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t * pxIdleTaskTCBBuffer = NULL;
StackType_t * pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* The Idle task is created using user provided RAM - obtain the
* address of the RAM then create the idle task. */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */
portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
{
/* 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 */
/* 2、如果使能软件定时器,则调用函数xTimerCreateTimerTask()创建定时器任务 */
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
/* xReturn为创建任务返回值,创建成功则返回pdPASS */
if( xReturn == pdPASS )
{
/* freertos_tasks_c_additions_init() should only be called if the user
* definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
* the only macro called by the function. */
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
freertos_tasks_c_additions_init();
}
#endif
/* 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. */
/* 3、关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断 */
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Switch Newlib's _impure_ptr variable to point to the _reent
* structure specific to the task that will run first.
* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
* for additional information. */
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
/* 4、初始化全局变量,并将任务调度器的运行标志设置为已运行 */
xNextTaskUnblockTime = portMAX_DELAY; /* 下一个任务的阻塞超时时间,由于是在开启任务调度器,并没有任务,所以设置为最大阻塞时间 */
xSchedulerRunning = pdTRUE; /* 调度器运行状态设置为正在运行 */
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;/* 系统节拍周期,滴答定时器每中断一次,变量xTickCount加1,为系统提高心跳节拍。由于此时并没有任务执行,则变量xTickCount赋值为0 */
/* 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. */
/* 5、初始化任务运行时间统计功能的时基定时器(统计任务运行时间,此处只有接口,需要时需自行编写) */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/* 调试功能,函数并未实现,只保留了函数接口 */
traceTASK_SWITCHED_IN();
/* Setting up the timer tick is hardware specific and thus in the
* portable interface. */
/* 6、调用函数 xPortStartScheduler() */
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(). */
}
}
else
{
/* This line will only be reached if the kernel could not be started,
* because there was not enough FreeRTOS heap to create the idle task
* or the timer task. */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
* meaning xIdleTaskHandle is not used anywhere else. */
( void ) xIdleTaskHandle;
/* OpenOCD makes use of uxTopUsedPriority for thread debugging. Prevent uxTopUsedPriority
* from getting optimized out as it is no longer used by the kernel. */
( void ) uxTopUsedPriority;
}
在函数vTaskStartScheduler()中调用了函数xPortStartScheduler(),具体作用是用于完成启动任务调度器中与硬件架构相关的配置部分(比如滴答定时器的初始化配置),以及启动第一个任务。运行完函数时xPortStartScheduler()并不会再返回,而是直接跳转第一个任务开始执行。
xPortStartScheduler()函数完整代码+中文注释步骤如下:
BaseType_t xPortStartScheduler( void )
{
/* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
* See https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
/* This port can be used on all revisions of the Cortex-M7 core other than
* the r0p1 parts. r0p1 parts should use the port from the
* /source/portable/GCC/ARM_CM7/r0p1 directory. */
configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
/* 1、检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误 */
#if ( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
/* Determine the maximum priority from which ISR safe FreeRTOS API
* functions can be called. ISR safe functions are those that end in
* "FromISR". FreeRTOS maintains separate thread and ISR API functions to
* ensure interrupt entry is as fast and simple as possible.
*
* Save the interrupt priority value that is about to be clobbered. */
ulOriginalPriority = *pucFirstUserPriorityRegister;
/* Determine the number of priority bits available. First write to all
* possible bits. */
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
/* Read the value back to see how many bits stuck. */
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* The kernel interrupt priority should be set to the lowest
* priority. */
configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
/* Use the same mask on the maximum system call priority. */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
/* Calculate the maximum acceptable priority group value for the number
* of bits read back. */
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
#ifdef __NVIC_PRIO_BITS
{
/* Check the CMSIS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif
#ifdef configPRIO_BITS
{
/* Check the FreeRTOS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif
/* Shift the priority group value back to its position within the AIRCR
* register. */
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
/* Restore the clobbered interrupt priority register to its original
* value. */
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* configASSERT_DEFINED */
/* Make PendSV and SysTick the lowest priority interrupts. */
/* 2、配置 PendSV 和 SysTick 的中断优先级为最低优先级 */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
* here already. */
/* 3、调用函数 vPortSetupTimerInterrupt()配置 SysTick,初始化滴答定时器,设置滴答定时器的中断频率 */
vPortSetupTimerInterrupt();
/* Initialise the critical nesting count ready for the first task. */
/* 4、初始化临界区嵌套计数器为 0 */
uxCriticalNesting = 0;
/* Ensure the VFP is enabled - it should be anyway. */
/* 5、调用函数 prvEnableVFP()使能 FPU,ARM Cortex-M3内核无FPU,仅M4/M7才有此行代码 */
prvEnableVFP();
/* Lazy save always. */
/* 将寄存器的30和31位置1,在进出异常时,自动保存和恢复FPU相关寄存器 */
/* FPU寄存器有32位寄存器,S0~S31,S0~S15自动保存和恢复,S16~S31需要手动恢复和保存 */
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
/* Start the first task. */
/* 6、调用函数 prvStartFirstTask()启动第一个任务 */
prvStartFirstTask();
/* Should not get here! */
return 0;
}
滴答定时器配置函数vPortSetupTimerInterrupt():
portNVIC_SYSTICK_LOAD_REG是滴答定时器的重装载值,寄存器地址是0xe000e014;configSYSTICK_CLOCK_HZ 是CPU时钟,这里使用STM32F429,CPU时钟是180MHz,系统时钟也是180M;configTICK_RATE_HZ是在FreeRTOSConfig.h中设置的系统时钟节拍1000;portNVIC_SYSTICK_LOAD_REG 等于configSYSTICK_CLOCK_HZ/configTICK_RATE_HZ=180000,当数至0时,触发中断;滴答定时器时钟源通过portNVIC_SYSTICK_CTRL_REG控制寄存器设置,地址是0xe000e010,通过位2设置时钟源,如果此位为1,内核时钟180M,如果是外部时钟180M/8。portNVIC_SYSTICK_INT_BIT为1左移1位,倒数到0时产生SysTick异常请求。
程序中,滴答定时器时钟源为内部时钟180M,计数一次的时间为1/180M,滴答定时器为向下计数,计数时间为,也就是滴答定时器中断时间为1ms。
__weak void vPortSetupTimerInterrupt( void )
{
/* Calculate the constants required to configure the tick interrupt. */
#if ( configUSE_TICKLESS_IDLE == 1 )
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif /* configUSE_TICKLESS_IDLE */
/* Stop and clear the SysTick. */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
使能FPU寄存器函数prvEnableVFP()
函数中用到了汇编语言,在《Cortex M3权威指南(中文)》可以查到指令集的基本使用。在查找FPU寄存器使用时需要到《Cortex M3与M4权威指南》中查找,因为Cortex M3中无FPU寄存器。
__asm void prvEnableVFP( void )
{
/* *INDENT-OFF* */
PRESERVE8
/* The FPU enable bits are in the CPACR. */
ldr.w r0, =0xE000ED88
ldr r1, [ r0 ]
/* Enable CP10 and CP11 coprocessors, then save back. */
orr r1, r1, #( 0xf << 20 )
str r1, [ r0 ]
bx r14
nop
/* *INDENT-ON* */
}
将FPU寄存器使能操作的位是C11和C10位,如果将C11和C10都设置为11,则为完全访问,使能FPU。
2.启动第一个任务
在开启任务调度器函数xPortStartScheduler()末尾,调用了启动第一个任务的函数prvStartFirstTask()。而prvStartFirstTask()函数中调用了vPortSVCHandler ()SVC中断服务函数。
OS如何启动第一个任务?
如果创建了n个任务,从中选取优先级最高的任务执行。假设我们要启动的第一个任务是任务A,那么就需要将任务A的寄存器值恢复到CPU寄存器。任务A的寄存器值,在一开始创建任务时就保存在任务堆栈里边。
注意:
1、中断产生时,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0保存和恢复(进入中断保存到任务堆栈是入栈,出中断时寄存器从堆栈中恢复到到CPU寄存器); 而R4~R11需要手动保存和恢复。
2、进入中断后硬件会强制使用MSP指针 ,此时LR(R14)的值将会被自动被更新为特殊的值EXC_RETURN
关于以上提到的寄存器,可查看《 Cortex M3权威指南(中文) 》第37页。
2.1prvStartFirstTask ()
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
/* 首先进行8字节对齐 */
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08 /* 0xE000ED08为VTOR地址 */
ldr r0, [ r0 ] /* 获取VTOR的值,通过地址来找到向量表存储的地址 */
ldr r0, [ r0 ] /* 获取MSP的初始值,通过向量表存储的地址获取第一个元素 */
/* Set the msp back to the start of the stack. */
/* 初始化MSP,将r0赋值给msp */
msr msp, r0
/* Clear the bit that indicates the FPU is in use in case the FPU was used
* before the scheduler was started - which would otherwise result in the
* unnecessary leaving of space in the SVC stack for lazy saving of FPU
* registers. */
mov r0, #0
msr control, r0
/* Globally enable interrupts. */
/* 使能全局中断,打开中断 */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
/* 调用SVC启动第一个任务 */
svc 0
nop
nop
/* *INDENT-ON* */
}
函数用于初始化启动第一个任务前的环境,主要是重新设置MSP 指针,并使能全局中断(触发SVC中断)。
prvStartFirstTask()第一行代码首先进行了8字节对齐,这是因为栈在任何时候都需要4字节对齐,而在调用入口需要8字节对齐。 在进行C编程的时候,编译器会自动完成对齐操作,而对于汇编则需要开发者手动进行对齐。
后三行是为了获取MSP的初始值,0xE000ED08地址是向量表的寄存器地址。向量表在startup_stm32f429xx.s中,向量表的第一个成员是__initial_sp,也就是MSP的初始值,向量表的起始处都必须包含主堆栈指针(MSP)的初始值、复位向量、NMI、硬fault服务例程。
为什么要将向量表第一个成员赋值给MSP?
因为上电时默认使用MSP来做主堆栈指针,从上电跑到这里,经过一系列的函数调用、出/入栈,MSP不是一开始的位置,重新将初始值值赋值给MSP,MSP回到原点。之前使用的寄存器就不再需要保存,直接丢掉,因为现在不需要回去,启动了第一个任务后,后面的执行都在任务和任务之间,这是一条不归路。
2.1.1什么是MSP指针?
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时, MCU 会自动更新 SP 指针(也就是R13),ARM Cortex-M 内核提供了两个栈空间,:
主堆栈指针(MSP):它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。
在FreeRTOS中,中断使用MSP(主堆栈),中断以外使用PSP(进程堆栈)。在FreeRTOS中使用的是双堆栈指针,裸机无论是进程还是中断都是使用的MSP主堆栈指针。
2.1.2为什么是 0xE000ED08?
因为需从 0xE000ED08 获取向量表的偏移,为啥要获得向量表呢?因为向量表的第一个是 MSP 指针!
取 MSP 的初始值的思路是先根据向量表的位置寄存器 VTOR (0xE000ED08) 来获取向量表存储的地址; 在根据向量表存储的地址,来访问第一个元素,也就是初始的 MSP。
CM3 允许向量表重定位——从其它地址处开始定位各异常向量 这个就是向量表偏移量寄存器,向量表的起始地址保存的就是主栈指针MSP 的初始值
2.2 vPortSVCHandler ()
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
/* 首先进行8字节对齐 */
PRESERVE8
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB /* 获取优先级最高的任务控制块地址 */
ldr r1, [ r3 ] /* 通过任务控制块的地址获取其成员的首地址 */
ldr r0, [ r1 ] /* 通过首成员的地址获取首成员的值 */
/* Pop the core registers. */
ldmia r0!, {r4-r11,r14} /* 出栈指令,从R0地址开始,将值赋值给后面的寄存器,出栈到CPU寄存器 */
msr psp, r0 /* 将R0的值赋值给PSP,因为退出中断时,从PSP自动恢复剩下的寄存器 */
isb
/* 使能所有中断 */
mov r0, #0 /* 将r0等于0 */
msr basepri, r0 /* 将0赋值给中断屏蔽寄存器,为0时开启所有中断 */
bx r14 /* 跳转到任务的任务函数中执行,也就是跳转到PC指向任务函数地址去执行 */
/* *INDENT-ON* */
}
前面提到只需要将R4-R11手动恢复,但是这里多了一个R14,这里Cortex M3系列是没用R14的出栈操作的,Cortex M3最后多了一行代码“orr r14, # oxd”,将R14或上0x0d,退出中断时使用PSP进程堆栈指针。由于M4、M7支持FPU寄存器,会有R14寄存器被赋值特殊值EXC_RETURN,该特殊值不同的值会有不同的意义。EXC_RETURN只有6个合法的值(M4、M7,M3只有3个合法值,可以在《Cortex M3与M4权威指南》中找到),如下表所示
描述 | 使用浮点单元(M4、M7支持,M3不支持) | 未使用浮点单元 |
---|---|---|
中断返回后进入Hamdler模式,并使用MSP | 0xFFFFFFE1 | 0xFFFFFFF1 |
中断返回后进入线程模式,并使用 MSP | 0xFFFFFFE9 | 0xFFFFFFF9 |
中断返回后进入线程模式,并使用 PSP | 0xFFFFFFED | 0xFFFFFFFD |
恢复R14的值是为了判别是否使用浮点单元。注意:SVC中断只在启动第一次任务时会调用一次,以后均不调用,后面的任务切换都在PendSV中实现。
函数实现步骤:
1. 通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务是系统将要运行的任务 。
2. 通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已初始化,然后设置 PSP 指针 。
3. 通过往 BASEPRI 寄存器中写 0,允许中断。
4. R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN
2.2.1出栈/压栈汇编指令详解
1、出栈(恢复现场,从内存里将地址的值恢复到CPU,CPU寄存器就会执行),方向:从下往上(低地址往高地址):假设r0地址为0x04汇编指令示例:
ldmia r0!, {r4-r6} /* 任务栈r0地址由低到高,将r0存储地址里面的内容手动加载到 CPU寄存器r4、r5、r6 */
r0地址(0x04)内容加载到r4,此时地址r0 = r0+4 = 0x08
r0地址(0x08)内容加载到r5,此时地址r0 = r0+4 = 0x0C
r0地址(0x0C)内容加载到r6,此时地址r0 = r0+4 = 0x10
2、压栈(保存现场,将CPU寄存器保存到内存里),方向:从上往下(高地址往低地址):假设r0地址为0x10汇编指令示例:
stmdb r0!, {r4-r6} } /* r0的存储地址由高到低递减,将r4、r5、r6里的内容存储到r0的任务栈里面。 */
地址:r0 = r0-4 = 0x0C,将r6的内容(寄存器值)存放到r0所指向地址(0x0C)
地址:r0 = r0-4 = 0x08,将r5的内容(寄存器值)存放到r0所指向地址(0x08)
地址:r0 = r0-4 = 0x04,将r4的内容(寄存器值)存放到r0所指向地址(0x04)
3.任务切换
任务切换的本质:就是CPU寄存器的切换。
假设当由任务A切换到任务B时,主要分为两步:
第一步:需暂停任务A的执行,并将此时任务A的寄存器(CPU寄存器的值)保存到任务堆栈,这个过程叫做保存现场;
第二步:将任务B的各个寄存器值(被存于任务B的任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;
对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换(可以理解为任务切换)
任务A在运行时,CPU寄存器中是A的各个寄存器;任务B抢占时,第一步需要将任务A寄存器保存,将CPU中任务A的各个寄存器保存到任务A的任务堆栈中,下次恢复任务A运行时,将任务A的任务堆栈中的寄存器恢复到CPU寄存器中,接着被打断的点继续运行;第二步将任务B的寄存器值恢复到CPU寄存器中;任务切换后,CPU寄存器中就是B的各个寄存器。
注意:任务切换的过程在PendSV中断服务函数里边完成
3.1PendSV中断如何触发?
1、滴答定时器中断调用(判断符合条件时,触发PendSV中断)
2、执行FreeRTOS提供的相关API函数:portYIELD()(只要调用此函数就会触发PendSV中断,FreeRTOS提供了很多API函数,这些函数都是一些宏定义,主要还是调用此函数)
本质:通过向中断控制和状态寄存器 ICSR 的bit28 写入 1 挂起 PendSV 来启动 PendSV 中断
3.1.1滴答定时器中断
在开始任务调度器后调用函数xPortSysTickHandler(),
//systick中断服务函数,使用OS时用到
void SysTick_Handler(void)
{
HAL_IncTick();
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED) //OS开始跑了,才执行正常的调度处理
{
xPortSysTickHandler();
}
}
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
* executes all interrupts must be unmasked. There is therefore no need to
* save and then restore the interrupt mask value as its value is already
* known - therefore the slightly faster vPortRaiseBASEPRI() function is used
* in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
vPortRaiseBASEPRI();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
* the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR();
}
#define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
0xe000ed04是中断控制及状态寄存器地址,将第28位写入1将挂起PendSV启动PendSV中断,当if条件满足时,触发PendSV中断。xTaskIncrementTick()函数中有两种情况,需要任务切换,一个是阻塞超时时间到了,任务从阻塞列表中解除,挂载到就绪列表中,如果挂载到就绪列表中的任务比当前正在执行的任务优先级高,则需任务切换;还有一个是时间片调度,就绪列表中存在同等优先级的任务,也需任务切换。
3.1.2执行FreeRTOS相关API函数
虽然有众多API函数都可触发PendSV中断,但是本质都是下列函数:
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* Barriers are normally not required but do ensure the code is completely \
* within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
3.2PendSV中断服务函数实现步骤
首先进行8字节对齐
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* *INDENT-OFF* */
PRESERVE8
将psp任务栈赋值给r0, psp是在中断以外所使用的堆栈指针,中断服务函数以内是MSP主堆栈指针。由于是在中断服务函数PendSV中,所以使用的是MSP,外面任务运行用的是PSP。
mrs r0, psp /* 将psp的值赋值给r0 */
isb
通过以下两部获取了栈顶指针的地址。栈顶指针保存的是栈顶地址,底压栈时,psp会不断往下移,直至栈顶,将地址赋值给栈顶指针,寄存器恢复时只要从栈顶指针往上恢复即可。
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB /* r3等于当前正在运行任务控制块的地址 */
ldr r2, [ r3 ] /* 从地址取值,获取到任务控制块栈顶指针地址 */
浮点单元有32个寄存器S0~S31,S0~S15自动保存和恢复,S16~S31手动保存和恢复。
/* Is the task using the FPU context? If so, push high vfp registers. */
tst r14, #0x10 /* 判断位4是否为1,如果为1则不使用浮点数,为0使用浮点数 *、
it eq
vstmdbeq r0!, {s16-s31} /* 如果使用寄存器压栈保存寄存器s16-s31 */
从R0地址开始,将R4~R11和R14的值保存到任务栈中。
/* Save the core registers. */
stmdb r0!, {r4-r11, r14}
将R0地址写入到R2所指向的内存中,R2前面已经说了,指向的是栈顶指针的地址,所以将R0写入到栈顶指针内容中去。
/* Save the new top of stack into the first member of the TCB. */
str r0, [ r2 ]
stmdb sp!, {r0, r3} /* 将R0,R3进行压栈,sp就是msp */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 将宏的值5写入R0 */
msr basepri, r0 /* 将r0写入中断屏蔽寄存器,关闭5~15中断优先级的中断 */
dsb
isb
bl vTaskSwitchContext /* 通过函数vTaskSwitchContext查找下一个要运行的任务的任务控制块 */
mov r0, #0 /* 将r0清0 */
msr basepri, r0 /* 将r0赋值给中断屏蔽寄存器,打开中断 */
ldmia sp!, {r0, r3} /* 出栈,*/
/* The first item in pxC urrentTCB is the task top of stack. */
ldr r1, [ r3 ] /* 取出r3地址中的值 */
ldr r0, [ r1 ] /* 取出r1地址中的值,R0获取的是栈顶地址 */
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14} /* 将r0地址中的值出栈,恢复到r4~r11,r14 */
/* Is the task using the FPU context? If so, pop the high vfp registers
* too. */
tst r14, #0x10 /* 判断r14的bit4是否被置1,为1则不使用浮点数,为0则使用浮点数 */
it eq
vldmiaeq r0!, {s16-s31} /* 如果使用则出栈操作 */
msr psp, r0 /* 将r0的值赋值给psp,自动将剩余寄存器出栈 */
isb
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif
bx r14 /* 返回r14 */
/* *INDENT-ON* */
}
3.2.1 查找最高优先级任务
函数vTaskSwitchContext()是通过调用函数taskSELECT_HIGHEST_PRIORITY_TASK(),函数taskSELECT_HIGHEST_PRIORITY_TASK()又调用函数portGET_HIGHEST_PRIORITY()找到的最高优先级的任务,内容如下:
#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 ] ) \); /* 获取当前任务最高优先级的任务控制块 */ \ *
}
在之前文章中提到过,任务优先级是0~31,对应32个就绪列表,就绪列表的序号也是0~31,和任务优先级相对应,每一个就绪列表都用一个位来表示其中有没有任务挂载其中,如果有则为1,反之为0。32位的值就保存在变量uxReadyPriorities中,函数__clz()是前导置0指令,此处是通过硬件的方式查找任务最高优先级。
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
3.2.2前导置0指令
左侧是一个32位的变量表示就绪列表,只要列表中挂载任务,则该优先级的就绪列表的标志位置1,前导置0指令此时为3,因为最高位前面有3个0。
再举个例子,如下图所示,由于第30位为1,前面有一位为0,则前导置0 指令结果为1。所谓的前导置0指令,大家可以简单理解为计算一个 32位数,头部 0 的个数 。通过前导置0指令获得任务的最高优先级。
3.2.3获取最高优先级任务的任务控制块
参数pxList是最高优先级的就绪列表,将就绪列表赋值给pxConstList。
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList );
就绪列表的pxIndex指针指向最高优先级就绪列表的pxIndex指针的下一个,一开始初始化列表时,pxIndex指针指向末尾列表项,也就是将pxIndex指针指向下图中的列表项1。
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
判断最高优先级就序列表pxConstList的pxIndex指针是否指向末尾列表项,否则不执行if中内容。
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
}
将当前最高优先级任务的所属任务控制块,赋值给pxTCB。
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
假如又触发了一次PendSV中断,此时又需要进行一次任务切换,此时就绪列表的pxIndex指针指向列表项1,则指向列表项1的下一个末尾列表项。此时的if条件成立,进入程序将就绪列表的pxIndex指针更换成下一个,还是指向列表项1。以上是当列表中仅有一个任务时。
当最高优先级就绪列表中有多个任务时,指针会先指向1,判断列表项1是否等于末尾列表项,否将返回任务1的任务控制块;同理指向列表项2,返回列表项2的任务控制块,;下一次将指向末尾列表项,if条件成立则更换pxIndex指针指向列表项1。最终在步骤②和③之间循环,也就是时间片调度,每触发一次PendSV中断,将同等优先级的任务轮流执行。
4.总结