学习RTOS(4)任务

什么是任务
在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。 任务的大概形式具体见代码清单 7-2。
在这里插入图片描述

创建任务

创建任务

我们先回想下,在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生。那么系统在运行的时候,全局变量放在哪里,子函数调用时,局部变量放在哪里,中断发生时,函数返回地址放哪里。如果只是单纯的裸机编程,它们放哪里我们不用管,但是如果要写一个 RTOS,这些种种环境参数,我们必须弄清楚他们是如何存储的。在裸机系统中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由 C 库函数_main 进行初始化。

但是,在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM中。
本章我们要实现两个变量按照一定的频率轮流的翻转,每个变量对应一个任务,那么就需要定义两个任务栈,具体见代码清单 7-3。在多任务系统中,有多少个任务就需要定义多少个任务栈。
在这里插入图片描述

代码清单 7-3 (1):任务栈其实就是一个预先定义好的全局数据,数据类型为StackType_t,大小由TASK1_STACK_SIZE 这个宏来定义, 默认为 128,单位为字,即 512字节,这也是 FreeRTOS 推荐的最小的任务栈。

定义任务函数

任务是一个独立的函数,函数主体无限循环且不能返回。本章我们在 main.c 中定义的两个任务具体见代码清单 7-5。
在这里插入图片描述

定义任务控制块

在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。 定义一个任务控制块需要一个新的数据类型, 该数据类型在task.c 这 C 头文件中声明。
在这里插入图片描述

代码清单 7-6 (1):栈顶指针,作为 TCB 的第一个成员。
代码清单 7-6 (2):任务节点,这是一个内置在 TCB 控制块中的链表节点,通过这个节点,可以将任务控制块挂接到各种链表中。这个节点就类似晾衣架的钩子, TCB 就是衣服。
代码清单 7-6 (3):任务栈起始地址。
代码清单 7-6 (4):任务名称,字符串形式, 长度由宏 configMAX_TASK_NAME_LEN来控制,该宏在 FreeRTOSConfig.h 中定义,默认为 16。
代码清单 7-6 (5):数据类型重定义。

实现任务创建函数

任务的栈,任务的函数实体,任务的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由任务创建函数 xTaskCreateStatic()来实现,该函数在 task.c( task.c 第一次使用需要自行在文件夹 freertos 中新建并添加到工程的 freertos/source 组)中定义, 在 task.h 中声明, 所有跟任务相关的函数都在这个文件定义。 xTaskCreateStatic()函数的实现见代码清单 7-8。
在这里插入图片描述

代码清单 7-8 (1): FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。目前我们以静态创建为例来讲解,configSUPPORT_STATIC_ALLOCATION 在 FreeRTOSConfig.h 中定义,我们配置为1
代码清单 7-8 (2):任务入口,即任务的函数名称。 TaskFunction_t 是在 projdefs.h中重定义的一个数据类型,实际就是空指针。
代码清单 7-8 (3): 任务名称,字符串形式,方便调试。
代码清单 7-8 (4): 任务栈大小,单位为字。
代码清单 7-8 (5): 任务形参。
代码清单 7-8 (6): 任务栈起始地址。
代码清单 7-8 (7): 任务控制块指针。
代码清单 7-8 (8): 定义一个任务句柄 xReturn, 任务句柄用于指向任务的 TCB。 任务句柄的数据类型为 TaskHandle_t, 在 task.h 中定义,实际上就是一个空指针。
代码清单 7-8 (9): 调用 prvInitialiseNewTask()函数,创建新任务,该函数在 task.c 实现,具体实现见代码清单 7-11。
在这里插入图片描述

代码清单 7-11(1): 任务入口。
代码清单 7-11(2): 任务名称,字符串形式。
代码清单 7-11(3): 任务栈大小,单位为字。
代码清单 7-11(4): 任务形参。
代码清单 7-11(5): 任务句柄。
代码清单 7-11(6): 任务控制块指针。
代码清单 7-11(7): 获取栈顶地址。
代码清单 7-11(8):将栈顶指针向下做 8 字节对齐。 在 Cortex-M3( Cortex-M4 或Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行,可这样为啥要 8 字节?难道有哪些操作是 64 位的?确实有,那就是浮点运算,所以要 8 字节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。如果栈顶指针是 8 字节对齐的,在进行向下 8 字节对齐的时候,指针不会移动,如果不是8 字节对齐的,在做向下 8 字节对齐的时候,就会空出几个字节,不会使用,比如当pxTopOfStack 是 33,明显不能整除 8,进行向下 8 字节对齐就是 32,那么就会空出一个字节不使用。
代码清单 7-11(9): 将任务的名字存储在 TCB 中。
代码清单 7-11(10):任务名字的长度不能超过 configMAX_TASK_NAME_LEN, 并以’\0’结尾。
代码清单 7-11(11):初始化 TCB 中的 xStateListItem 节点, 即初始化该节点所在的链表为空,表示节点还没有插入任何链表。
代码清单 7-11(12):设置 xStateListItem 节点的拥有者,即拥有这个节点本身的 TCB。
代码清单 7-11(13):调用 pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。该函数在 port.c( port.c 第一次使用需要在freertos\portable\RVDS\ARM_CM3( ARM_CM4 或 ARM_CM7) 文件夹下面新建然后添加到工程freertos/source 这个组文件)中定义,具体实现见代码清单 7-12。 任务栈初始化完毕之后,栈空间内部分布图具体见图 7-3。在这里插入图片描述
在这里插入图片描述

代码清单 7-12(1): 异常发生时, CPU 自动从栈中加载到 CPU 寄存器的内容。包括 8个寄存器,分别为 R0、 R1、 R2、 R3、 R12、 R14、 R15 和 xPSR 的位 24,且顺序不能变。
代码清单 7-12(2): xPSR 的 bit24 必须置 1,即 0x01000000。
代码清单 7-12(3): 任务的入口地址。
代码清单 7-12(4): 任务的返回地址,通常任务是不会返回的,如果返回了就跳转到prvTaskExitError, 该函数是一个无限循环。
代码清单 7-12(5): R12, R3, R2 and R1 默认初始化为 0。
代码清单 7-12(6): 异常发生时,需要手动加载到 CPU 寄存器的内容, 总共有 8 个,
代码清单 7-12(7): 返回栈顶指针,此时 pxTopOfStack 指向具体见图 7-3。 任务第一次运行时,就是从这个栈指针开始手动加载 8 个字的内容到 CPU 寄存器: R4、 R5、 R6、 R7、R8、 R9、 R10 和 R11,当退出异常时,栈中剩下的 8 个字的内容会自动加载到 CPU 寄存器:R0、 R1、 R2、 R3、 R12、 R14、 R15 和 xPSR 的位 24。此时 PC 指针就指向了任务入口地址,从而成功跳转到第一个任务。
代码清单 7-11(14): 让任务句柄指向任务控制块。
代码清单 7-8 (10): 返回任务句柄,如果任务创建成功,此时 xReturn 应该指向任务控制块, xReturn 作为形参传入到 prvInitialiseNewTask 函数。

实现就绪列表

定义就绪列表

任务创建好之后,我们需要把任务添加到就绪列表里面, 表示任务已经就绪,系统随时可以调度。 就绪列表在 task.c 中定义。

List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /* 任务就绪列表 */

就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最大任务优先级的宏configMAX_PRIORITIES 决定, configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。一个空的就绪列表具体见图 7-4。
在这里插入图片描述

就绪列表初始化

就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists()里面实现。
在这里插入图片描述
在这里插入图片描述

将任务插入到就绪列表

任务控制块里面有一个 xStateListItem 成员, 数据类型为 ListItem_t, 我们将任务插入到就绪列表里面,就是通过将任务控制块的 xStateListItem 这个节点插入到就绪列表中来实现的。如果把就绪列表比作是晾衣架, 任务是衣服,那 xStateListItem 就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中。
在这里插入图片描述

就绪列表的下标对应的是任务的优先级,但是目前我们的任务还不支持优先级,有关支持多优先级的知识点我们后面会讲到,所以 Task1 和 Task2 任务在插入到就绪列表的时候,可以随便选择插入的位置。 在代码清单 7-15 中, 我们选择将 Task1 任务插入到就绪列表下标为 1 的链表中, Task2 任务插入到就绪列表下标为 2 的链表中,具体的示意图见图7-6。
在这里插入图片描述

实现调度器

调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。从代码上来看,调度器无非也就是由几个全局变量和一些可以实现任务切换的函数组成,全部都在 task.c 文件中实现。
在这里插入图片描述

启动调度器

调度器的启动由 vTaskStartScheduler()函数来完成,该函数在 task.c 中定义。

代码清单 7-16(1): pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。 目前我们还不支持优先级,则手动指定第一个要运行的任务。
代码清单 7-16(2):调用函数 xPortStartScheduler()启动调度器, 调度器启动成功, 则不会返回。 该函数在 port.c 中实现,具体见代码清单 7-17。
在这里插入图片描述

代码清单 7-17 (1): 配置 PendSV 和 SysTick 的中断优先级为最低。 SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级, 即优先相应系统中的外部硬件中断, 所以 SysTick 和 PendSV 的中断优先级配置为最低。
代码清单 7-17 (2): 调用函数 prvStartFirstTask()启动第一个任务, 启动成功后, 则不再返回,该函数由汇编编写, 在 port.c 实现,具体代码见代码清单 7-18。

prvStartFirstTask()函数用于开始第一个任务,主要做了两个动作,一个是更新 MSP 的值,二是产生 SVC 系统调用,然后去到 SVC 的中断服务函数里面真正切换到第一个任务。
在这里插入图片描述

代码清单 7-18(1):
代码清单 7-18(2): 当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可。在 Cortex-M 中浮点运算是 8 字节的。
代码清单 7-18(3): 在 Cortex-M 中, 0xE000ED08 是 SCB_VTOR 寄存器的地址, 里面存放的是向量表的起始地址,即 MSP 的地址。 向量表通常是从内部 FLASH 的起始地址开始存放,那么可知 memory: 0x00000000 处存放的就是 MSP 的值。 这个可以通过仿真时查看内存的值证实,具体见图 7-7。在这里插入图片描述

代码清单 7-18(4): 将 0xE000ED08 这个立即数加载到寄存器 R0。
代码清单 7-18(5): 将 0xE000ED08 这个地址指向的内容加载到寄存器 R0,此时 R0等于 SCB_VTOR 寄存器的值, 等于 0x00000000,即 memory 的起始地址。
代码清单 7-18(6): 将 0x00000000 这个地址指向的内容加载到 R0,此时 R0 等于0x200008DB,与图 7-7 查询到的值吻合。
代码清单 7-18(7): 将 R0 的值存储到 MSP,此时 MSP 等于 0x200008DB,这是主堆栈的栈顶指针。起始这一步操作有点多余,因为当系统启动的时候,执行完 Reset_Handler的时候, 向量表已经初始化完毕, MSP 的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针。
代码清单 7-18(8): 使用 CPS 指令把全局中断打开。 为了快速地开关中断, Cortex-M内核 专门设置了一条 CPS 指令,有 4 种用法,具体见代码清单 7-19。在这里插入图片描述

代码清单 7-19 中 PRIMASK 和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,有关这三个寄存器的详细用法见表格 7-1。在这里插入图片描述

代码清单 7-18(9):产生系统调用,服务号 0 表示 SVC 中断,接下来将会执行 SVC 中断服务函数。

SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中, SVC 的中断服务函数注册的名称是 SVC_Handler, 所以 SVC 中断服务函数的名称我们应该写成 SVC_Handler, 但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler(),为了能够顺利的响应 SVC 中断,我们有两个选择,改中断向量表中 SVC 的注册的函数名称或者改 FreeRTOS 中 SVC 的中断服务名称。这里,我们采取第二种方法,即在FreeRTOSConfig.h 中添加添加宏定义的方法来修改,具体见代码清单 7-20, 顺便把PendSV 和 SysTick 的中断服务函数名也改成与向量表的一致。
在这里插入图片描述

vPortSVCHandler()函数开始真正启动第一个任务,不再返回,实现具体见代码清单7-21。在这里插入图片描述

代码清单 7-21(1):声明外部变量 pxCurrentTCB, pxCurrentTCB 是一个在 task.c 中定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。
代码清单 7-21(2): 加载 pxCurrentTCB 的地址到 r3。
代码清单 7-21(3): 加载 pxCurrentTCB 到 r1。
代码清单 7-21(4): 加载 pxCurrentTCB 指向的任务控制块到 r0,任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针。 一个刚刚被创建还没有运行过的任务的栈空间分布具体如图 7-8 所示,即 r0 等于图 7-8 的 pxTopOfStack。在这里插入图片描述

代码清单 7-21(5): 以 r0 为基地址,将栈中向上增长的 8 个字的内容加载到 CPU 寄存器 r4~r11,同时 r0 也会跟着自增。
代码清单 7-21(6): 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp。
代码清单 7-21(7):将寄存器 r0 清 0。
代码清单 7-21(8):设置 basepri 寄存器的值为 0,即打开所有中断。 basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。
代码清单 7-21(9): 当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上0x0D,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态。在 SVC 中断服务里面,使用的是 MSP 堆栈指针, 是处在 ARM 状态。
当 r14 为 0xFFFFFFFX,执行是中断返回指令, cortext-m3 的做法, X 的 bit0 为 1 表示返回 thumb 状态, bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还是用户模式
代码清单 7-21(10):异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR, PC(任务入口地址), R14, R12, R3, R2, R1, R0(任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶,具体指向见图 7-9。在这里插入图片描述

任务切换

任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。但是目前我们还不支持优先级,仅实现两个任务轮流切换。在这里插入图片描述
在这里插入图片描述

代码清单 7-22(1): portYIELD 的实现很简单,实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV 中断服务函数,在里面实现任务切换。
在这里插入图片描述

代码清单 7-23(1): 声明外部变量 pxCurrentTCB, pxCurrentTCB 是一个在 task.c 中定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。
代码清单 7-23(2): 声明外部函数 vTaskSwitchContext, 等下会用到。
代码清单 7-23(3): 当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可。在 Cortex-M 中浮点运算是 8 字节的。
代码清单 7-23(4): 将 PSP 的值存储到 r0。 当进入 PendSVC Handler 时,上一个任务运行的环境即: xPSR, PC(任务入口地址), R14, R12, R3, R2, R1, R0(任务的形参)这些 CPU 寄存器的值会自动存储到任务的栈中,剩下的 r4~r11 需要手动保存,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶) ,此时 PSP 具体指向见图 7-10。
在这里插入图片描述

代码清单 7-23(5): 加载 pxCurrentTCB 的地址到 r3。
代码清单 7-23(6): 加载 r3 指向的内容到 r2,即 r2 等于 pxCurrentTCB。
代码清单 7-23(7): 以 r0 作为基址(指针先递减,再操作, STMDB 的 DB 表示Decrease Befor),将 CPU 寄存器 r4~r11 的值存储到任务栈,同时更新 r0 的值,此时 r0 的指向具体见。
在这里插入图片描述

代码清单 7-23(8): 将 r0 的值存储到 r2 指向的内容, r2 等于 pxCurrentTCB。 具体为将r0 的值存储到上一个任务的栈顶指针 pxTopOfStack,具体指向如图 7-11 的 r0 指向一样。到此, 上下文切换中的上文保存就完成了。
代码清单 7-23(9): 将 R3 和 R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈,栈指针使用的是 MSP),因为接下来要调用函数 vTaskSwitchContext,调用函数时,返回地址自动保存到 R14 中,所以一旦调用发生, R14 的值会被覆盖( PendSV 中断服务函数执行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,出栈时使用的是 PSP 还是 MSP) ,因此需要入栈保护。 R3 保存的是当前正在运行的任务(准确来说是上文,因为接下来即将要切换到新的任务)的 TCB 指针(pxCurrentTCB)地址,函数调用后 pxCurrentTCB 的值会被更新,后面我们还需要通过 R3 来操作 pxCurrentTCB,但是运行函数 vTaskSwitchContext 时不确定会不会使用 R3 寄存器作为中间变量, 所以为了保险起见, R3 也入栈保护起来。

代码清单 7-23(10): 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值存储到r0,该宏在FreeRTOSConfig.h 中定义,用来配置中断屏蔽寄存器 BASEPRI 的值, 高四位有效。目前配置为 191,因为是高四位有效,所以实际值等于 11,即优先级高于或者等于11 的中断都将被屏蔽。在关中断方面,FreeRTOS 与其它的 RTOS 关中断不同,而是操作BASEPRI 寄存器来预留一部分中断,并不像 μC/OS 或者 RT-Thread 那样直接操作PRIMASK 把所有中断都关闭掉(除了硬 FAULT) 。
代码清单 7-23(11): 关中断,进入临界段,因为接下来要更新全局指针 pxCurrentTCB的值。
代码清单 7-23(12): 调用函数 vTaskSwitchContext。该函数在 task.c 中定义,作用只有一个,选择优先级最高的任务,然后更新 pxCurrentTCB。目前我们还不支持优先级,则手动切换,不是任务 1 就是任务 2,该函数的具体实现见代码清单 7-24 vTaskSwitchContext()函数。
在这里插入图片描述

代码清单 7-24(1):如果当前任务为任务 1,则把下一个要运行的任务改为任务 2。
代码清单 7-24(2):如果当前任务为任务 2,则把下一个要运行的任务改为任务 1。

代码清单 7-23(13): 退出临界段, 开中断,直接往 BASEPRI 写 0。
代码清单 7-23(14): 从主堆栈中恢复寄存器 r3 和 r14 的值,此时的 sp 使用的是 MSP。
代码清单 7-23(15): 加载 r3 指向的内容到 r1。 r3 存放的是 pxCurrentTCB 的地址, 即
让 r1 等于 pxCurrentTCB。 pxCurrentTCB 在上面的 vTaskSwitchContext 函数中被更新, 指
向了下一个将要运行的任务的 TCB。
代码清单 7-23(16): 加载 r1 指向的内容到 r0,即下一个要运行的任务的栈顶指针。
代码清单 7-23(17): 以 r0 作为基地址(先取值,再递增指针, LDMIA 的 IA 表示Increase After),将下一个要运行的任务的任务栈的内容加载到 CPU 寄存器 r4~r11。
代码清单 7-23(18): 更新 psp 的值,等下异常退出时,会以 psp 作为基地址,将任务栈中剩下的内容自动加载到 CPU 寄存器。
代码清单 7-23(19): 异常发生时, R14 中保存异常返回标志,包括返回后进入任务模式还是处理器模式、使用 PSP 堆栈指针还是 MSP 堆栈指针。此时的 r14 等于 0xfffffffd,表示异常返回后进入任务模式,SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向任务栈的栈顶。当调用 bx r14 指令后,系统以 PSP 作为 SP 指针出栈,把接下来要运行的新任务的任务栈中剩下的内容加载到 CPU 寄存器: R0(任务形参)、 R1、 R2、 R3、 R12、 R14( LR)、 R15( PC)和 xPSR,从而切换到新的任务。

main 函数

任务的创建,就绪列表的实现,调度器的实现,现在把全部的测试代码都放到 main.c 里面。

在这里插入图片描述

查看输出波形。
在这里插入图片描述

先手动触发 SVC_Handler 中断
然后在里面 挂起 PendSV_Handler 中断
最后进入到 PendSV_Handler 中断完成任务切换

在这里插入图片描述

Hankin
2020.08.21

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值