FreeRtos源码分析之启动任务调度vTaskStartScheduler(二)

一、概述

FreeRtos在创建任务之后,需要启动任务调度器才能使任务正常有序的运行。任务调度器的开启依赖于底层硬件,对于CortexM3内核而言,任务调度器需要用到中断和滴答定时器。FreeRtos在对中断优先级、空闲任务等进行初始化之后,会开启滴答定时器的中断,这样每隔1毫秒系统就会进入滴答定时器中断,FreeRtos会在这个中断中进行诸如记录系统运行时间、查找下一个就绪态任务等操作。换句话说,滴答定时器就是FreeRtos的心脏。
FreeRtos任务调度需要用到CortexM3和汇编知识,可以结合CorteM3权威指南进行学习。

二、FreeRtos任务调度开启流程

FreeRtos调用vTaskStartScheduler函数开启任务调度,流程如下:

  • [1]初始化空闲任务
  • [2]创建定时器管理任务
  • [3]中断初始化
  • [4]滴答定时器初始化
  • [5]调用vPortStartFirstTask函数出发svc中断
  • [6]在SVC中断函数vPortSVCHandler中启动第一个任务

2.1、空闲任务

  1. 当没有其它任务运行时,FreeRtos会自动运行空闲任务。空闲任务的优先级默认为0,在所有的任务中优先级最低。即使有和空闲任务相同优先级的任务被创建,系统依然会优先运行非空闲任务的其它任务。
    空闲任务会回收调用vTaskDelete(NULL)删除自己任务的内存,避免内存泄漏。
  2. 空闲任务会不停的监测是否有新的任务处于就绪状态,如果有则进行任务切换
  3. 空闲任务可以用于实现系统的低功耗,其原理是在系统空闲时间进入硬件低功耗模式。

空闲任务的功能总结如下:

  • 回收已删除任务的内存;
  • 检查是否需要进行任务切换
  • 调用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 位寄存器,分别是抢占优先级和响应(亚)优先级。这些寄存器可以按照字节、字或者半字访问。 如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LiMR8DQb-1611110514033)(en-resource://database/588:1)]

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定时器有四个寄存器,相关定义如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nmJQEjMw-1611110514035)(en-resource://database/590:1)]

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中断

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值