FreeRTOS 任务调度

开启任务调度器

vTaskStartScheduler()作用:

用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度 

该函数内部实现,如下:

  1. 创建空闲任务: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 */
  2. 如果使能软件定时器,则创建定时器任务。创建软件定时器任务:xTimerCreateTimerTask
     #if ( configUSE_TIMERS == 1 )
            {
                if( xReturn == pdPASS )
                {
                    xReturn = xTimerCreateTimerTask();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
     #endif /* configUSE_TIMERS */
  3. 关中断(在启动第一个任务时开启)防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
    /* 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();
  4. 初始化全局变量,并将任务调度器的运行标志设置为已运行
       xNextTaskUnblockTime = portMAX_DELAY;
       xSchedulerRunning = pdTRUE;
       xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
    
  5. 初始化任务运行时间统计功能的时基定时器
    /* 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();
  6. 调用函数 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() 作用:

该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务

 该函数内部实现,如下:

  1. 检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误 
  2. 配置 PendSVSysTick 的中断优先级为最低优先级
    /* Make PendSV and SysTick the lowest priority interrupts. */
     portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    
     portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
  3. 调用函数 vPortSetupTimerInterrupt ( ) 配置 SysTick
    /* Start the timer that generates the tick ISR.  Interrupts are disabled
     * here already. */
    vPortSetupTimerInterrupt();
    
  4. 初始化临界区嵌套计数器为 0
     /* Initialise the critical nesting count ready for the first task. */
     uxCriticalNesting = 0;
  5. 调用函数 prvEnableVFP() 使能 FPU(F1系列没有该功能)
  6. 调用函数 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 ( )作用: 

SVC中断服务函数 ;当使能了全局中断,并且手动触发 SVC 中断后,就会进入到 SVC 的中断服务函数中
  1. 通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务是系统将要运行的任务
  2. 通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已初始化,然后设置 PSP 指针 。
  3. 通过往 BASEPRI 寄存器中写 0,允许中断。 
  4. 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;                                         
    }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值