精华内容
下载资源
问答
  • freertos任务调度原理
    万次阅读
    2016-10-15 17:07:34

    @(嵌入式)

    Freertos
    FreeRtos

    简述

    前面文章 < FreeRTOS 任务调度 任务创建 > 介绍了 FreeRTOS 中如何创建任务以及其具体实现。
    一般来说, 我们会在程序开始先创建若干个任务, 而此时任务调度器还没又开始运行,因此每一次任务创建后都会依据其优先级插入到就绪链表,同时保证全局变量 pxCurrentTCB 指向当前创建的所有任务中优先级最高的一个,但是任务还没开始运行。
    当初始化完毕后,调用函数 vTaskStartScheduler启动任务调度器开始开始调度,此时,pxCurrentTCB所指的任务才开始运行。
    所以, 本章,介绍任务调度器启动以及如何进行任务切换。

    调度器涉及平台底层硬件操作,本文以Cotex-M3 架构为例, 具体可以参考 《Cortex-M3权威指南》(文末附)

    分析的源码版本是 v9.0.0
    (为了方便查看,github 上保留了一份源码Source目录下的拷贝)

    启动调度器

    创建任务后,系统不会自动启动任务调度器,需要用户调用函数 vTaskStartScheduler 启动调度器。 该函数被调用后,会先创建系统自己需要用到的任务,比如空闲任务 prvIdleTask,定时器管理的任务等。 之后, 调用移植层提供的函数 xPortStartScheduler
    代码解析如下,

    void vTaskStartScheduler( void )
    {
        BaseType_t xReturn;
        #if( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            // 采用静态内存创建空闲任务
            StaticTask_t *pxIdleTaskTCBBuffer = NULL;
            StackType_t *pxIdleTaskStackBuffer = NULL;
            uint32_t ulIdleTaskStackSize;
            // 获取静态内存地址/参数
            vApplicationGetIdleTaskMemory(
                &pxIdleTaskTCBBuffer, 
                &pxIdleTaskStackBuffer, 
                &ulIdleTaskStackSize );
            // 创建任务
            // 空闲任务优先级为 0, 也就是其优先级最低
            // !! 但是, 设置了特权位, 所以其运行在 特权模式
            xIdleTaskHandle = xTaskCreateStatic(prvIdleTask, "IDLE", 
                ulIdleTaskStackSize, (void *) NULL, 
                (tskIDLE_PRIORITY | portPRIVILEGE_BIT), 
                pxIdleTaskStackBuffer,
                pxIdleTaskTCBBuffer); 
    
            if( xIdleTaskHandle != NULL )
            {
                xReturn = pdPASS;
            }
            else
            {
                xReturn = pdFAIL;
            }
        }
        #else
        {
            // 动态申请内存创建任务
            xReturn = xTaskCreate(prvIdleTask,
                "IDLE", configMINIMAL_STACK_SIZE,
                (void *)NULL,
                (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
                &xIdleTaskHandle );     
        }
        #endif
    
        // 如果工程使用了软件定时器, 需要创建定时器任务进行管理
        #if ( configUSE_TIMERS == 1 )
        {
            if( xReturn == pdPASS )
            {
                xReturn = xTimerCreateTimerTask();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif
    
        if( xReturn == pdPASS )
        {
    
            // 关闭中断, 避免调度器运行前节拍定时器产生中断
            // 中断在第一个任务启动时恢复
            portDISABLE_INTERRUPTS();
    
            #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                // 如果使用了这个库
                // 更新第一个任务的的指针到全局变量
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
            #endif
    
            // 初始化变量
            xNextTaskUnblockTime = portMAX_DELAY;
            xSchedulerRunning = pdTRUE;
            xTickCount = ( TickType_t ) 0U;
    
            // 如果启动统计任务运行时间, 宏 configGENERATE_RUN_TIME_STATS = 1
            // 需要定义以下宏, 初始化一个定时器用于该功能 
            portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
    
            // 设置系统节拍计数器, 启动任务
            // 硬件相关, 由系统移植层提供, 下面介绍
            if( xPortStartScheduler() != pdFALSE )
            {
                // 不会运行到这里, 如果调度器运行正常
            }
            else
            {
                // 当调用 xTaskEndScheduler()才会来到这里
            }
        }
        else
        {
            // 内存不足,创建空闲任务/定时任务失败, 调度器启动失败
            configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
        }
    
        // 预防编译器警告
        ( void ) xIdleTaskHandle;
    }

    移植层调度器

    上面提到, 创建系统所需任务和初始化相关静态变量后, 系统调用了 xPortStartScheduler设置节拍定时器和启动第一个任务,开始系统正常运行调度。 而对于不同架构平台,该函数的实现可能存在不同,以下, 拿比较常用的 Cotex-M3 架构举例。
    对于 M3, 可以在源码目录下 /Source/portable/GCC/ARM_CM3/port.c 看到该函数的实现。

    与 FreeRTOS 任务优先级相反, Cotex-M3 优先级值越小, 优先级越高。 Cotex-M3的优先级配置寄存器考虑器件移植而向高位对齐,实际可用的 CPU 会裁掉表达优先级低端的有效位,以减少优先级数。 举例子说, 加入平台支持3bit 表示优先级,则其优先级配置寄存器的高三位可以编程写入,其他位被屏蔽,不管写入何值,重新读回都是0。
    另外提供抢占优先级和子优先级分段配置相关,详细阅读 《Cortex-M3权威指南》

    在系统调度过程中,主要涉及到的三个异常:
    * SVC 系统服务调用
    操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数。 这里主要触发后,在异常服务中启动第一个任务
    * PendSV 可悬起系统调用
    相比 SVC, PenndSV 异常后可能不会马上响应, 等到其他高优先级中断处理后才响应。 用于上下文切换,同时保证其他中断可以被及时响应处理。
    * SysTick 节拍定时器
    在没有高优先级任务强制下,同优先级任务按时间片轮流执行,每次SysTick中断,下一个任务将获得一个时间片。

    BaseType_t xPortStartScheduler( void )
    {
        configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
        #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;
    
            // 判断平台支持优先级位数
            // 先全写 1
            *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
            // 重新读回, 不能设置的位依然是 0
            ucMaxPriorityValue = *pucFirstUserPriorityRegister;
            // 确保用户设置优先级不会超出范围
            ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
    
            // 判断有几个1, 得到对应优先级数最大值
            ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
            while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
            {
                ulMaxPRIGROUPValue--;
                ucMaxPriorityValue <<= ( uint8_t ) 0x01;
            }
            ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
            ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
    
            // 恢复优先级配置寄存器值
            *pucFirstUserPriorityRegister = ulOriginalPriority;
        }
        #endif /* conifgASSERT_DEFINED */
    
        // 设置 PendSV 和 SysTIck 异常优先级最低
        // 保证系统会话切换不会阻塞系统其他中断的响应
        portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
        portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    
        // 初始化系统节拍定时器
        vPortSetupTimerInterrupt();
        // 初始化边界嵌套计数器
        uxCriticalNesting = 0;
    
        // 触发 svc 异常 启动第一个任务
        prvPortStartFirstTask();
    
        /* Should not get here! */
        prvTaskExitError();
        return 0;
    }

    启动第一个任务

    函数中调用了 prvPortStartFirstTask 来启动第一个任务, 该函数重新初始化了系统的栈指针,表示 FreeRtos 开始接手平台的控制, 同时通过触发 SVC 系统调用,运行第一个任务。具体实现如下

    static void prvPortStartFirstTask( void )
    {
        __asm volatile(
        " ldr r0, =0xE000ED08   \n" /*向量表偏移寄存器地址 CotexM3*/
        " ldr r0, [r0]          \n" /*取向量表地址*/
        " ldr r0, [r0]          \n" /*取 MSP 初始值*/
        /*重置msp指针 宣示 系统接管*/
        " msr msp, r0           \n"
        " cpsie i               \n" /*开中断*/
        " cpsie f               \n" /*开异常*/
        /*流水线相关*/
        " dsb                   \n" /*数据同步隔离*/
        " isb                   \n" /*指令同步隔离*/
        /*触发异常 启动第一个任务*/
        " svc 0                 \n"
        " nop                   \n"
        );
    }

    前面创建任务的文章介绍过, 任务创建后, 对其栈进行了初始化,使其看起来和任务运行过后被系统中断切换了一样。 所以,为了启动第一个任务,触发 SVC 异常后,异常处理函数中直接执行现场恢复, 把 pxCurrentTCB “恢复”到运行状态。

    (另外,Cotex-M3 具有三级流水线,所以切换任务的时候需要清除预取的指令,避免错误。)

    对于 Cotex-M3 , 其代码实现如下,

    void vPortSVCHandler( void )
    {
        __asm volatile (
        /*取 pxCurrentTCB 的地址*/
        "ldr r3, pxCurrentTCBConst2      \n"
        /*取出 pxCurrentTCB 的值 : TCB 地址*/
        "ldr r1, [r3]                    \n" 
        /*取出 TCB 第一项 : 任务的栈顶 */
        "ldr r0, [r1]                   \n"
        /*恢复寄存器数据*/
        "ldmia r0!, {r4-r11}            \n" 
        /*设置线程指针: 任务的栈指针*/
        "msr psp, r0                    \n" 
        /*流水线清洗*/
        "isb                            \n"
        "mov r0, #0                     \n"
        "msr    basepri, r0             \n"
        /*设置返回后进入线程模式*/
        "orr r14, #0xd                  \n"
        "bx r14                         \n"
        "                               \n"
        ".align 4               \n"
        "pxCurrentTCBConst2: .word pxCurrentTCB     \n"
        );
    }

    异常返回后, 系统进入线程模式, 自动从堆栈恢复PC等寄存器,而由于此时栈指针已经更新指向对应准备运行任务的栈,所以,程序会从该任务入口函数开始执行。
    到此, 第一个任务启动。

    前面提到, 第一个任务启动通过 SVC 异常, 而后续的任务切换, 使用的是 PendSV 异常, 而其对应的服务函数是 xPortPendSVHandler。 后续介绍任务切换再分析。

    任务切换

    FreeRTOS 支持时间片轮序和优先级抢占。系统调度器通过调度算法确定当前需要获得CPU 使用权的任务并让其处于运行状态。对于嵌入式系统,某些任务需要获得快速的响应,如果使用时间片,该任务可能无法及时被运行,因此抢占调度是必须的,高优先级的任务一旦就绪就能及时运行;而对于同优先级任务,系统根据时间片调度,给予每个任务相同的运行时间片,保证每个任务都能获得CPU 。
    1. 最高优先级任务 Task 1 运行,直到其被阻塞或者挂起释放CPU
    2. 就绪链表中最高优先级任务Task 2 开始运行, 直到…
    1. 调用接口进入阻塞或者挂起状态
    2. 任务 Task 1 恢复并抢占 CPU 使用权
    3. 同优先级任务TASK 3 就绪,时间片调度
    3. 没有用户任务执行,运行系统空闲任务。

    FreeRTOS 在两种情况下执行任务切换:
    1. 同等级任务时间片用完,提前挂起触发切换
    在 SysTick 节拍计数器中断中触发异常
    2. 高优先任务恢复就绪(如信号量,队列等阻塞、挂起状态下退出)时抢占
    最终都是通过调用移植层提供的 portYIELD() 宏悬起 PendSV 异常

    但是无论何种情况下,都是通过触发系统 PendSV 异常,在该服务程序中完成切换。
    使用该异常切换上下文的原因是保证切换不会影响到其他中断的及时响应(切换上下文抢占了 ISR 的执行,延时时间不可预知,对于实时系统是无法容忍的),在SysTick 中或其他需要进行任务切换的地方悬起一个 PendSV 异常,系统会直到其他所有 ISR 都完成处理后才执行该异常的服务程序,进行上下文切换。

    系统响应 PendSV 异常,在该中断服务程序中,保存当前任务现场, 选择切换的下一个任务,进行任务切换,退出异常恢复线程模式运行新任务,完成任务切换。

    以下是 Cotex-M3 的服务程序,
    首先先要明确的是,系统进入异常处理程序的时候,使用的是主堆栈指针 MSP, 而一般情况下运行任务使用的线程模式使用的是进程堆栈指针 PSP。后者使用是系统设置的,前者是硬件强制设置的。
    对应这两个指针,系统有两种堆栈,系统内核和异常程序处理使用的是主堆栈,MSP 指向其栈顶。而对应而不同任务,我们在创建时为其分配了空间,作为该任务的堆栈,在该任务运行时,由系统设置进程堆栈 PSP 指向该栈顶。
    如下分析该服务函数的执行:

    void xPortPendSVHandler( void )
    {
        /* This is a naked function. */
        __asm volatile
        (
        /*取出当前任务的栈顶指针 也就是 psp -> R0*/
        "   mrs r0, psp                         \n"
        "   isb                                 \n"
        "                                       \n"
        /*取出当前任务控制块指针 -> R2*/
        "   ldr r3, pxCurrentTCBConst           \n"
        "   ldr r2, [r3]                        \n"
        "                                       \n"
        /*R4-R11 这些系统不会自动入栈,需要手动推到当前任务的堆栈*/
        "   stmdb r0!, {r4-r11}                 \n"
        /*最后,保存当前的栈顶指针 
        R0 保存当前任务栈顶地址
        [R2] 是 TCB 首地址,也就是 pxTopOfStack
        下次,任务激活可以重新取出恢复栈顶,并取出其他数据
        */
        "   str r0, [r2]                        \n"
        "                                       \n"
        /*保护现场,调用函数更新下一个准备运行的新任务*/
        "   stmdb sp!, {r3, r14}                \n"
        /*设置优先级 第一个参数,
        即:configMAX_SYSCALL_INTERRUPT_PRIORITY
        进入临界区*/
        "   mov r0, %0                          \n"
        "   msr basepri, r0                     \n"
        "   bl vTaskSwitchContext               \n"
        "   mov r0, #0                          \n"
        "   msr basepri, r0                     \n"
        "   ldmia sp!, {r3, r14}                \n"
        "                                       \n"
        /*函数返回 退出临界区
        pxCurrentTCB 指向新任务
        取出新的 pxCurrentTCB 保存到 R1
        */
        "   ldr r1, [r3]                        \n"
        /*取出新任务的栈顶*/
        "   ldr r0, [r1]                        \n"
        /*恢复手动保存的寄存器*/
        "   ldmia r0!, {r4-r11}                 \n"
        /*设置线程指针 psp 指向新任务栈顶*/
        "   msr psp, r0                         \n"
        "   isb                                 \n"
        /*返回, 硬件执行现场恢复
        开始执行任务
        */
        "   bx r14                              \n"
        "                                       \n"
        "   .align 4                            \n"
        "pxCurrentTCBConst: .word pxCurrentTCB  \n"
        ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
        );
    }

    在服务程序中,调用了函数 vTaskSwitchContext 获取新的运行任务, 该函数会更新当前任务运行时间,检查任务堆栈使用是是否溢出,然后调用宏 taskSELECT_HIGHEST_PRIORITY_TASK()设置新的任务。该宏实现分两种情况,普通情况下使用的定义如下

    UBaseType_t uxTopPriority = uxTopReadyPriority;
    while(listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))
    {
        --uxTopPriority;
    }
    
    listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, 
        &(pxReadyTasksLists[ uxTopPriority]));
    
    uxTopReadyPriority = uxTopPriority;

    通过 while 查找当前存在就绪任务的最高优先级链表,获取链表项设置任务指针。(通一个链表内多个项目通过指针循环,实现同优先级任务获得相同时间片执行)。

    而另外一种方式,需要平台支持,主要差别是查找最高任务优先级,平台支持利用平台特性,效率会更高,但是移植性就不好说了。

    发生异常跳转到异常处理服务前,自动执行的现场保护会保留返回模式(线程模式),使用堆栈指针等信息,所以,结束任务切换, 通过执行 bx r14返回,系统会自动恢复现场(From stack),开始运行任务。

    至此,任务切换完成。

    参考

    更多相关内容
  • 默认情况下,FreeRTOS使用固定优先级抢占式调度策略,并对同等优先级的任务进行循环时间切片: "固定优先级"意味着调度程序不会一直更改任务的优先级,但是由于优先级继承,它可能会暂时提高任务的优先级。 ...

    目录

    默认FreeRTOS调度策略(单核)

    FreeRTOS调度策略的实现

    任务创建

    任务调度的4种情景:

     1.第一次启动任务调度器 

     2.任务主动触发调度

     3.SystemTick时钟触发调度

     4.因为中断而引起的任务调度


    默认FreeRTOS调度策略(单核)

    默认情况下,FreeRTOS使用固定优先级抢占式调度策略,并对同等优先级的任务进行循环时间切片:

    • "固定优先级"意味着调度程序不会一直更改任务的优先级,但是由于优先级继承,它可能会暂时提高任务的优先级

    • "抢占式"意味着调度程序始终运行能够运行的最高优先级的 RTOS 任务,而不管任务何时能够运行。例如,如果中断服务例程 (ISR) 更改了能够运行的最高优先级任务,则计划程序将停止当前运行的较低优先级任务并启动优先级较高的任务 - 即使这发生在某个时间片内。在这种情况下,优先级较低的任务被称为已被优先级较高的任务"抢占"。

    • "轮循机制"是指共享优先级的任务轮流进入"正在运行"状态。

    • "时间切片"意味着调度程序将在每个系统中断时优先级相等的任务之间切换 - 系统中断之间的时间是一个时间片。

    FreeRTOS调度策略的实现

    list是是FreeRTOS核心的数据结构,它在list.c文件中实现为一个双向循环的链表。而FreeRTOS的抢占式的调度策略就是基于此实现的。

    在task.c中定义了一个pxReadyTasksLists[ configMAX_PRIORITIES ] 链表数组,其中

    configMAX_PRIORITIES 是定义在FreeRTOSConfig.h中的配置参数表示task的最大优先级。所以有几个优先级就有几个ReadyList,这是一一对应的。如下图:

    任务创建

     而当我们创建task时就会在创建时,把创建的task插入对应优先级的pxReadyTasksLists中,如下图:

    同时创建任务时也会使pxCurrentTCB指针指向优先级最高的TCB,如下图:

    任务调度的4种情景:

    1.第一次启动任务调度器 

    任务创建完成之后,启动任务调度器,调度器就会使能SVC中断,如下图:(至于为什么要进入SVC中断,因为在只有在特权级模式下,程序才以访问一些内核的寄存器,和特殊功能寄存器,且我们希望在task正常运行时芯片工作在线程模式,而要启动第一个task,需要访问这些寄存器,具体的可以参考这篇文件章:【RTX操作系统教程】第9章 任务运行在特权级或非特权级模式_硬汉Eric2013_新浪博客 (sina.com.cn)

     之后会跳转到SVC中断,而在SVC中会初始化一些通用寄存器,psp指针,打开BASEPRI中断屏蔽寄存器,并切换到线程模式跳转到第一个task的函数入口,之后退出中断就会执行第一个task了,如下图:

     2.任务主动触发调度

    当任务执行一些系统提供的API函数时,就可能触发任务调度,例如执行vTaskDelay函数,执行接收或发送队列函数且设置了阻塞时间,执行信号量和锁相关的函数导致阻塞等等。

    例如:调用xQueueReceive队列接收函数时,如果队列为空且设置了阻塞时间,则会把当前的task从pxReadyTasksLists插入到delay链表中,然后调用portYIELD_WITHIN_API 使能PendSV中断,然后在PendSV中断里保存当前任务的上下文,并切换到下一个优先级task

     3.SystemTick时钟触发调度

    整个os的时间依赖于SystemTick,而SystemTick时间来源于内核硬件的滴答定时器。当某个task运行时,SystemTick时间到了会进入SystemTick的中断,在此中断里会调用xTaskIncrementTick,先判断是否有任务的阻塞时间到了,如果有然后把任务从delayList中放入pxReadyTasksLists中,同时判断新加入的task优先级是否大于当前正在运行的优先级,如果是就置一个pdTRUE的标志位

    如果设置了时间片轮转的宏configUSE_TIME_SLICING,就会再判断当前task所在的pxReadyTasksLists中是否还有其他同优先级的任务,有的话就置标志位相当于一个时间片调度一次同优先级的任务。

    BaseType_t xTaskIncrementTick( void )
    {
        TCB_t * pxTCB;
        TickType_t xItemValue;
        BaseType_t xSwitchRequired = pdFALSE;
    
        /* Called by the portable layer each time a tick interrupt occurs.
         * Increments the tick then checks to see if the new tick value will cause any
         * tasks to be unblocked. */
        traceTASK_INCREMENT_TICK( xTickCount );
    
        if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
        {
            /* Minor optimisation.  The tick count cannot change in this
             * block. */
            const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
    
            /* Increment the RTOS tick, switching the delayed and overflowed
             * delayed lists if it wraps to 0. */
            xTickCount = xConstTickCount;
            //判断tick是溢出,是则交换delaylist
            if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
            {
                taskSWITCH_DELAYED_LISTS();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
    
            /* See if this tick has made a timeout expire.  Tasks are stored in
             * the  queue in the order of their wake time - meaning once one task
             * has been found whose block time has not expired there is no need to
             * look any further down the list. */
            if( xConstTickCount >= xNextTaskUnblockTime )
            {
                for( ; ; )
                {    //判断延时链表是否为空,为空就是没有要解阻塞的任务,当前任务就一直运行
                    if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                    {
                        /* The delayed list is empty.  Set xNextTaskUnblockTime
                         * to the maximum possible value so it is extremely
                         * unlikely that the
                         * if( xTickCount >= xNextTaskUnblockTime ) test will pass
                         * next time through. */
                        xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
                        break;
                    }
                    else
                    {
                        /* The delayed list is not empty, get the value of the
                         * item at the head of the delayed list.  This is the time
                         * at which the task at the head of the delayed list must
                         * be removed from the Blocked state. */
                          //找出阻塞优先级最高的task,查看其阻塞时间是否到了
                        pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
                        xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
    
                        if( xConstTickCount < xItemValue )
                        {
                            /* It is not time to unblock this item yet, but the
                             * item value is the time at which the task at the head
                             * of the blocked list must be removed from the Blocked
                             * state -  so record the item value in
                             * xNextTaskUnblockTime. */
                            xNextTaskUnblockTime = xItemValue;
                            break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                        //时间到了从阻塞链表中移除
                        /* It is time to remove the item from the Blocked state. */
                        ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
    
                        /* Is the task waiting on an event also?  If so remove
                         * it from the event list. */
                        if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                        {
                            ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                        //插入reday链表中
                        /* Place the unblocked task into the appropriate ready
                         * list. */
                        prvAddTaskToReadyList( pxTCB );
    
                        /* A task being unblocked cannot cause an immediate
                         * context switch if preemption is turned off. */
                        //如果支持抢占式调度
                        #if ( configUSE_PREEMPTION == 1 )
                            {
                                /* Preemption is on, but a context switch should
                                 * only be performed if the unblocked task has a
                                 * priority that is equal to or higher than the
                                 * currently executing task. */
                                //判断插入的task优先级是否大于当前的task
                                if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                                {
                                    xSwitchRequired = pdTRUE;
                                }
                                else
                                {
                                    mtCOVERAGE_TEST_MARKER();
                                }
                            }
                        #endif /* configUSE_PREEMPTION */
                    }
                }
            }
    
            /* Tasks of equal priority to the currently running task will share
             * processing time (time slice) if preemption is on, and the application
             * writer has not explicitly turned time slicing off. */
            //是否支持时间片调度
            #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
                {
                     //同优先级任务不止一个
                    if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
                    {
                        xSwitchRequired = pdTRUE;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
            #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
    
            #if ( configUSE_TICK_HOOK == 1 )
                {
                    /* Guard against the tick hook being called when the pended tick
                     * count is being unwound (when the scheduler is being unlocked). */
                    if( xPendedTicks == ( TickType_t ) 0 )
                    {
                        vApplicationTickHook();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
            #endif /* configUSE_TICK_HOOK */
    
            #if ( configUSE_PREEMPTION == 1 )
                {    //是否有挂起请求
                    if( xYieldPending != pdFALSE )
                    {
                        xSwitchRequired = pdTRUE;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
            #endif /* configUSE_PREEMPTION */
        }
        else
        {
            ++xPendedTicks;
    
            /* The tick hook gets called at regular intervals, even if the
             * scheduler is locked. */
            #if ( configUSE_TICK_HOOK == 1 )
                {
                    vApplicationTickHook();
                }
            #endif
        }
    
        return xSwitchRequired;
    }

    最后会判断返回的标志位,来确定是否进行调度,如果是pdTRUE则会启动pendSV中断在此中断里切换上下文调度到新的task。

    4.因为中断而引起的任务调度

    中断里调用系统API导致更高优先级的task唤醒,如果不显示的调用调度的函数,则唤醒的任务不会在中断退出后立马调度,它会等到下一个SystemTick中断来时才切换task,这样会造成中断的响应延时,在某些实时性要求高的地方是不能满足需求的。所以可以在中断里直接在条件满足时,先切换task,这样在中断退出时就会直接返回到高优先级的task了。见如下例子:

    void ISR(void)
    {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE, tmpHPT=pdFALSE;
        xQueueSendFromISR(rx_handle, &buffer, tmpHPT);
        xHigherPriorityTaskWoken = xHigherPriorityTaskWoken || tmpHPT;
    
        /* If lHigherPriorityTaskWoken is now equal to pdTRUE, then a context
        switch should be performed before the interrupt exists.  That ensures the
        unblocked (higher priority) task is returned to immediately. */
    
        portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
    }

    (如有描述不对烦请指正谢谢!)

    展开全文
  • FreeRTOS调度原理

    千次阅读 多人点赞 2020-03-29 21:29:22
    以个人理解,FreeRTOS内核调度的本质是利用了从异常中断返回时,切换任务栈的机制,使得进入新的任务下进行执行任务,实现内核调度功能。 内核进入第一个空闲任务分析 内核版本:FreeRTOS V9.0.0 硬件平台:STM32...

    前言

    以个人理解,FreeRTOS内核调度的本质是利用了从异常中断返回时,切换任务栈的机制,使得进入新的任务下进行执行任务,实现内核调度功能。

    内核进入第一个空闲任务分析

    • 内核版本:FreeRTOS V9.0.0
    • 硬件平台:STM32F103ZE
    • 仿真平台:MDK5.23

    启动代码分析

    // startup_stm32f10x_hd.s
    ; Reset handler
    Reset_Handler   PROC
                    EXPORT  Reset_Handler             [WEAK]
                    IMPORT  __main
                    IMPORT  SystemInit
                    LDR     R0, =SystemInit
                    BLX     R0               
                    LDR     R0, =__main
                    BX      R0
                    ENDP
    
    

    内核初始化时首先执行startup_stm32f10x_hd.s这个文件内部的内容:

    • 首先设置初始化堆栈
    • 然后设置PC指针为Reset_Handler标签
      这样在进入复位向量后,先初始化时钟系统,然后进入main函数执行应用程序代码。
    // src code
    int main(void)
    {
    	BaseType_t xReturn = pdPASS;//定义一个创建信息返回值,默认为 pdPASS 
    	BspInit();
    	printf("FreeRTOSTask\r\n");
    	printf("Please send queue message by press KEY2 or KEY_UP\n");
    	printf("ReceiveTask receive message echo in USART\n\n");
    	if(pdPASS == xReturn)
    	{
    		vTaskStartScheduler();//启动任务,开始调度
    	}		
    	else
    	{
    		return -1;
    	}
    	while(1);//正常不会执行到这里	
    }
    

    上面进入初始化bsp相关外设后,会直接进入系统调度vTaskStartScheduler:

    void vTaskStartScheduler( void )
    {
    BaseType_t xReturn;
    
    	/* 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,
    												"IDLE",
    												ulIdleTaskStackSize,
    												( void * ) NULL,
    												( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
    												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
    	{
    	  "本例使用的是动态申请内存的方式进行分析调度原理,所以可以从此处开始:"
    	  "这一步是申请空闲任务,优先级为最低"
    		/* The Idle task is being created using dynamically allocated RAM. */
    		xReturn = xTaskCreate(	prvIdleTask,
    								"IDLE", configMINIMAL_STACK_SIZE,
    								( void * ) NULL,
    								( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
    								&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
    	}
    	#endif /* configSUPPORT_STATIC_ALLOCATION */
    
    	#if ( configUSE_TIMERS == 1 )
    	{
    		if( xReturn == pdPASS )
    		{
    			xReturn = xTimerCreateTimerTask();
    		}
    		else
    		{
    			mtCOVERAGE_TEST_MARKER();
    		}
    	}
    	#endif /* configUSE_TIMERS */
    
    	if( xReturn == pdPASS )
    	{
    		/* 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();
    
    		#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. */
    			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
    		}
    		#endif /* configUSE_NEWLIB_REENTRANT */
    
    		xNextTaskUnblockTime = portMAX_DELAY;
    		xSchedulerRunning = pdTRUE;
    		xTickCount = ( TickType_t ) 0U;
    
    		/* 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. */
    		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
    		
    		"上面申请完空闲任务后,会开始进入系统调度,按照已有的概念,我们知道最终会进入空闲"
    		"任务中执行,如何进入这个空闲任务,是接下来我们分析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(). */
    		}
    	}
    	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;
    }
    

    上面申请完空闲任务后,会开始进入系统调度,按照已有的概念,我们知道最终会进入空闲任务中执行,如何进入这个空闲任务,是接下来我们分析xPortStartScheduler的重点:

    // ARM_CM3/port.c
    BaseType_t xPortStartScheduler( void )
    {
    ...
    	/* Make PendSV and SysTick the lowest priority interrupts. */
    	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    
    	/* Start the timer that generates the tick ISR.  Interrupts are disabled
    	here already. */
    	vPortSetupTimerInterrupt();
    	
    	/* Initialise the critical nesting count ready for the first task. */
    	uxCriticalNesting = 0;
    	
    	"重点会进入这个函数,开启第一个任务:"
    	/* Start the first task. */
    	prvStartFirstTask();
    
    	/* Should not get here! */
    	return 0;
    }
    
    
    __asm void prvStartFirstTask( void )
    {
    	PRESERVE8    "8字节对齐"
    
    	/* Use the NVIC offset register to locate the stack. */
    	ldr r0, =0xE000ED08    "这个地址是NVIC偏移寄存器的地址,用来定位当前的栈的"
    	ldr r0, [r0]
    	ldr r0, [r0] "执行完这句后,r0寄存器内部存放的就是当前的栈地址"
    
    	/* Set the msp back to the start of the stack. */
    	msr msp, r0  "由于CM3的双堆栈特性,初始化时,会进入主堆栈。因此将r0的值传输msp"
    	/* Globally enable interrupts. */
    	cpsie i   "强制使能所有的中断"
    	cpsie f
    	dsb "同步数据,主要是担心某些存储器写入存在缓冲机制"
    	isb
    	/* Call SVC to start the first task. */
    	svc 0   
    	nop
    	nop
    }
    

    svc 0 可以触发一个系统调用,使其接下来的pc指针强制进入svc下的异常处理回调函数:

    // ARM_CM3/port.c
    __asm void vPortSVCHandler( void )
    {
    	PRESERVE8
    	
    	"下面这一句,是将当前存储TCB变量的地址放入r3寄存器中"
    	ldr	r3, =pxCurrentTCB	/* Restore the context. */
    	"下面这一句,是将当前的TCB指向的地址取出放入r1寄存器中"
    	ldr r1, [r3]			/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
    	"下面这一句,是将TCB结构体中的第一个变量取出来放入r0寄存器中;"
    	"TCB结构体中的第一个变量也就是当前的栈顶地址"
    	ldr r0, [r1]			/* The first item in pxCurrentTCB is the task top of stack. */
    	"下面这一句,是将r0处读取多个数据,依次放入r4-r11寄存器中,每读取一次,r0自增一次"
    	"这里相当于从当前的栈顶指针出栈8个数据到r4-r11寄存器中"
    	ldmia r0!, {r4-r11}		/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
    	"这个时候将r0处的栈顶指针放入psp中,psp属于进程堆栈,专门用来保存正在执行的任务堆栈"
    	"此时r0寄存器存放的是空闲任务携带参数的地址"
    	msr psp, r0				/* Restore the task stack pointer. */
    	"强制指令同步,数据同步"
    	isb
    	"下面这一句,是将r0寄存器清0"
    	mov r0, #0
    	"下面这一句,是将特殊寄存器basepri置0,表示开启所有的中断使能"
    	msr	basepri, r0
    	"下面这一句,逻辑或的意思,将r14寄存器内部的值与0x0d或后,再存入r14"
    	"这一步很关键,为什么要或0x0d?下面详细说明"
    	orr r14, #0xd
    	"异常返回指令,这个返回指令有两种意思:在普通函数返回时,即将使用的PC指针就是目前r14"
    	"寄存器中的值;在异常函数中返回时,r14表示一个exc_return数值,硬件系统会根据这个"
    	"值来判断下一步的堆栈、pc值"
    	bx r14
    }
    

    关于exc_return的解释:
    在这里插入图片描述

    从上图看出,使用0x0d或操作,是为了返回线程模式,并使用线程堆栈。
    那么是如何使用线程堆栈的呢?此处的使用的硬件平台是stm32f103系列,所以对应的内核为cortex-m3内核,使用的指令架构为armv7-m指令集,因此可以查阅该指令集下异常返回伪代码:

    Exception return operation
    The ExceptionReturn() pseudocode function describes the exception return operation:
    // ExceptionReturn()
    // =================
    ExceptionReturn(bits(28) EXC_RETURN)
    assert CurrentMode == Mode_Handler;
    if HaveFPExt() then
    if !IsOnes(EXC_RETURN<27:5>) then UNPREDICTABLE;
    else
    if !IsOnes(EXC_RETURN<27:4>) then UNPREDICTABLE;
    integer ReturningExceptionNumber = UInt(IPSR<8:0>);
    integer NestedActivation; // used for Handler => Thread check when value == 1
    NestedActivation = ExceptionActiveBitCount(); // Number of active exceptions
    if ExceptionActive[ReturningExceptionNumber] ==0’ then
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // returning from an inactive handler
    return;
    else
    case EXC_RETURN<3:0> of
    when ‘0001// return to Handler
    frameptr = SP_main;
    CurrentMode = Mode_Handler;
    CONTROL.SPSEL =0;
    when ‘1001// returning to Thread using Main stack
    if NestedActivation != 1 && CCR.NONBASETHRDENA ==0’ then
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return to Thread exception mismatch
    return;
    else
    frameptr = SP_main;
    CurrentMode = Mode_Thread;
    CONTROL.SPSEL =0;
    when ‘1101// returning to Thread using Process stack
    if NestedActivation != 1 && CCR.NONBASETHRDENA ==0’ then
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return to Thread exception mismatch
    return;
    else
    frameptr = SP_process;
    CurrentMode = Mode_Thread;
    CONTROL.SPSEL =1;
    otherwise
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // illegal EXC_RETURN
    return;
    DeActivate(ReturningExceptionNumber);
    PopStack(frameptr);
    if CurrentMode==Mode_Handler AND IPSR<8:0> ==000000000’ then
    UFSR.INVPC =1;
    PushStack(); // to negate PopStack()
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return IPSR is inconsistent
    return;
    if CurrentMode==Mode_Thread AND IPSR<8:0> !=000000000’ then
    UFSR.INVPC =1;
    "出栈的关键部分"
    PushStack(); // to negate PopStack()
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return IPSR is inconsistent
    return;
    ClearExclusiveLocal();
    SetEventRegister() // see WFE instruction for more details
    InstructionSynchronizationBarrier();
    if CurrentMode==Mode_Thread AND NestedActivation == 0 AND SCR.SLEEPONEXIT ==1’ then
    SleepOnExit(); // IMPLEMENTATION DEFINED
    

    上述伪代码中PushStack为出栈部分,当异常返回时,正常的话会执行出栈操作,这里我们可以找到pc寄存器即将放置哪个值:

    // PopStack()
    // ==========
    PopStack(bits(32) frameptr) /* only stack locations, not the load order, are architected */
    if HaveFPExt() && EXC_RETURN<4> ==0’ then
    framesize = 0x68;
    forcealign =1;
    else
    framesize = 0x20;
    forcealign = CCR.STKALIGN;
    "此处的frameptr就是对应psp的值"
    R[0] = MemA[frameptr,4];
    R[1] = MemA[frameptr+0x4,4];
    R[2] = MemA[frameptr+0x8,4];
    R[3] = MemA[frameptr+0xC,4];
    R[12] = MemA[frameptr+0x10,4];
    LR = MemA[frameptr+0x14,4];
    "下一句,就是取(psp+向右偏移24位bits)地址内的32位值,将此值赋值给pc"
    "此时新的pc的值必须半字对齐"
    PC = MemA[frameptr+0x18,4]; // UNPREDICTABLE if the new PC not halfword aligned
    psr = MemA[frameptr+0x1C,4];
    if HaveFPExt() then
    if EXC_RETURN<4> ==0’ then
    if FPCCR.LSPACT ==1’ then
    FPCCR.LSPACT =0; // state in FP is still valid
    else
    CheckVFPEnabled();
    for i = 0 to 15
    S[i] = MemA[frameptr+0x20+(4*i),4];
    FPSCR = MemA[frameptr+0x60,4];
    CONTROL.FPCA = NOT(EXC_RETURN<4>);
    spmask = Zeros(29)((psr<9> AND forcealign):00;
    case EXC_RETURN<3:0> of
    when ‘0001// returning to Handler
    SP_main = (SP_main + framesize) OR spmask;
    when ‘1001// returning to Thread using Main stack
    SP_main = (SP_main + framesize) OR spmask;
    when ‘1101// returning to Thread using Process stack
    SP_process = (SP_process + framesize) OR spmask;
    APSR<31:27> = psr<31:27>; // valid APSR bits loaded from memory
    if HaveDSPExt() then
    APSR<19:16> = psr<19:16>;
    IPSR<8:0> = psr<8:0>; // valid IPSR bits loaded from memory
    EPSR<26:24,15:10> = psr<26:24,15:10>; // valid EPSR bits loaded from memory
    return;
    

    从上面的伪代码可以发现异常返回时,先判断r14的值,再根据相应的值进行出栈,然后进入新的任务栈中执行任务。所以内核就是这样进入第一个空闲任务的:

    • 建立空闲任务堆栈,并初始化,填充任务回调函数等参数;
    • 触发svc中断,进入svc中断处理函数;
    • 在svc中断处理函数中,手动调整psp堆栈为空闲任务堆栈值;
    • 返回时,借助异常返回指令,最终正确跳转到空闲任务的回调函数中。

    仿真演示

    打开仿真软件,配置如下:
    在这里插入图片描述

    打开工程,启动断点调试:
    在这里插入图片描述

    此处是建立空闲任务后,初始化空闲任务的堆栈。
    在这里插入图片描述

    这里注意寄存器的值,还有此处堆栈处的值,在右下角内存地址处可以看到。

    在这里插入图片描述

    上面这张图可以发现r14寄存器的低4位是0x0d,说明异常返回时即将进入的线程模式下的进程堆栈。
    在这里插入图片描述

    上面这个图,注意看到psp的地址位0x20000578,r15的地址位0x08000fe8,r14的地址为0x080011dd,是不是与右下角的内存地址全部对应上了。

    仿真的结果与理论一致,所以内核就是这样进入第一个空闲任务的。那么后面是如何调度的呢?这就需要借助挂起中断了。

    内核如何调度

    上面介绍了内核如何进入第一个任务。那么在进入第一个任务之后,在运行过程中,内核是如何调度到第二个任务?其实内核切换任务是利用了PendSV(可悬起中断/系统调用)机制。在多任务环境下,内核每次切换任务时,都会进入PendSV中断服务函数里,进行切换任务栈操作。

    内核调度分析

    // ARM_CM3/port.c
    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. */
    			"下面这一句,是将pendsv中断寄存器位置1;执行完此句话后,会触发"
    			"pendsv中断,进入pendsv中断服务程序"
    			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
    		}
    	}
    	vPortClearBASEPRIFromISR();
    }
    

    上面是一个系统定时器中断服务程序,当定时到时,会进入该中断,当满足特定条件时,会将pendsv中断打开触发,然后进入pendsv中断服务程序。

    // ARM_CM3/port.c
    __asm void xPortPendSVHandler( void )
    {
    	"这是一个内嵌汇编pendsv中断服务程序,下面3句是引用3个全局变量"
    	extern uxCriticalNesting;
    	extern pxCurrentTCB;
    	extern vTaskSwitchContext;
    
    	PRESERVE8  "8字节对齐"
    	
    	"下面这一句是将当前psp堆栈值寄存在r0中,因为psp等下会变"
    	mrs r0, psp 
    	isb  "强制指令清空,我的理解是将三级流水线内的指令清空"
        "下面这一句,是将pxCurrentTCB变量的地址寄存在r3中"
    	ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
    	"下面这一句,是将pxCurrentTCB指向内存区域的第一个元素地址寄存在r2中"
    	"而pxCurrentTCB指向内存区域的第一个元素值,"
    	"其实就是记录当前TCB的栈顶地址变量."
    	"执行完下一句后,r2中的值就是指向当前TCB栈顶地址的变量"
    	ldr	r2, [r3]
    	"下面这一句是将寄存器r4-r11的值依次压入当前栈中,r0中的值会减少32(4x8)"
    	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
    	"此时的r0中的值就是当前任务压栈后的栈顶地址,执行完下一句后,"
    	"就是将当前TCB栈顶地址的变量重新指向更新后的栈顶地址"
    	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */
    	"因为该函数是中断服务程序,由于CM3的双堆栈特性,所以,"
    	"此处的sp表示的是msp(主堆栈)。下面这一句,是依次将r14、r3存入msp中"
    	"存放r14的原因是,后面需要使用到这个lr值;存放r3的原因是,后面要"
    	"使用r3获取pxCurrentTCB,其实使用pxCurrentTCB重新加载到寄存器中也可以"
    	stmdb sp!, {r3, r14}
    	"下面这一句,是将系统调用的最大中断优先级值寄存到r0中,为了屏蔽一部分的中断"
    	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    	msr basepri, r0
    	"强制同步数据"
    	dsb
    	"强制清除指令,清除3级流水线中的指令(个人理解)"
    	isb
    	"跳转到vTaskSwitchContext函数,此函数的目的是为了更新pxCurrentTCB"
    	"指向的地址。同时也可以看出为什么上面要压栈r14、r13了,因为存在子函数"
    	bl vTaskSwitchContext
    	"在更新pxCurrentTCB之后,下面这2句,是将所有的中断打开"
    	mov r0, #0
    	msr basepri, r0
    	"下面这一句,是将当初压栈的r3、r14出栈"
    	ldmia sp!, {r3, r14}
    	"下面这两句,是将r0存入更新后TCB的栈顶变量"
    	ldr r1, [r3]
    	ldr r0, [r1]				/* The first item in pxCurrentTCB is the task top of stack. */
    	"下面这一句,是将栈中的前8个值依次出栈到r4-r11"
    	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
    	"下面这一句,是将r0中的值赋值到psp上,其实在初始化栈的时候,"
    	"对应位置,也是r0那个位置顺序"
    	msr psp, r0
    	"清除指令"
    	isb
    	"跳转指令,这个会对r14的值进行判断,如果后4位是0x0d,"
    	"会根据psp的值,出栈到pc、r1等寄存器"
    	bx r14
    	nop
    }
    

    上面的bx r14条转指令是神奇的指令,在这一步,才决定了pc到底指向何方。上面的汇编程序就说明了内核如何调度了,每次进入pendsv中断函数内:

    • 压栈所需要的寄存器值,比如r14、r3
    • 进入子程序,更新当前任务指针pxCurrentTCB
    • 退出子程序,出栈,结束跳转
    • 在跳转指令中,指令系统会计算出pc应该指向的值

    内核调度演示

    在这里插入图片描述

    在这里插入图片描述

    上面图片显示在执行到bx r14时,lr的值的低4位是0x04,表明即将要进入用户模式的进程堆栈中,所以指令系统会将psp出栈,更新pc,进入新的任务栈,完成一次系统调度。

    内核如何更新pxCurrentTCB

    按照优先级大小,轮询调度

    • 优先级相同时,每次先调度先加入链表中的任务,然后再调用后加入链表中的任务
    • 优先级不同时,先调度优先高的,后依然调度优先级高的,所以称为可抢占调度内核
    • 没有处于就绪状态的任务,不参与每次调度
    展开全文
  • FreeRTOS任务调度

    2021-08-11 19:55:37
    首先FreeRTOS可以通过抢占式/时间片方式进行任务调度,这可以通过配置FreeRTOSConfig.h文件中的对应的宏值configUSE_PREEMPTION/configUSE_TIME_SLICING来完成。我使用了抢占式调度的方式,在FreeRTOS中的任务基本...

    首先FreeRTOS可以通过抢占式/时间片方式进行任务调度,这可以通过配置FreeRTOSConfig.h文件中的对应的宏值configUSE_PREEMPTION/configUSE_TIME_SLICING来完成。我使用了抢占式调度的方式,在FreeRTOS中的任务基本满足如下方式:

     

     

    当我们创建一个任务后,这个任务就处于就绪态,处于就绪态的所有任务,挂在同一条任务链表上,当任务开始调度,进行任务切换时,就绪态的任务中具有最高优先级的任务就进入运行态。而运行态的任务在任务执行过程中,可能需要等待某个内容,若一直等待将会占用处理器资源,此时可以令其进入阻塞态,阻塞态可以通过使用等待队列阻塞/延时阻塞/等待互斥信号阻塞等方式进入,而进入阻塞态的任务一旦等待的事件触发,则进入就绪态,等待下次任务调度继续执行。当我们不需要一个任务,而且又不希望删除任务的时候,可以利用创建任务时生成的任务句柄,让任务进入挂起态,且当我们需要任务回归时,只需要重新注册一下这个任务句柄就好。所有提到的阻塞态、就绪态、挂起态均有一个链表,大家有兴趣可以看一下内核代码实现。

    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    在FreeRTOS中,当开始任务调度后,内核通过滴答时钟中断实现任务调度,而滴答时钟可以通过配置FreeRTOSConfig.h文件中的宏configTICK_RATE_HZ来实现,当滴答中断发生时,内核会查询就绪任务表,并使就绪任务表中具有最高优先级的任务进入运行态,完成一次任务调度,等待下一次任务调度,如果所有的任务都处于挂起态/阻塞态,在FreeRTOS运行过程中,必须要有一个任务执行,此时将会执行空闲任务,我们可以通过空闲任务HOOK来添加我们自己的空闲任务,或者直接使用FreeRTOS提供的空闲任务。

    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    FreeRTOS中的任务需要满足如下的规范:

    1. 有一个进入点,并在进入后以无限循环形式运行,并不会退出
    2. 不允许有返回值,一定不能有return语句,如果一个任务不再需要时,我们只需要删除它就好了

    void vTestTask( void *pvParameters )

    {

        uint32_t testVar;

        pxParameters = ( xLEDParameters * ) pvParameters;

        for(;;)

        {

        }

    xTaskDelete(NULL);

    }

    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    创建任务调用xTaskCreate()接口,用户自己规定自己的任务函数,并给自己的任务命名(注意长度需要在宏值规定的范围内),规定任务栈深度,给出要传入任务的参数,给定任务优先级,并传入句柄指针,生成函数句柄,若希望删除任务、挂起任务、注册任务需要通过这个句柄来完成。值得一提的是uxPriority参数,这一个参数的配置需要参考FreeRTOSConfig.h配置的宏configMAX_PRIORITIES,支持的优先级范围为0 - configMAX_PRIORITIES-1。

    BaseType_t xTaskCreate( TaskFunction_t                 pxTaskCode,

                            const char * const             pcName, 

                            const configSTACK_DEPTH_TYPE  usStackDepth,

                            void * const                    pvParameters,

                            UBaseType_t                     uxPriority,

                        TaskHandle_t * const           pxCreatedTask )

    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    在创建任务后,我们此时可以配置FreeRTOSConfig.h的宏,来调用FreeRTOS提供的API接口实现不同的功能,如改变任务优先级、挂起任务、注册任务、删除任务、使用空闲任务HOOK函数等。不再展开。

     

    展开全文
  • 嵌入式操作系统RTOS任务调度原理分析,基于Cortex-M内核
  • 1.Cortex-M4中SysTick的重要性 2.Cortex-M4中的中断管理 3.Cortex-M4中影子栈指针 4.Cortex-M4中SVC和PendSV异常 5.多任务启动 6.PendSV业务流程 7.系统时钟节拍详解 8.SysTick中断服务函数 9.SysTick任务调度
  • 嵌入式操作系统学习(3)FreeRTOS任务调度机制

    万次阅读 多人点赞 2018-07-09 16:29:15
    FreeRTOS可以创建多个任务,但是对于单核cpu来说,在任意给定时间,实际上只有一个任务被执行,这样就可以把任务分成2个状态,即运行状态和非运行状态。 当任务处于运行状态时,处理器就执行该任务的代码。处于非...
  • 1:多任务启动 1:创建空闲任务 2:配置SysTick和PendSV为最低优先级 3:配置SysTick寄存器 4:调用SVC中断 2:SVC业务流程 1:获取当前任务栈顶 2:手动出栈 r4-r11,r14 3:更新栈顶到 PSP 4:使能全局中断,调用...
  • FreeRTOS工作原理

    2018-11-05 10:54:49
    备注:资源清晰度有所欠缺,但是不影响观看,内容对于初学者来说,理解FreeRTOS是非常很有帮助。 1. 多任务操作系统运行机制; 2. 任务的上下文切换,以一个实际的例子说明多任务运行机制; 3. 举了一个实例,说明...
  • FreeRTOS基本原理

    千次阅读 2020-07-09 16:26:04
    FreeRTOS基本原理 多任务 内核是操作系统的核心部分,操作系统例如Linux通过内核使用户看似同时的访问电脑,多...调度原理 调用度是内核中负责决定在某一时刻该执行什么任务的部分。内核可以挂起然后恢复一个任务许多次
  • 一、FreeRTOS任务在多数通用操作系统(如Linux、Windows等)中,线程为系统的调度最小单元,进程为独立应用程序的最小运行过程。而在实时操作系统中,多数情况下不区分线程与进程进...
  • freertos内核 任务定义与切换 原理分析主程序任务控制块任务创建函数任务栈初始化就绪列表调度器总结任务切换 主程序 这个程序目的就是,使用freertos让两个任务不断切换。看两个任务中变量的变化情况(波形)。 下面...
  • FreeRTOS原理剖析:任务调度器启动

    千次阅读 2019-10-08 06:52:35
    介绍了任务调度器启动的过程。
  • FreeRTOS调度算法 – 简述

    千次阅读 2021-12-31 13:50:00
    FreeRTOS 中的任务永远处于下面几个状态中的某一个: 二、优先级抢占式调度 FreeRTOS任务有如下特点: 1、每个任务都赋予了一个优先级。 2、每个任务都可以存在于一个或多个状态。 3、在任何时候都只有一个任务...
  • freertos任务调度简介

    2021-03-28 17:13:37
    c语言编写的调度算法,适用于所有的控制器;大概流程如下: 1.listLIST_IS_EMPTY()查找任务就绪列表数组pxReadyTasksLists[]中的最高优先级任务列表是否为空,为空即没有列表项,没有列表项即没有线程。为空的情况...
  • FreeRTOS(三)任务调度之抢占式

    千次阅读 2020-08-13 21:16:15
    FreeRTOS最核心的就是任务调度FreeRTOS 操作系统支持的任务调度方式:抢占式,时间片和合作式。合作式调度器由于占用资源大已经很少使用,官方没有删除,但以后不会升级了。我们平时默认使用的就是抢占式调度器。...
  • FreeRTOS运行时是不断在不同的任务间进行切换,而驱动这一调度过程是通过系统tick来驱动的,即每产生一次系统tick则检查一下当前正在运行的任务的环境判断是否需要切换任务,即调度,如果需要,则触发PendSV,通过...
  • 任务通知消息能从中断发给任务或者从任务发给任务,但是不能从任务发给中断; 任务通知消息只能发送给某个特定的任务,无法发送给多个任务任务通知的消息一次只能保持一个值,无法向队列那样存储多个消息; 消息...
  • [导读] 学习梳理一下FreeRTOS任务管理单元实现思路,代码分析基于V10.4.3。从本文开始计划写个图解freeRTOS内核系列笔记分享给朋友们,希望大家喜欢。文章中或有错误,也请留...
  • 一、CortexM3中断优先级 ...SVC在启动任务调度的时候使用; SysTIck定时器用于周期性的中断,为系统提供心跳; PendSV用于任务切换; 对于实时操作系统而言,我们一般外部中断优先得到响应,所以SysTick和PendSV的
  • FreeRTOS原理剖析:任务切换过程

    千次阅读 2019-10-04 16:35:49
    讲解任务切换的基本概念和PendSV任务切换过程,也分析了任务切换的两种方法。
  • ucosIII任务调度原理解析

    千次阅读 2019-01-22 21:12:39
    μcosIII任务调度原理解析前言μcosIII任务调度相关的数据结构任务控制块 OS_TCB就绪任务表结构μcosIII时间节拍轮任务阻塞表任务调度实现细节任务调度点时钟节拍轮相关调度任务阻塞表相关调度就绪任务表相关调度...
  • FreeRTOS任务管理

    千次阅读 2022-07-01 10:42:03
    FreeRTOS任务管理系统,任务的创建、删除、挂起和恢复等操作。
  • 相对于C/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行,其最新版本为2.6版。 1 FreeRTOS操作

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 901
精华内容 360
关键字:

freertos任务调度原理

友情链接: perceptron.py.tar.gz