一、概述
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( ; ; )
{
prvCheckTasksWaitingTermination();
#if ( configUSE_PREEMPTION == 0 )
{
taskYIELD();
}
#endif
#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )
{
taskYIELD();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
#if ( configUSE_IDLE_HOOK == 1 )
{
extern void vApplicationIdleHook( void );
vApplicationIdleHook();
}
#endif
#if ( configUSE_TICKLESS_IDLE != 0 )
{
}
#endif
}
}
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;
volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
ulOriginalPriority = *pucFirstUserPriorityRegister;
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
#ifdef __NVIC_PRIO_BITS
{
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif
#ifdef configPRIO_BITS
{
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
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 )
{
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
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 );
}
FreeRtos将SysTick的中断函数重新定义:
#define xPortSysTickHandler SysTick_Handler
当中断开启之后,系统会每隔一个时钟节拍执行一次xPortSysTickHandler函数。
2.5、启动第一个任务
FreeRtos任务的启动和切换依赖于SVC(系统服务调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。 SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。
SVC异常是必须立即得到响应的, PendSV 则不同,它是可以像普通的中断一样被悬起的。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。 悬起 PendSV 的方法是: 手工往 NVIC 的 PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。
vPortSVCHandler:
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11, r14}
msr psp, r0
isb
mov r0, #0
msr basepri, r0
bx r14
vPortStartFirstTask
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
msr msp, r0
mov r0, #0
msr control, r0
cpsie i
cpsie f
dsb
isb
svc 0
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)