大连权威发布网站,安平网站建设培训,简历模板电子版,网站优化排名方案我们已经学会了 FreeRTOS 的任务创建和删除#xff0c;挂起和恢复等基本操作#xff0c;并且也学习了分析FreeRTOS 源码所必须掌握的知识#xff1a;列表和列表项。但是任务究竟如何被创建、删除、挂起和恢复的#xff1f;系统是怎么启动的等等这些我们还不了解#xff0c… 我们已经学会了 FreeRTOS 的任务创建和删除挂起和恢复等基本操作并且也学习了分析FreeRTOS 源码所必须掌握的知识列表和列表项。但是任务究竟如何被创建、删除、挂起和恢复的系统是怎么启动的等等这些我们还不了解一个操作系统最核心的内容就是多任务管理所以我们非常有必要去学习一下 FreeRTOS 的任务创建、删除、挂起、恢复和系统启动等这样才能对 FreeRTOS 有一个更深入的了解。 本章和下一章要讲解的内容和 Cortex-M 处理器的内核架构联系非常紧密阅读本章必须先对 Cortex-M 处理器的架构有一定的了解在学习本章的时候一定要配合《权威指南》来学习 推荐大家仔细阅读《权威指南》中的如下章节 1、第 3 章 技术综述通过阅读本章可以对 Cortex-M 处理器的架构有一个大体的了解。 2、第 4 章 架构强烈建议仔细阅读本章内容尤其是要理解其中讲解到的各个寄存器。 3、第 5 章 指令集本章和下一章的内容会涉及到一些有关 ARM 的汇编指令在阅读的 时遇到不懂的指令可以查阅《权威指南》的第 5 章中相关指令的讲解。 4、第 7 章 异常和中断大概了解一下 。 5、第 8 章 深入了解异常处理强烈建议仔细阅读 6、第 10 章 OS 支持特性 强烈建议仔细阅读 《权威指南》中的其他章节大家依据个人爱好来阅读由于《权威指南》讲解的内容非常的“底层”所以看起来可能会感觉晦涩难懂如果看不懂的话不要着急看不懂的地方就跳过先对 Cortex-M 的处理器有一个大概的了解就行了。 任务调度器开启 前面的所有例程中我们都是在 main()函数中先创建一个开始任务 start_task后面紧接着调用函数 vTaskStartScheduler()。这个函数的功能就是开启任务调度器的这个函数在文件tasks.c中有定义具体可自行查阅。 内部实现流程大致如下 (1)、创建空闲任务如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务优先级为 tskIDLE_PRIORITY宏 tskIDLE_PRIORITY 为 0也就是说空闲任务的优先级为最低。 (2)、如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的这个函数很简单大家就自行查阅一下。 (3)、关闭中断在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。 (4)、变量 xSchedulerRunning 设置为 pdTRUE表示调度器开始运行。 (5)、当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能此时需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS此宏用来配置一个定时器/计数器。 (6)、调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件比如滴答定时器、 FPU 单元和 PendSV 中断等等。 内核相关硬件初始化函数分析 关于上面的最后一点内核相关硬件初始化函数 xPortStartScheduler()分析如下 FreeRTOS 系统时钟是由滴答定时器来提供的而且任务切换也会用到 PendSV 中断这些硬件的初始化由函数 xPortStartScheduler()来完成缩减后的函数代码如下 (1)、设置 PendSV 的中断优先级为最低优先级。 (2)、设置滴答定时器的中断优先级为最低优先级。 (3)、调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期并且使能滴答定时 器的中断函数比较简单大家自行查阅分析。 (4)、初始化临界区嵌套计数器。 (5)、调用函数 prvStartFirstTask()开启第一个任务。 有一个问题那就是滴答定时器的定时周期以及中断开启不需要我们在移植初始化的时候配置吗待解决。 启动第一个任务 经过上面的操作以后我们就可以启动第一个任务了函数 prvStartFirstTask()用于启动第一个任务这是一个汇编函数函数源码如下 (1)、将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地(0X00000000)开始存储的不过有些应用可能需要在运行时修改或重定义向量表Cortex-M 处理器为此提供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08通过这个寄存器可以重新定义向量表比如在 STM32F103 的 ST 官方库中会通过函数 SystemInit()来设置VTOR 寄存器代码如下 SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET; //VTOR0x080000000X00 通过上面一行代码就将向量表开始地址重新定义到了 0X08000000向量表的起始地址存储的就是 MSP 初始值。关于向量表和向量表重定位的详细内容请参阅《权威指南》的“第 7 章 异常和中断”的 7.5 小节。 (2)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器也就是读取寄存器 VTOR 中的值并将其保存在 R0 寄存器中。这一行代码执行完就以后 R0 的值应该为0X08000000。 (3)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器也就是读取地址0X08000000处存储的数据并将其保存在 R0 寄存器中。我们知道向量表的起始地址保存的就是主栈指针MSP 的初始值这一行代码执行完以后寄存器 R0 就存储 MSP 的初始值。现在来看(1)、(2)、(3)这三步起始就是为了获取 MSP 的初始值而已 (4)、复位 MSPR0 中保存了 MSP 的初始值将其赋值给 MSP 就相当于复位 MSP。 (5)和(6)、使能中断关于这两个指令的详细内容请参考《权威指南》的“第 4 章 架构”的第 4.2.3 小节。 (7)和(8)、数据同步和指令同步屏障这两个指令的详细内容请参考《权威指南》的“第 5 章 指令集”的 5.6.13 小节。 (9)调用 SVC 指令触发 SVC 中断SVC 也叫做请求管理调用SVC 和 PendSV 异常对于OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。关于 SVC 的详细内容请参考《权威指南》的“第 10 章 OS 支持特性”的 10.3 小节。在 FreeRTOS中仅仅使用 SVC 异常来启动第一个任务后面的程序中就再也用不到 SVC 了。 SVC中断服务函数 在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断而第一个任务的启动就是在 SVC 中断服务函数中完成的SVC 中断服务函数应该为 SVC_Handler()但是FreeRTOSConfig.h 中通过#define 的方式重新定义为了 xPortPendSVHandler()如下 #define vPortSVCHandler SVC_Handler 函数 vPortSVCHandler()在文件 port.c 中定义这个函数也是用汇编写的函数源码如下 详细过程参考视频此处不赘述。 RTOS 系统的核心是任务管理而任务管理的核心是任务切换任务切换决定了任务的执行顺序任务切换效率的高低也决定了一款系统的性能尤其是对于实时操作系统。 任务切换场合 有两种场合会进行任务切换 ● 可以执行一个系统调用 ● 系统滴答定时器(SysTick)中断。 执行系统调用 执行系统调用就是执行 FreeRTOS系统提供的相关API函数比如任务切换函数 taskYIELD() FreeRTOS 有些 API 函数也会调用函数 taskYIELD()这些 API 函数都会导致任务切换这些 API 函数和任务切换函数 taskYIELD()都统称为系统调用。函数 taskYIELD()其实就是个宏在文件 task.h中有如下定义 #define taskYIELD() portYIELD() 函数 portYIELD()也是个宏在文件 portmacro.h 中有如下定义 通过向中断控制和状态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。 这样就可以在 PendSV 中断服务函数中进行任务切换了。 中断级的任务切换函数为 portYIELD_FROM_ISR()定义如下 可以看出 portYIELD_FROM_ISR()最终也是通过调用函数 portYIELD()来完成任务切换的。 系统滴答定时器(SysTick)中断 FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换滴答定时器中断服务函数如下 在滴答定时器中断服务函数中调用了 FreeRTOS 的 API 函数 xPortSysTickHandler()此函数源码如下 (1)、关闭中断 (2)、通过向中断控制和状态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。这样就可以在 PendSV 中断服务函数中进行任务切换了。 (3)、打开中断。 PendSV异常 PendSV(可挂起的系统调用)异常对 OS 操作非常重要其优先级可以通过编程设置。可以通过将中断控制和状态寄存器 ICSR 的 bit28也就是 PendSV 的挂起位置 1 来触发PendSV 中断。与 SVC 异常不同它是不精确的因此它的挂起状态可在更高优先级异常处理内设置且会在高优先级处理完成后执行。利用该特性若将 PendSV 设置为最低的异常优先级可以让 PendSV 异常处理在所有其他中断处理完成后执行这对于上下文切换非常有用也是各种 OS 设计中的关键。 中断的优先级永远高于任务的优先级用中断最低优先级的中断来切换任务既不会影响高优先级的中断的执行又能实现任务的切换。 在具有嵌入式 OS 的典型系统中处理时间被划分为了多个时间片。若系统中只有两个任 务这两个任务会交替执行如下图所示 在 OS 中任务调度器决定是否应该执行上下文切换如上图中任务切换都是由 SysTick中断执行每次它都会决定切换到一个不同的任务中。 若中断请求(IRQ)在 SysTick 异常前产生则 SysTick 异常可能会抢占 IRQ 的处理在这种情况下OS 不应该执行上下文切换否则中断请求 IRQ 处理就会被延迟而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。对于 Cortex-M3 和 Cortex-M4 处理器当存在活跃的异常服务时设计默认不允许返回到线程模式若存在活跃中断服务且 OS 试图返回到线程模式则将触发用法 fault如下图 所示。 在一些 OS 设计中要解决这个问题可以在运行中断服务时不执行上下文切换此时可以检查栈帧中的压栈 xPSR 或 NVIC 中的中断活跃状态寄存器。不过系统的性能可能会受到影响特别时当中断源在 SysTick 中断前后持续产生请求时这样上下文切换可能就没有执行的机会了。 为了解决这个问题PendSV 异常将上下文切换请求延迟到所有其他 IRQ 处理都已经完成后此时需要将 PendSV 设置为最低优先级。若 OS 需要执行上下文切换他会设置PendSV 的挂起状态并在 PendSV 异常内执行上下文切换。如下图所示 上图中事件的流水账记录如下 (1) 任务 A 呼叫 SVC 来请求任务切换例如等待某些工作完成 (2) OS 接收到请求做好上下文切换的准备并且 pend 一个 PendSV 异常。 (3) 当 CPU 退出 SVC 后它立即进入 PendSV从而执行上下文切换。 (4) 当 PendSV 执行完毕后将返回到任务 B同时进入线程模式。 (5) 发生了一个中断并且中断服务程序开始执行。 (6) 在 ISR 执行过程中发生 SysTick 异常并且抢占了该 ISR。 (7) OS 执行必要的操作然后 pend 起 PendSV 异常以作好上下文切换的准备。 (8) 当 SysTick 退出后回到先前被抢占的 ISR 中 ISR 继续执行 (9) ISR 执行完毕并退出后 PendSV 服务例程开始执行并且在里面执行上下文切换。 (10) 当 PendSV 执行完毕后回到任务 A同时系统再次进入线程模式。 讲解 PendSV 异常的原因就是让大家知道FreeRTOS 系统的任务切换最终都是在 PendSV 中断服务函数中完成的UCOS 也是在 PendSV 中断中完成任务切换的。 PendSV中断服务函数 前面说了 FreeRTOS 任务切换的具体过程是在 PendSV 中断服务函数中完成的接着我们就来学习PendSV 的中断服务函数看看任务切换过程究竟是怎么进行的。PendSV 中断服务函数本应该为 PendSV_Handler()但是 FreeRTOS 使用#define 重定义了如下 #define xPortPendSVHandler PendSV_Handler 该函数源码如下 (1)、读取进程栈指针保存在寄存器 R0 里面。 (2)和(3)获取当前任务的任务控制块并将任务控制块的地址保存在寄存器 R2 里面。 (4)、保存 r4~r11 和 R14 这几个寄存器的值。 (5)、将寄存器 R0 的值写入到寄存器 R2 所保存的地址中去也就是将新的栈顶保存在任务控制块的第一个字段中。此时的寄存器 R0 保存着最新的堆栈栈顶指针值所以要将这个最新的栈顶指针写入到当前任务的任务控制块第一个字段而经过(2)和(3)已经获取到了任务控制块并将任务控制块的首地址写如到了寄存器 R2 中。 (6)、将寄存器 R3 和 R14 的值临时压栈寄存器 R3 中保存了当前任务的任务控制块而接下来要调用函数 vTaskSwitchContext()为了防止 R3 和 R14 的值被改写所以这里临时将 R3和 R14 的值先压栈。 (7)和(8)、关闭中断进入临界区 (9)、调用函数 vTaskSwitchContext()此函数用来获取下一个要运行的任务并将 pxCurrentTCB 更新为这个要运行的任务。 (10)和(11)、打开中断退出临界区。 (12)、刚刚保存的寄存器 R3 和 R14 的值出栈恢复寄存器 R3 和 R14 的值。注意经过(12)步此时 pxCurrentTCB 的值已经改变了所以读取 R3 所保存的地址处的数据就会发现其值改变了成为了下一个要运行的任务的任务控制块。 (13)和(14)、获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在寄存器 R0 中。 (15)、R4~R11,R14 出栈也就是即将运行的任务的现场。 (16)、更新进程栈指针 PSP 的值。 (17)、执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值确定 异常返回以后应该进入处理器模式还是进程模式使用主栈指针(MSP)还是进程栈指针(PSP)。 很明显这里会进入进程模式并且使用进程栈指针(PSP)寄存器 PC 值会被恢复为即将运行的任务的任务函数新的任务开始运行至此任务切换成功。 总的来说其实就是保存之前的任务现场然后恢复下一个任务的现场。 查找下一个要运行的任务 在 PendSV 中断服务程序中有调用函数 vTaskSwitchContext()来获取下一个要运行的任务 也就是查找已经就绪了的优先级最高的任务。 该函数内部实现过程如下 (1)、如果调度器挂起那就不能进行任务切换。 (2)、调用函数 taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务。 taskSELECT_HIGHEST_PRIORITY_TASK()本质上是一个宏在 tasks.c 中有定义。 FreeRTOS 中查找下一个要运行的任务有两种方法一个是通用的方法另外一个就是使用硬件的方法这个在我们讲解 FreeRTOSCofnig.h 文件的时候就提到过了至于选择哪种方法通过宏configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定的。当这个宏为 1 的时候就使用硬件的方法否则的话就是使用通用的方法我们来看一下这两个方法的区别。 通用方法 顾名思义就是所有的处理器都可以用的方法通用方法是完全通过 C 语言来实现的肯定适用于不同的芯片和平台而且对于任务数量没有限制但是效率肯定相对于使用硬件方法的要低很多。 硬件方法 硬件方法就是使用处理器自带的硬件指令来实现的比如 Cortex-M 处理器就带有的计算前 导 0 个数指令CLZ。 如果使用硬件方法的话最多只能有 32 个优先级。 可以看出硬件方法借助一个指令就可以快速的获取处于就绪态的最高优先级但是会限制任务的优先级数比如 STM32 只能有 32 个优先级不过 32 个优先级已经完全够用了。要知道FreeRTOS 是支持时间片的每个优先级可以支持无限多个任务。 FreeRTOS 时间片调度 前面多次提到 FreeRTOS 支持多个任务同时拥有一个优先级这些任务的调度是一个值得考虑的问题不过这不是我们要考虑的。在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权让拥有同优先级的下一个任务运行至于下一个要运行哪个任务在上小节里面已经分析过了FreeRTOS 中的这种调度方法就是时间片调度。下图展示了运行在同一优先级下的执行时间图在优先级 N 下有 3 个就绪的任务。 1任务3正在运行。 2这时一个时钟节拍中断(滴答定时器中断)发生任务3的时间片用完但是任务3还 没有执行完。 3FreeRTOS 将任务切换到任务1任务1是优先级 N 下的下一个就绪任务。 4任务1连续运行至时间片用完。 5任务3再次获取到 CPU 使用权接着运行。 6任务3运行完成调用任务切换函数 portYIELD()强行进行任务切换放弃剩余的时间片 从而使优先级N下的下一个就绪的任务运行。 7FreeRTOS 切换到任务1。 8任务1执行完其时间片。 要使用时间片调度的话宏 configUSE_PREEMPTION 和宏 configUSE_TIME_SLICING 必须为 1。时间片的长度由宏 configTICK_RATE_HZ 来确定一个时间片的长度就是滴答定时器的中断周期比如本教程中 configTICK_RATE_HZ 为 1000那么一个时间片的长度就是 1ms。时间片调度发生在滴答定时器的中断服务函数中前面讲解滴答定时器中断服务函数的时候说了在中断服务函数 SysTick_Handler()中会调用 FreeRTOS 的 API 函数xPortSysTickHandler()而函数 xPortSysTickHandler() 会 引 发 任 务 调 度 但 是 这个 任 务 调 度 是 有 条 件 的 函 数xPortSysTickHandler()如下 上述代码中红色部分表明只有函数 xTaskIncrementTick()的返回值不为pdFALSE的时候就会进行任务调度查看函数 xTaskIncrementTick()会发现有如下条件编译语句 (1)、当宏 configUSE_PREEMPTION 和宏 configUSE_PREEMPTION 都为 1 的时候下面的代码才会编译。所以要想使用时间片调度的话这这两个宏都必须为 1缺一不可 (2)、判断当前任务所对应的优先级下是否还有其他的任务。 (3)、如果当前任务所对应的任务优先级下还有其他的任务那么就返回 pdTRUE。 从上面的代码可以看出如果当前任务所对应的优先级下有其他的任务存在那么函数xTaskIncrementTick() 就会返回pdTURE 由于函数返回值为 pdTURE因此函数xPortSysTickHandler()就会进行一次任务切换。 也就是说时间片调度只有在同一优先级下还有其他任务时才会进行任务切换。 遗留问题抢占式调度是怎么实现的