一、概述
FreeRtos在创建任务之后,需要启动任务调度器才能使任务正常有序的运行。任务调度器的开启依赖于底层硬件,对于CortexM3内核而言,任务调度器需要用到中断和滴答定时器。FreeRtos在对中断优先级、空闲任务等进行初始化之后,会开启滴答定时器的中断,这样每隔1毫秒系统就会进入滴答定时器中断,FreeRtos会在这个中断中进行诸如记录系统运行时间、查找下一个就绪态任务等操作。换句话说,滴答定时器就是FreeRtos的心脏。
FreeRtos任务调度需要用到CortexM3和汇编知识,可以结合CorteM3权威指南进行学习。
二、FreeRtos任务调度开启流程
FreeRtos调用vTaskStartScheduler函数开启任务调度,流程如下:
- [1]初始化空闲任务
- [2]创建定时器管理任务
- [3]中断初始化
- [4]滴答定时器初始化
- [5]调用vPortStartFirstTask函数出发svc中断
- [6]在SVC中断函数vPortSVCHandler中启动第一个任务
2.1、空闲任务
- 当没有其它任务运行时,FreeRtos会自动运行空闲任务。空闲任务的优先级默认为0,在所有的任务中优先级最低。即使有和空闲任务相同优先级的任务被创建,系统依然会优先运行非空闲任务的其它任务。
空闲任务会回收调用vTaskDelete(NULL)删除自己任务的内存,避免内存泄漏。 - 空闲任务会不停的监测是否有新的任务处于就绪状态,如果有则进行任务切换
- 空闲任务可以用于实现系统的低功耗,其原理是在系统空闲时间进入硬件低功耗模式。
空闲任务的功能总结如下:
- 回收已删除任务的内存;
- 检查是否需要进行任务切换
- 调用vApplicationIdleHook函数,执行用户自定义代码
- 可以用于实现系统的低功耗功能
空闲任务源码:
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
/* 防止编译警告. */
( void ) pvParameters;
/** 空闲任务,在任务调度器启动时自动创建. **/
/*如果具有安全上下文的任务删除了自己,在这种情况下,空闲任务负责删除任务的安全上下文(如果有). */
portALLOCATE_SECURE_CONTEXT( configMINIMAL_SECURE_STACK_SIZE );
for( ; ; )
{
/* 如果有任务删除了自己,空闲任务负责释放被删除任务的TCB和内存。 */
prvCheckTasksWaitingTermination();
#if ( configUSE_PREEMPTION == 0 )
{
/*如果没有使用抢占式调度器,只要有可用的其它任务就进行任务切换*/
taskYIELD();
}
#endif /* configUSE_PREEMPTION */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
{
/* 如果使用抢占式调度的任务具有相同的优先级,那么它们将会按照时间片进行调度。
* 如果有任务和空闲任务具有相同的优先级,空闲任务应该在时间片耗尽之前进行调度。
* 由于我们只是从列表中读取内容,因此此处不需要关键区域,偶尔出现的错误值也无关紧要。
* 如果处于空闲优先级的就绪列表包含多个任务,则准备执行非空闲任务以外的任务。*/
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )//如果就绪列表中的任务数量大于1则执行任务切换
{
taskYIELD();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) */
#if ( configUSE_IDLE_HOOK == 1 )
{
extern void vApplicationIdleHook( void );
/* 调用用户自定义的钩子函数。这个函数禁止调用可能阻塞的函数。 */
vApplicationIdleHook();
}
#endif /* configUSE_IDLE_HOOK */
/* configUSE_TICKLESS_IDLE表示是否开启低功耗模式,为0表示关闭,非0表示打开. */
#if ( configUSE_TICKLESS_IDLE != 0 )
{
//此处省略低功耗部分代码
}
#endif /* configUSE_TICKLESS_IDLE */
}
}
2.2、定时器任务
定时器任务也在任务调度开始前创建,其优先级为2,略高于空闲任务,主要用来对FreeRtos软件定时器进行管理。
2.4、中断初始化中断
CortexM3和CortexM4的每一个外部中断都有一个对应的优先级寄存器,每个寄存器占用8位,但是最少允许使用最高3位。 4 个相临的优先级寄存器拼成一个 32 位寄存器,分别是抢占优先级和响应(亚)优先级。这些寄存器可以按照字节、字或者半字访问。 如下图:
stm32f4xx系列芯片使用高4位用来配置优先级,优先级取值范围为0-15。低4位始终为0,用户无法修改,FreeRtos使用这个特性来检验优先级中断寄存器的可用位数。
FreeRtos中断初始化流程:
- [1]读取地址为0xE000E400优先级寄存器的值;
- [2]将0xFF写入该优先级寄存器;
- [3]读取该优先级寄存器地址的内容;
- [4]判断读取到的值有多少位为1从而确定实际可用的优先级位数;
- [5]使用读出来的值和stm32f4芯片实际的可用优先级进行比较,避免出错;
- [6]恢复0xE000E400优先级寄存器访问之前的值;
- [7]将PendSV和SysTick的优先级设为最低;
- [8]初始化SysTick定时器
源码:
#if ( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
//计算后得到的值为0xE000E400,表示外部中断优先级寄存器的地址
volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
/* 保存即将被破坏的优先级值,后面会修改寄存器的值. */
ulOriginalPriority = *pucFirstUserPriorityRegister;
/* 确定有多少优先级位可用,不可用的位始终为0,通过将寄存器所有位置1,再读取寄存器的值来确定. */
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
/* Read the value back to see how many bits stuck. */
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* Use the same mask on the maximum system call priority. */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
/* 根据读回来的值确定最大优先级组的值 ,比如读出来的值是0xf0,那么表示最高4位用来表示优先级*/
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
#ifdef __NVIC_PRIO_BITS
{
/*检查CMIS配置的优先级位数是否和实际得到的硬件优先级位数是否相同*/
/*stm32f4使用了高4位作为可配置的优先级位*/
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;
/* 将中断寄存器的值复原*/
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* conifgASSERT_DEFINED */
/* 使 PendSV and SysTick 的优先级最低. */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/* 初始化SysTick定时器. 此时中断已经被关闭. */
vPortSetupTimerInterrupt();
2.4、初始化SysTick定时器
CortexM3和CortexM4都具有SysTick定时器,它被捆绑在 NVIC中,用于产生SYSTICK异常(异常号: 15)。
SysTick定时器有四个寄存器,相关定义如下:
SysTick定时器重装载数值寄存器的初始值计算公式:
t=reload*(1/clock)
计数值为:
reload = clock*t-1
减一是因为计数值从0开始。比如stm32f407的系统时钟是168MHz,如果我们希望FreeRtos的时钟节拍设置为1ms,那么得到reload的值为168000000*(1/1000)-1
FreeRtos对SysTick初始化的源码如下:
__weak void vPortSetupTimerInterrupt( void )
{
/* Stop and clear the SysTick. */
portNVIC_SYSTICK_CTRL_REG = 0UL;//复位SysTick控制寄存器
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;//设置重装载数值寄存器值
//使用内部时钟、SysTick倒数至0时产生中断、使能SysTick
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
FreeRtos将SysTick的中断函数重新定义:
#define xPortSysTickHandler SysTick_Handler
当中断开启之后,系统会每隔一个时钟节拍执行一次xPortSysTickHandler函数。
2.5、启动第一个任务
FreeRtos任务的启动和切换依赖于SVC(系统服务调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。 SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。
SVC异常是必须立即得到响应的, PendSV 则不同,它是可以像普通的中断一样被悬起的。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。 悬起 PendSV 的方法是: 手工往 NVIC 的 PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。
vPortSVCHandler:
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB//将pxCurrentTCB放入R3寄存器中
ldr r1, [r3] //将pxCurrentTCB的内容(*pxCurrentTCB)放入R1寄存器,这个值是pxCurrentTCB结构体的第一个元素pxTopOfStack
ldr r0, [r1] //将pxCurrentTCB结构体的第一个元素pxTopOfStack存储的内容放到R0中
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14} //出栈,从R4-R11,R14中一次性读出9个寄存器的数据
msr psp, r0
isb //指令同步隔离。它会清洗流水线,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令。
mov r0, #0 //将立即数0传送到R0寄存器
msr basepri, r0/*将basepri寄存器所有位写0使能中断*/
bx r14 //跳转指令
/*-----------------------------------------------------------*/
vPortStartFirstTask
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08/*向量表偏移量寄存器(VTOR)*/
ldr r0, [r0] //将r0存储地址(0xE000ED08)的内容放入r0
ldr r0, [r0]
/* Set the msp back to the start of the stack. */
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
/* Call SVC to start the first task. */
cpsie i //开中断
cpsie f //开异常
dsb
isb
svc 0 //触发SVC中断