FreeRTOS 任务调度

学习调度器之前的一些建议:
本讲的内容和 Cortex-M 处理器的内核架构密切联系,所以学习之前建议大家:

  1. 提前阅读《Cortex M3权威指南(中文)》和《Cortex M3与M4权威指南》
  2. 结合文档教程《FreeRTOS开发指南》第八章进行学习

1. 开启任务调度器

vTaskStartScheduler() 

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

该函数内部实现,如下:

  1. 创建空闲任务
  2. 如果使能软件定时器,则创建定时器任务
  3. 关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
  4. 初始化全局变量
    • 下一个任务的阻塞超时时间,初始值是最大值 0xFFFF FFFF。(还没有一个任务是在运行的)
    • 并将任务调度器的运行标志设置为已运行(pdTRUE)
    • 系统节拍计数器,初始值是 0。(滴答定时器每中断一次这个变量就会加一,给我们的系统提供心跳节拍就是靠这个变量。)
  5. 初始化任务运行时间统计功能的时基定时器(未实现)
  6. 调用函数 xPortStartScheduler()
xPortStartScheduler() 

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

该函数内部实现,如下:

  1. 检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级
  3. 调用函数 vPortSetupTimerInterrupt() 配置 SysTick(初始化滴答定时器)
  4. 初始化临界区嵌套计数器为 0
  5. 调用函数 prvEnableVFP() 使能 FPU
  6. 调用函数 prvStartFirstTask() 启动第一个任务

注意:运行完 xPortStartScheduler() 之后是不会再返回了,直接跳到第一个任务去执行了。

初始化滴答定时器:

  1. 配置重装载值寄存器:CPU 时钟(系统时钟/CPU 主频 )/ 系统节拍 = 180M / 1000 = 180000。当它倒数数至 0 时,就会触发中断。
    所以大家注意,这个 180000 它是会一直递减,减到 0 的时候就会触发一次中断了,那么这里要注意它的一个时钟源是多少。
  2. 滴答定时器的时钟源通过 0xE000 E010 这个控制寄存器,它的位 2 来决定:如果是等于 1 的话,那么就是内核时钟,也就是 180M;如果你是外部时钟的话,那么就是 180M 要除以 8。

那我们这里:

  1. 当它倒数到 0 时产生 SysTick 异常请求,那这个就是使能它的中断。
  2. 滴答定时器的时钟源选择内核时钟,也就是 180M

我们刚刚已经计算了重装载值是 180 000,然后滴答定时器的时钟源是内核时钟,也就是 180M,这是它计数频率,那么它的一个计数时间是多少,计数一次的时间就是它频率的倒数 ,1/180M ,那这个是他计数一次所花的时间。那大家要注意了,滴答定时器的计数器是向下计数,也就是说你计数一次,我们这个重装载值就减一,再计数一次又减一,减到 0 的时候,此时会怎样?当它倒数到 0 的时候,就会产生滴答定时器异常请求,也就是滴答中断了,所以呢你要计数 180000 这么多次,就是你减 180000 这么多次,你就会减到 0 嘛,那么此时你所花的时间是多少,也就是 180000 * 1/180M(你计数一次的时间)= 1/1000,这个单位是秒的,那我换算成毫秒,其实就是 1ms,所以我们滴答定时器中断时间就是 1ms 中断一次,就是这么算出来的。这个是滴答定时器初始化的描述。

一、对于 M3 来说是没有 FPU 的,那 prvEnableVFP() 这个函数主要针对是 M4 以及 M7 的,因为 M4、M7 才有 FPU。

  1. 那 prvEnableVFP() 函数里的话主要就是一些汇编,那对于这些汇编指令,大家如果不熟悉,可以看一下 M3 的一个权威指南,指令集这里呢应该都有,那这里有汇编语言的基础,那这里就一堆指令,那大家如果不熟悉的话,可以去看一下 M3 的权威指南去学习一下。
  2. 那么这里呢主要操作就是 0xE000ED88 这个寄存器,那大家注意我们要找这个寄存器的时候是不能在 M3 这里找的,因为 M3 是没有 FPU 的,所以大家找一定要在 M3 以及 M4 的权威指南这里去查找。找到描述的地方,其实我们操作就是 20、21(CP10)以及 22、23(CP11)这四个位,这里有对这几个位的一个描述,CP10 以及 CP11 它的一个设置,如果你都设置成 11 的话,代表完全访问,也就是使能 FPU,就这么一个作用。
  3. 我们看一下它这里其实也是这么操作的,orr 是一个或操作,ldr 取 r0 这个地址它的一个值到这个 r1,然后将这个 r1 进行一个或操作,这里 0xf 是什么,就是 0x1111,然后左移 20 位,20 位是不是就是这里啊,20、21、22、23 都是这 1111 呀,就这么一个意思,也就完全访问。那这里简单说一下,就这样。

二、把 FPU 的控制数据寄存器(0xE000 EF34)的 30 以及 31 全部给置 1,在进出异常时,自动保存和恢复 FPU 相关寄存器。

  1. 那这个就 M4 以及 M7 才有的,M3 是没有 FPU 的,所以它不适用。
  2. 那么此时呢 FPU 它是有 32 个寄存器的,S0~S31。当进出异常时,也就是进出中断时,它的 S0~S15 是会自动保存和恢复的;S16~S31 是需要手动恢复和保存的。

那它就是这么一个惰性压栈的一个作用。

2. 启动第一个任务

我们主要讲解两个函数:

prvStartFirstTask () 	/* 开启第一个任务 */
vPortSVCHandler () 		/* SVC中断服务函数 */

prvStartFirstTask() 函数里都是一些汇编语言。SVC 中断会在 prvStartFirstTask() 函数里被触发。

想象下 OS 应该如何启动第一个任务?

比如 OS 创建了多个任务,那么多个任务 OS 怎么知道启动哪一个?肯定从任务的优先级来判断,首先 OS 第一个启动的肯定是优先级最高的。

假设我知道了当然任务优先级最高的是任务 A(我要启动的第一个任务是任务A),那么我要让 OS 运行任务 A,怎么运行?就需要将任务A的寄存器值恢复到 CPU 寄存器,也就是任务 A 的寄存器值,给它加载到 CPU 寄存器,此时 CPU 的寄存器是任务 A 的寄存器,那么 CPU 就会执行任务 A了。

任务 A 的寄存器值,在一开始创建任务时就保存在任务堆栈里边!

注意:

  1. 中断产生时,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0 保存到任务堆栈和恢复到 CPU 寄存器(出/入栈);而R4~R11需要手动保存和恢复。(如果我们想要任务 A 去运行,那么我们就将任务 A 的寄存器恢复到 CPU 的寄存器,那我们需要手动操作恢复哪一些,就只有 R4~R11,像 xPSR,PC(R15),LR(R14),R12,R3-R0 这些寄存器是硬件自动恢复的,不需要我们管)
  2. 进入中断后硬件会强制使用 MSP 指针 ,此时 LR(R14)的值将会被自动被更新为特殊的 EXC_RETURN

关于这些寄存器功能,可查看《 Cortex M3权威指南(中文) 》第37页

堆栈指针 R13:

  • 主堆栈指针(MSP)
  • 进程堆栈指针(PSP)

在 FreeRTOS 中断中使用的是 MSP 指针,而在进程中(除了中断以外)就是使用了 PSP 指针。

2.1 prvStartFirstTask ()

用于初始化启动第一个任务前的环境,主要是重新设置 MSP 指针,并使能全局中断(触发 SVC 中断)。

  1. 八字节对齐。

因为栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐。在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。

  1. 获取 MSP 的初始值(栈顶指针)。
  2. 初始化 MSP。

为什么要这样操作?因为我们上电的时候默认就是使用的 MSP 来作为主堆栈指针,那我们从上电跑到这里,经过了一系列的函数调用,出栈入栈,MSP 早就不是一开始的那一个位置了,所以我们这里重新把 MSP 的初始值 赋值给它,那这样 MSP 是不是回到原点了,那我们前面的那一些寄存器,我们就不需要去保存了,我们这里的话就直接给丢掉了,因为我们现在不需要再回去了,我们只要启动了第一个任务之后,后面的一个执行,都在任务与任务之间的,我们不会再回去了,这是一条不归路。

  1. 使能全局中断,触发 SVC 的一个 0 号中断。

  2. 调用 SVC 启动第一个任务。

所以启动第一个任务主要就在 SVC 中断服务函数中实现的。

问题:

  1. 什么是MSP指针?

程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新 SP 指针(R13),ARM Cortex-M 内核提供了两个栈空间:

  • 主堆栈指针(MSP)它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用
  • 进程堆栈指针(PSP)用于常规的应用程序代码(不处于异常服务例程中时)。

在FreeRTOS中,中断使用 MSP(主堆栈),中断以外使用 PSP(进程堆栈)。也就是说在 FreeRTOS 中使用的是双堆栈指针,两个互不干扰,这样使我们的进程更加安全;像裸机的话,不管是中断还是进程,都使用的是 MSP 主堆栈指针。

  1. 为什么要获取 0xE000ED08?

因为这个地址就是向量表偏移量寄存器的地址。向量表的第一个成员是 MSP 指针!也就是 MSP 的初始值。

取 MSP 的初始值的思路是先根据向量表的位置寄存器 VTOR (0xE000ED08) 来获取向量表存储的地址;在根据向量表存储的地址,来访问第一个元素,也就是初始的 MSP;CM3 允许向量表重定位——从其它地址处开始定位各异常向量;这个就是向量表偏移量寄存器,向量表的起始地址保存的就是主栈指针MSP 的初始值。

2.2 SVC 中断服务函数 vPortSVCHandler ()

注意:SVC 中断只在启动第一次任务时会调用一次,以后均不调用。像后面的任务切换都是在 PendSV 里面进行实现的。

当使能了全局中断,并且手动触发 SVC 中断后,就会进入到 SVC 的中断服务函数中。

操作步骤:

  1. 8 字节对齐。
  2. 通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务控制块的地址,优先级最高的就绪态任务是系统将要运行的任务;然后通过任务控制块的地址来获取它的首成员的地址;再通过首成员的地址来获取首成员的值。那这时候首成员的值就是栈顶指针
  3. 通过任务的栈顶指针,来手动出栈,将任务栈中的内容(R4~R11、R14)出栈到 CPU 寄存器中(每出栈一次地址 + 4),任务栈中的内容在调用任务创建函数的时候,已初始化。
  4. 然后就把 r0 这个地址赋值给 PSP 指针,因为一会退出中断的时候,要从 PSP 自动恢复这些寄存器,所以此时 r0 指向是哪里,那么退出中断之后,会根据 PSP(r0 指向的位置)进行恢复这些寄存器,它这里会自动恢复的,就不需要我们操作了。我们的操作就把 r0 赋值给 PSP,设置 PSP 为任务栈指针。
  5. 通过往 BASEPRI 寄存器中写 0,就代表开启所有中断。
  6. 最后就是返回 r14。这时候 r14 就为 EXC_RETURN,那这个是特殊值,那么它的一个末尾都是 D,要么是 FD,要么是 ED,但不管是哪一个 D,那么退出中断之后,所使用的都是 PSP 进程堆栈指针,所以此时呢 PSP 指向了任务栈。

然后呢当退出中断(执行 bx r14 指令)时,那么此时它就会跳转到任务的任务函数中(PC 寄存器)去执行,CPU 就会自动从 PSP 出栈硬件自动出栈的、不需要我们去手动去管理的寄存器。
为什么手动出栈的时候包含 R14 呢?

那这里啊就要区分了。M3 系列是没有 R14 的这个出栈操作的,那大家可以去看一下源码是没有的。而 M4 以及 M7 这些系列在这里它就有一个出栈 R14 的一个操作,因为 M4 以及 M7 是支持 FPU 的。我们说过,在我们进入中断之后,R14 会等于一个特殊值 EXC_RETURN,而这个特殊值它不同的一个值,它表示不同的意义,我们可以看一下:

而EXC_RETURN 只有 6 个合法的值(M4、M7),如下表所示:
在这里插入图片描述

  • 那我们说过,在 FreeRTOS 中,中断返回之后使用的是 PSP,在中断里面用的是 MSP,中断以外的都是用的 PSP,所以要的是这个 PSP 的。所以 PSP 呢就两个,一个是使用浮点,一个是未使用浮点,所以 FFD 就是未使用浮点单元,而 FFED 就是使用浮点单元。但是大家注意 M3 它是没有浮点单元不支持的,所以 M3 的话它只有右边三个合法值,而 M4、M7 就 6 个合法值了。所以我们这里还需要恢复一个 EXC_RETURN 的值,就代表你是支持还是不支持浮点单元,所以我们这里多了一步恢复 R14 的值。
  • R14 的值在创建任务初始化堆栈的时候是给它赋了一个初始值的,就是 FFD,也就是不使用浮点单元。到时候它会把 FFD 赋值给 R14,实际上就代表它未使用浮点单元。

2.3 仿真

3. 出栈/压栈汇编指令详解

  1. 出栈(恢复现场):把内存里面的值出栈到 CPU 寄存器里面

方向:从下往上(低地址往高地址)

假设 r0 地址为 0x04 汇编指令示例:

ldmia r0!, {r4-r6}   /* 任务栈 r0 地址由低到高,将 r0 存储地址里面的内容手动加载到 CPU 寄存器 r4、r5、r6 */
  • r0地址(0x04)内容加载到r4,此时地址r0 = r0+4 = 0x08
  • r0地址(0x08)内容加载到r5,此时地址r0 = r0+4 = 0x0C
  • r0地址(0x0C)内容加载到r6,此时地址r0 = r0+4 = 0x10
  1. 压栈(保存现场):把 CPU 寄存器给保存到内存里面

方向:从上往下(高地址往低地址)

假设 r0 地址为 0x10 汇编指令示例:

stmdb r0!, {r4-r6} }   /* r0 的存储地址由高到低递减,将 r4、r5、r6 里的内容存储到 r0 的任务栈里面。 */

地址:r0 = r0-4 = 0x0C,将r6的内容(寄存器值)存放到r0所指向地址(0x0C)
地址:r0 = r0-4 = 0x08,将r5的内容(寄存器值)存放到r0所指向地址(0x08)
地址:r0 = r0-4 = 0x04,将r4的内容(寄存器值)存放到r0所指向地址(0x04)

4. 任务切换

任务切换的本质:就是 CPU 寄存器的切换。

假设当由任务 A 切换到任务 B 时,主要分为两步:

  • 第一步:需暂停任务A的执行,并将此时任务A的寄存器(CPU寄存器)保存到任务堆栈,这个过程叫做保存现场
  • 第二步:将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场

对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换

在这里插入图片描述
那这时候 CPU 就会去执行任务 B 的任务函数。

注意:任务切换的过程在 PendSV 中断服务函数里边完成(实现)

PendSV 中断是如何触发的?

  1. 滴答定时器中断调用
    • 阻塞超时时间到了
    • 时间片调度
  2. 执行 FreeRTOS 提供的相关 API 函数:主要调用 portYIELD() 这个函数。
    • 延时函数
    • 恢复任务调度器
    • 等等

本质:通过向中断控制和状态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断

在这里插入图片描述
上表摘取于《Cortex M3权威指南(中文)》第131页。

4.1 PendSV 中断服务函数实现步骤

一、PendSV的任务切换操作(入栈,即保存现场)

  1. 8 字节对齐

  2. 把 PSP 任务栈赋值给 r0,此时 r0 保存的就是任务栈。为什么?PSP 是在中断以外所使用的堆栈,中断以内使用的是 MSP 主堆栈指针,那么现在这是中断服务函数里面,在中断以内,所以这里使用的是 MSP,那它外面用的就是 PSP。那大家想一下,任务在执行的时候,它是在中断以外,所以任务在运行的时候用的是 PSP,所以 PSP 保存的就是任务栈。

那么接着看一下任务切换示意图,当前任务 A 正在执行,那么此时它在中断以外,所以使用的是 PSP 进程堆栈指针,所以 PSP 就是它的任务栈指针,那这时候我们要切换到任务 b,怎么切换呢?我们说了它首先会自动保存,也就自动压栈部分的寄存器,那大家想一下这部分的自动保存它是发生在中断里面还是发送在中断以外?

这两个有什么区别,就决定了你使用的是 PSP 还是 MSP,中断以外的那就是 PSP,那中断里面保存的那就是 MSP。M3 权威指南里有个
M3 的双堆栈机制:在中断以外用的是 PSP,那中断里面用的是 MSP,退出中断以后也是用的 PSP,我们要的就是这样的一个场景:
在这里插入图片描述
大家看,这里在进入中断之前,它就会自动压栈;然后退出中断之后它就会自动出栈。

进入异常(中断之前)的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP,退出异常时切换回 PSP,并且从进程堆栈上弹出数据,也就是恢复、保存压栈用的都是 PSP。我们就可以从这张图看出不管你是自动压栈还是自动出栈,用的都是 PSP 进程堆栈指针。

那么在进入中断之前,要自动压栈,那此时呢任务 A 用的是 PSP,那现在把这些寄存器给恢复了任务 A 的任务堆栈里面,怎么压?压栈的话就高地址往低地址压,所以 PSP 它地址一开始在任务堆栈的最顶部,然后压到由硬件自动保存 R0 这里,所以此时 PSP 它地址会一直减到这里,所以最终呢硬件自动保存之后 PSP 指向 R0 这个地址。

硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0 使用 PSP 压入/出任务堆栈中,

现在我把 PSP 赋值给 r0,那么 r0 指向 R0 这个地址,那指向这里有什么用?后面是要手动压栈的,要根据这个地址往下继续压,压手动操作的寄存器进来,然后呢就可以压到 R4 这里,下面也有这么一个操作的。

  1. 获取当前运行任务的栈顶地址。R3 等于 pxCurrentTCB 的地址,再从任务控制块的地址取它的一个值(结构体的第一个成员)出来,那它的值就是栈顶指针,所以 R2 保存的任务控制块的栈顶地址。
ldr r3, =pxCurrentTCB 
ldr r2, [ r3 ] 

栈顶指针保存的是 R4 的地址,那么现在我们要压栈,r0 现在指向是 R0 的地址,但是后面要手动压栈,压栈之后,r0 是不是会改变,会一直往下走,走到 R4 的地址,走到这里之后要干嘛?我们再把这个地址赋值给栈顶指针,那么后面要恢复任务是不是就简单了,我们要恢复怎么恢复啊,就找到这个栈顶指针所指向这个地址,给它往上恢复这些寄存器(手动+自动),那这个任务是不是继续接着执行啊,接着之前被打断点去执行。

  1. r14 判断 bit4 是不是 1。如果是 1,就代表是不使用浮点数的;如果是 0,那代表是使用浮点数的。如果使用了浮点数,那么还需要压栈保存 s16~s31 这些寄存器,因为浮点单元有 32 个寄存器,就是 s0~s31,然后 s0~s15 是自动保存恢复的,那 s16~s31 是需要手动保存恢复的,这个要注意。这里我们是 F,也就是 1,那我们就是不使用这个浮点单元,所以我们这里可以不用看。

  2. 压栈,从上往下压,将r0的值,当压栈的起始地址,开始压栈

stmdb r0!, {r4-r11, r14}

如:先压r14,r0 = r14(即将r14中的内容放入r0所指的内存地址),接着r0 = r0 - 4,再压r11,r0 = r11 …压栈向下长,高到低,此时r0的值为所保存的这些数据的最底部的一个地址,手动操作的寄存器值都保存到任务堆栈里面,只要我们按照地址往上找就可以找到这些寄存器所保存的值

  1. 将r0的值(前面的底部地址),存到r2地址所指向的内存中(即栈顶地址指向的内存,pxTopOfStack中)
str r0, [ r2 ] 

以便后面要恢复这个任务的时候用得到:首先我得找到它的任务控制块,接着通过任务控制块获取到它的栈顶地址,然后获取它里面的一个值,这个值就是指向任务栈 R4 这里,那我获取到这个地址了,接着就出栈,从这里开始往上恢复这些寄存器(手动+自动),那恢复完之后,cpu 就可以指向当前任务了。
在这里插入图片描述
接着就是把 r0,r3 进行压栈,保存下来,因为我们后面的操作可能会修改这两个值,这个 sp 就是 MSP,因为现在在中断里面,那我们现在就把 r0,r3 保存到 MSP 里面了。

然后接着 r0 = FreeRTOS 所管理的最高中断的宏,我们这里是 5,然后就把这个值给写到 r0,然后把 r0 写到中断屏蔽寄存器,也就是说这两步其实就是关中断,但是它只是关 5~15 中断优先级的中断。

二、PendSV的任务切换操作(出栈,即恢复现场)

硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0使用PSP压入/出任务堆栈中。

  1. 关完中断之后,通过该函数,获取下一个执行任务的任务控制块,赋值给pxCurrentTCB
bl vTaskSwitchContext
  1. 获取最高优先级,就是获取当前最高任务的优先级。
  2. 每一个任务优先级都对应一个就绪列表,比如有 0~31 的任务优先级,那么它对应的就绪列表就有 0~31 个就绪列表,那参数这里呢就是当前最高任务的优先级的就绪列表。那么我们就可以从就绪列表里面找到最高优先级的一个任务的任务控制块,找到之后把它赋值给 pxCurrentTCB 变量。
    注意:pxCurrentTCB 变量永远指向的是当前任务优先级最高的一个任务。也就是我们即将运行的任务。那我们获取到这个任务控制块之后,那么这里就返回了。(地址不变,值改变)
  1. 然后把 r0 给清零,然后开中断。(把 0 给写到中断屏蔽寄存器)

  2. 大家注意,这个 r3 是什么,我们前面说了是 pxCurrentTCB 的地址,

  3. 出栈,把 r0 和 r3 给出栈出来。r3 前面说过是存放的 pxCurrentTCB 的地址,此时该地址指向了当前下一个要运行的任务控制块,那么接下来要取这个地址里面的值,所以 r1 指向 pxCurrentTCB 的(首成员)地址,即栈顶指针 pxTopOfStack 的地址,r0 就取这个地址里边的值,即栈顶指针,该值为入栈时所保存的寄存器寻址地址

ldr r1, [ r3]
ldr r0, [ r1 ] 
  1. 出栈,以寻址地址开始,从下往上进行出栈,将保存在这些地址的值恢复到寄存器里边去。
ldmia r0!, {r4-r11, r14}
  1. 判断 r14 的 bit4 有没有被置 1,即判断你有没有使用浮点数,如果使用了浮点数,要出栈(恢复) s16~s31 这些寄存器

  2. 把 r0 赋值给 PSP,因为退出中断之后,堆栈只能用的是 PSP,我们是要自动恢复的,然后 PSP 再从 R0 这个地址就开始恢复寄存器,那这样我们整个寄存器值就都给恢复回去了,那这样才能执行任务。

msr psp, r0     /* 更新任务B的栈给PSP */
bx r14

返回 r14 连接寄存器,这时候它就会来到下一个要运行的任务控制块的任务函数去执行任务了。

在这里插入图片描述

4.2 查找最高优先级任务

我们这里是通过硬件的方式来查找当前最高的一个优先级

vTaskSwitchContext( )						/* 查找最高优先级任务(下一个要运行的任务控制块) */
taskSELECT_HIGHEST_PRIORITY_TASK( )			/* 通过这个函数完成 */
 #define taskSELECT_HIGHEST_PRIORITY_TASK()
{                                                                                          									
    UBaseType_t uxTopPriority;
    portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );							/* 获取当前的最高优先级 */
    configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );	/* 获取当前最高优先级的任务控制块 */
}

任务优先级取值范围是 0~31,然后对应 32 个就绪列表,然后每个列表都有一个位来表示当前列表有没有任务,这个就凑成了一个 32 位的变量。

前导置零指令

所谓的前导置零指令,大家可以简单理解为计算一个 32位数,头部 0 的个数

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )      uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

在这里插入图片描述

通过前导置零指令获得最高优先级

获取最高优先级任务的任务控制块

那么现在已经获取到了最高优先级了,然后就会给它赋值到 uxTopPriority 这个变量,然后通过这个变量给带入到 pxReadyTasksLists[ uxTopPriority ] 这个列表里面,这个就是就绪列表,这样我们就可以找到有任务的、最高优先级的就绪列表。那有任务了,我从这个列表里面把任务给取出来,获取到它的任务控制块。那接着就来到这个函数:

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{
    List_t * const pxConstList = ( pxList );												/* 获取就绪列表 */
    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;							/* p 指针指向末尾列表项的下一个:列表项 1 */
    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )	/* 判断列表项 1 是否等于末尾列表项(去除末尾列表项) */
    {
        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;						/* p 指针指向末尾列表项的下一个:列表项 1 */
    }	
    ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;											/* 列表项 1 的任务控制块赋值给 pxTCB */
}

通过该函数获取当前最高优先级任务的任务控制块。

同等优先级多任务时就类似于时间片调度。(时间片调度的一个方法)每触发一次 PendSV 中断,两个同等优先级是轮流执行的,这个就是时间片调度。

在这里插入图片描述

4.3 仿真

5. 总结

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值