• 【freertos】005-启动调度器分析


    前言

    本节主要讲解启动调度器。

    这些都是与硬件相关,所以会分两条线走:posix和cortex m3。

    原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16076476.html

    5.1 调度器的基本概念

    5.1.1 调度器

    调度器就是使用相关的调度算法来决定当前需要执行的任务。

    调度器特点:

    1. 调度器可以区分就绪态任务和挂起任务。
    2. 调度器可以选择就绪态中的一个任务,然后激活它。
    3. 不同调度器之间最大的区别就是如何分配就绪态任务间的完成时间。

    嵌入式实时操作系统的核心就是调度器和任务切换:

    • 调度器的核心就是调度算法。
    • 任务切换是基于硬件内核架构实现。

    5.1.2 抢占式调度

    抢占式调度:

    • 每个任务都被分配了不同的优先级,抢占式调度器会获得就绪列表中优先级最高的任务,并运行这个任务。
    • 在FreeRTOS系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。

    5.1.3 时间片调度

    最常用的的时间片调度算法就是Round-robin调度算法,这种调度算法可以用于抢占式或者合作式的多任务中。

    实现Round-robin调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片。

    当任务就绪链表中最高优先级中存在两个以上的任务时,当前运行的任务耗尽时间片后,当前链表的下一个任务到运行态,把当前任务重新插入到当前优先级就绪链表尾部。

    使用时间片调度需要在FreeRTOSConfig.h文件中使能宏定义:#defineconfigUSE_TIME_SLICING 1

    需要注意的是,freertos时间片不能随意的设置时间为多少个tick,只能默认一个tick。

    5.2 cortex m3架构的三个异常

    在Cortex-M3架构中,FreeRTOS为了任务启动和任务切换使用了三个异常:SVC、PendSV和SysTick。

    对应三个异常回调:

    注意:Cortex-M的优先级数值越大其优先级越低。

    5.2.1 SVC

    SVC(系统服务调用,亦简称系统调用)用于任务启动。所以只被调用一次。

    有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件,它就会产生一个SVC异常。

    在该异常回调里启动第一个任务。

    5.2.2 PendSV

    PendSV(可挂起系统调用)用于完成任务切换。

    该异常可以像普通的中断一样被挂起的,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会延迟执行,直到高优先级中断执行完毕,这样子产生的PendSV中断就不会打断其他中断的运行。

    在该异常的回调函数里执行任务切换。

    5.2.3 SysTick

    SysTick用于产生系统节拍时钟。

    每次systick异常产生都会检查是否需要任务调度,如果需要,则出发PendSV异常即可。

    5.3 启动调度器

    5.3.1 启动调度器描述

    启动调度器使用API函数vTaskStartScheduler()

    该函数会:

    • 创建一个空闲任务;
    • 创建软件定时器任务;
    • 初始化一些静态变量;
    • 会初始化系统节拍定时器并设置好相应的中断;
    • 启动第一个任务。

    启动调度器,硬件相关是调用xPortStartScheduler()

    5.3.2 创建空闲任务

    空闲任务时在启动调度器时创建的,该任务不能阻塞,创建空闲任务是为了不让系统退出,因为系统一旦启动就必须占有任务。

    空闲任务主体主要是做一些系统内存的清理工作、进入休眠或者低功耗操作等操作。

    创建空闲任务,也分两种方式,取决于是否开启静态内存分配宏configSUPPORT_STATIC_ALLOCATION

    5.3.2.1 静态内存创建

    参考前面任务基础相关的文章便可知,静态内存创建任务需要用户提供任务控制块和任务栈空间。

    由于空闲任务是内核API创建的,所以用户需要通过指定的函数vApplicationGetIdleTaskMemory()提供这些信息。

    实现代码如下:

    5.3.2.2 动态内存创建

    动态内存创建空闲任务,直接使用xTaskCreate()实现即可。

    5.3.3 创建软件定时器任务

    软件定时器组件功能,后面会详细分析,这里只做简单说明

    和创建空闲任务一个道理。

    前提条件时需要配置configUSE_TIMERS开启软件定时器功能。

    创建软件定时器内容集成在xTimerCreateTimerTask()API内部了,其实现和创建空闲任务一样的。

    通过宏configSUPPORT_STATIC_ALLOCATION区分静态和动态内存创建。

    5.3.3.1 初始化软件定时器组件内容

    调用prvCheckForValidListAndQueue()API初始化定时链表和创建定时器通信服务队列。

    5.3.3.2 静态内存创建

    通过用户实现的vApplicationGetTimerTaskMemory()API获取软件定时器任务控制块和任务栈信息。

    5.3.3.3 动态内存创建

    动态内存创建软件定时器任务,直接使用xTaskCreate()实现即可。

    5.3.4 调度器中的用户函数

    在启动调度器时,内核运行用户插入一个函数调用,一般用于启动调度器标识处理。

    指定函数:freertos_tasks_c_additions_init()

    使能宏:FREERTOS_TASKS_C_ADDITIONS_INIT

    5.3.5 CPU利用率统计配置

    如果用户配置了portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()宏函数,在调度器启动时需要调用。

    该函数一般是重置定时器起始值,搭配portGET_RUN_TIME_COUNTER_VALUE()宏函数实现运行时间统计功能。

    可以参考李柱明博客:cpu利用率统计后面可能会有独立章节描述该功能的实现

    在启动调度器中的代码:

    5.3.6 posix启动调度器分析

    源码分析:

    • 启动调度:xPortStartScheduler()
    • 利用进程实时定时器实现系统滴答:prvSetupTimerInterrupt()
    • 利用线程通信实现启动第一个任务:vPortStartFirstTask()
    • 在第一次初始化任务栈时会跑该函数(只跑一次):prvSetupSignalsAndSchedulerPolicy()

    5.3.6.1 启动调度器

    在接口层,启动调度调用xPortStartScheduler()

    • 获取线程ID;
    • 配置系统滴答时钟;
    • 启动第一个任务;
    • 等待用户调用vPortEndScheduler()关闭调度。
    • 系统调度求关闭后需要删除和释放启动调度器时创建的空闲任务和软件定时器任务。
    • 恢复主线程型号掩码。

    5.3.6.2 实现滴答时钟

    利用进程实时定时器实现系统滴答:prvSetupTimerInterrupt()

    采用posix标准下的getitimer()setitimer()API去实现。

    在进程里使用ITIMER_REAL计数器实现系统滴答时钟。

    • posix标准下,每个进程都会维护三个域的定时器,当前使用的ITIMER_REAL是进程实时定时器。

    5.3.6.3 启动第一个任务

    利用线程通信实现启动第一个任务:vPortStartFirstTask()

    原理在前面posix模拟器设计说过。

    利用线程型号实现线程的启停从而实现任务切换。

    先获取线程句柄:

    发信号给下一个需要跑的线程,让其启动,这样就进入了freertos世界嘞:

    那还需要停止当前线程嘞,完成这些时后,回进入等待结束调度器事件而阻塞:(代码在xPortStartScheduler()中)

    5.3.7 cortex m3启动调度器分析

    启动调度器:xPortStartScheduler()

    SVC异常启动第一个任务:vPortSVCHandler()

    5.3.7.1 基本知识

    1. cortex m的双堆栈指针MSP和PSP的切换。

    2. 硬件出入栈和软件出入栈。

      1. 硬件出入栈:异常时,硬件会完成部分必要寄存器的出入栈。
      2. 软件出入栈:由于硬件压栈信息对保护上下文不够,需要软件出入栈完成其它CPU寄存器的出入栈。

    5.3.7.2 cortex m3的启动调度器的基本内容

    1. 把PendSV和SysTick设置为最低优先级的中断。

    2. 启动滴答定时器。

    3. 启动第一个任务。通过SVC异常方式。

      1. 重置MSP堆栈指针。

      2. 使能全局中断。

      3. 触发SVC异常。进入SVC异常。

        1. 获取pxCurrentTCB值,即是当前需要跑的任务句柄。
        2. 通过任务句柄获取任务控制块,通过任务控制块获取任务栈顶。
        3. 软件出栈。
        4. 更新栈顶指针到PSP。
        5. 修改R14寄存器,使异常退出时,进入线程模式,使用PSP栈指针。
        6. 退出异常。硬件自动使用PSP出栈。

    至此,系统已经启动,进入freertos世界。

    5.3.7.3 FromISR中断保护配置

    在freertos中会看到FromISR后缀的API,这些API执行环境不一样,一般用于中断回调中使用,要求不能阻塞,快进快出。

    这些API不能在中断保护外的中断回调中使用,取决于宏configMAX_SYSCALL_INTERRUPT_PRIORITY

    所以需要配置进出临界能屏蔽中断的优先级级别,优先级等于或低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断能被临界API屏蔽,可调用FromISR后缀的API。

    先了解下几个宏:(数值越小,中断优先级越高)

    • configLIBRARY_LOWEST_INTERRUPT_PRIORITY:定义SysTick与PendSV的中断优先级。
    • configKERNEL_INTERRUPT_PRIORITY:配置SysTick与PendSV的中断优先级到寄存器。
    • configMAX_SYSCALL_INTERRUPT_PRIORITY:定义freertos系统可控最大中断优先级。
    • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:用于配置basepri寄存器的,当 basepri 设置为某个值的时候,会让系统不响应比该优先级低的中断,而优先级比之更高
      的中断则不受影响。这样,freertos可以通过控制basepri值来控制部分中断,实现中断保护。

    5.3.7.4 配置PendSV和SysTick中断优先级

    PendSV用于切换任务;

    SysTick用于系统节拍。

    这两个都配置为最低优先级。

    这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,有利于系统稳定。

    而且SysTick是硬件定时器,响应可能会延迟,都是系统事件不会有偏差。

    5.3.7.5 启动滴答定时器

    调用vPortSetupTimerInterrupt()实现。

    5.3.7.6 启动第一个任务

    调用prvStartFirstTask()实现。

    启动第一个任务:

    • 先使能全局中断;
    • 触发进入SVC异常回调;
    • 在SVC回调切入第一个任务。
    __asm void prvStartFirstTask( void )
    {
        PRESERVE8 /* 当前栈需按照 8 字节对齐 */
        /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */
        ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */
        ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */
        ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */
    
        /* 将msp设置回堆栈的开始 */
        msr msp, r0
        /* 使能全局中断 */
        cpsie i
        cpsie f
        dsb
        isb
        /* 触发SVC异常开启动第一个任务. */
        svc 0
        nop
        nop
    /* *INDENT-ON* */
    }

    SVC回调:

    • 通过pxCurrentTCB获取当前需要跑的第一个任务控制块;
    • 获取该任务栈顶地址;
    • 从栈顶地址软件出栈;(下文恢复)
    • 更新栈顶地址到PSP;
    • 双堆栈指针从MSP转用PSP;
    • 异常返回,硬件会根据PSP栈出栈,完成下文恢复,进入freertos第一个任务。
    __asm void vPortSVCHandler( void )
    {
    /* *INDENT-OFF* */
        PRESERVE8
    
        ldr r3, = pxCurrentTCB   /* 加载 pxCurrentTCB 的地址到 r3. */
        ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */
        ldr r0, [ r1 ]           /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */
        ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */
        msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */
        isb
        mov r0, # 0 /* 将寄存器 r0 清 0 */
        msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */
        orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */
        bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */
    /* *INDENT-ON* */
    }

    5.3.7.7 启动第一个任务后的任务栈情况

    该图片源自野火

    附件

    vTaskStartScheduler()

    posix:xPortStartScheduler()

    posix:prvSetupTimerInterrupt()

    posix:vPortStartFirstTask()

    cortex m3:xPortStartScheduler()

    cortex m3:prvStartFirstTask()

    __asm void prvStartFirstTask( void )
    {
    /* *INDENT-OFF* */
        PRESERVE8 /* 当前栈需按照 8 字节对齐 */
    
        /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */
        ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */
        ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */
        ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */
    
        /* 将msp设置回堆栈的开始 */
        msr msp, r0
        /* 使能全局中断 */
        cpsie i
        cpsie f
        dsb
        isb
        /* 触发SVC异常开启动第一个任务. */
        svc 0
        nop
        nop
    /* *INDENT-ON* */
    }

    cortex m3:vPortSVCHandler()

    __asm void vPortSVCHandler( void )
    {
    /* *INDENT-OFF* */
        PRESERVE8
    
        ldr r3, = pxCurrentTCB   /* 加载 pxCurrentTCB 的地址到 r3. */
        ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */
        ldr r0, [ r1 ]           /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */
        ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */
        msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */
        isb
        mov r0, # 0 /* 将寄存器 r0 清 0 */
        msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */
        orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */
        bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */
    /* *INDENT-ON* */
    }

    cortex m3:xPortPendSVHandler()

    __asm void xPortPendSVHandler(void)
    {
        extern uxCriticalNesting;
        extern pxCurrentTCB; /* 指向当前激活的任务 */
        extern vTaskSwitchContext;
    
        PRESERVE8
    
        mrs r0, psp     /* PSP内容存入R0 */
        isb /* 指令同步隔离,清流水线 */
    
        ldr r3, = pxCurrentTCB /* 当前激活的任务TCB指针存入R2 */
        ldr r2,[r3]
    
        stmdb r0 !,{r4 - r11} /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */
        str r0,[r2] /* 将新的栈顶保存到任务TCB的第一个成员中 */
    
        stmdb sp !,{r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护*/
        mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界区 */
        msr basepri,r0
        dsb /* 数据和指令同步隔离 */
        isb
        bl vTaskSwitchContext /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
        mov r0,#0 /* 退出临界区*/
        msr basepri,r0
        ldmia sp !,
        {r3, r14} /* 恢复R3和R14*/
    
        ldr r1,[r3] 
        ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
        ldmia r0 !,{r4 - r11} /* 出栈*/
        msr psp,r0
        isb
        bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
        nop
    }

    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16076476.html
  • 关于博主: 嵌入式从业者。RTOS、Linux ...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    为什么在桌面右键word、excel和ppt会出现“在笔记本中编辑”的字样?
    rosnode ping指令
    exoplayer的使用-4,手势,事件监听等
    写Python爬虫又被屏蔽了,你现在需要一个稳定的代理IP
    26 WEB漏洞-XSS跨站之订单及Shell箱子反杀记
    OpenCV实战(31)——基于级联Haar特征的目标检测
    用 JavaScript 编写枚举的最有效方法
    java计算机毕业设计学生选课系统MyBatis+系统+LW文档+源码+调试部署
    使用Java NIO进行文件操作、网络通信和多路复用的案例
    你能猜出这是什么代码吗
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16076476.html