FreeRTOS基础学习_Part1
1 单片机程序设计模式
1.1 裸机程序设计模式
裸机程序的设计模式可以分为:轮询、前后台(中断)、定时器驱动、基于状态机。前面3种方法都无法解决一个问题:假设有 A、B 两个都很耗时的函数,无法降低它们相互之间的影响。 第4种方法可以解决这个问题,但是实践起来有难度。比如这样一个场景:一位职场妈妈需要同时解决 2 个问题:给小孩喂饭、回复工作信息。
1.1.1 轮询模式
示例代码如下:
1 | /* 经典单片机程序: 轮询 */ |
轮询模式在main函数中是一个while循环,里面依次调用2个函数,这两个函数相互之间有影响:如果“喂一口饭”太花时间,就会导致迟迟无法“回一个信息”;如果“回一个信息” 太花时间,就会导致迟迟无法“喂下一口饭”。
1.1.2 前后台(中断)
所谓“前后台”就是使用中断程序。假设收到同事发来的信息时,电脑会发出“滴”的一声,这时候妈妈才需要去回复信息。示例程序如下:
1 | /* 前后台程序 */ |
前后台(中断)方式在main函数里while循环里的代码是后台程序,平时都是while循环在运行; 当同事发来信息,电脑发出“滴”的一声,触发了中断。妈妈暂停喂饭,去执行“滴_中断”给同事回复信息;在这个场景里,给同事回复信息非常及时:即使正在喂饭也会暂停下来去回复信息。“喂一口饭”无法影响到“回一个信息”。但是,如果“回一个信息”太花时间,就会导致 “喂一口饭”迟迟无法执行。
1.1.3 定时器驱动
定时器驱动模式,是前后台模式的一种,可以按照不用的频率执行各种函数。比如需要每2分钟给小孩喂一口饭,需要每5分钟给同事回复信息。那么就可以启动一个定时器,让它每1分钟产生一次中断,让中断函数在合适的时间调用对应函数。示例代码如下:
1 | /* 前后台程序: 定时器驱动 */ |
1.1.4 基于状态机
当“喂一口饭”、“回一个信息”都需要花很长的时间,无论使用前面的哪种设计模式, 都会退化到轮询模式的缺点:函数相互之间有影响。可以使用状态机来解决这个缺点,示例代码如下:
1 | /* 状态机 */ |
以“喂一口饭”为例,函数内部拆分为 4个状态:舀饭、喂饭、舀菜、喂菜。每次执行“喂一口饭”函数时,都只会执行其中的某一状态对应的代码。以前执行一次“喂一口饭” 函数可能需要4秒钟,现在可能只需要 1秒钟,就降低了对后面“回一个信息”的影响。
使用状态机模式,可以解决裸机程序的难题:假设有 A、B 两个都很耗时的函数,怎样降低它们相互之间的影响。但是很多场景里,函数 A、B 并不容易拆分为多个状态,并且这些状态执行的时间并不好控制。所以这并不是最优的解决方法,需要使用多任务系统。
1.2 多任务系统
对于裸机程序,无论使用哪种模式进行精心的设计,在最差的情况下都无法解决这个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。使用状态机模式时, 如果函数拆分得不好,也会导致这个问题。本质原因是:函数是轮流执行的。假设“喂一口饭”需要 t1~t5这 5 段时间,“回一个信息”需要ta~te 这 5 段时间,轮流执行时:先执行完t1~t5,再执行ta~te,如下图所示:
对于职场妈妈,她怎么解决这个问题呢?她是一个眼明手快的人,可以一心多用,她这样做:
1 | - 左手拿勺子,给小孩喂饭 |
上述过程看似完美,但是脑子只有一个啊,虽然说“一心多用”,但是谁能同时思考两件事?只是她反应快,上一秒钟在考虑夹哪个菜给小孩,下一秒钟考虑给同事回复什么信息,这个过程简单说本质是:交叉执行,t1,t5和ta,te交叉执行,如下图所示:
基于多任务系统编写程序时,示例代码如下:
1 | /* RTOS 程序 */ |
基于多任务系统编写程序时,反而更简单了: ① 上面第2~8行是“喂饭任务”的代码; ② 第10~16行是“回信息任务”的代码,编写它们时甚至都不需要考虑它和其他函数的相互影响。就好像有2个单片机:一个个运行“喂饭任务”这个函数、另一个个运行“回信息任务”这个函数。
多任务系统会依次给这些任务分配时间:你执行一会,我执行一会,如此循环。只要切换的间隔足够短,用户会“感觉这些任务在同时运行”。如下图所示:
1.2.2 互斥操作
多任务系统中,多个任务可能会“同时”访问某些资源,需要增加保护措施以防止混乱。 比如任务 A、B 都要使用串口,能否使用一个全局变量让它们独占地、互斥地使用串口?示例代码如下:
1 | /* RTOS 程序 */ |
程序的意图是:task_A打印0123456789,task_B打印abcdefghij。在task_A或task_B打印的过程中,另一个任务不能打印,以避免数字、字母混杂在一起,比如避免打印这样的字符:012abc。第6行使用全局变量g_canuse实现互斥打印,它等于1时表示“可以打印”。在进行实际打印之前,先把g_canuse设置为0,目的是防止别的任务也来打印。
该程序大部分时间没问题,但只要它运行的时间足够长,就会出现数字、字母混杂的情况。看视频或者自己分析一下就很容易明白。
从上面的例子可以看到,基于多任务系统编写程序时,访问公用的资源的时候要考虑“互斥操作”。任何一种多任务系统都会提供相应的函数。
1.2.3 同步操作
如果任务之间有依赖关系,比如任务A执行了某个操作之后,需要任务B进行后续的处理。如果代码如下编写的话,任务B大部分时间做的都是无用功。
1 | /* RTOS 程序 */ |
上述代码中,在任务A没有设置flag为1之前,任务B的代码都只是去判断flag。而任务 A、B 的函数是依次轮流运行的,假设系统运行了100秒,其中任务A总共运行了50秒,任务B总共运行了50秒,任务A在努力处理复杂的运算,任务B仅仅是浪费CPU资源。
如果可以让任务B阻塞,即让任务B不参与调度,那么任务A就可以独占CPU资源加快处理复杂的事情。当任务A处理完事情后,再唤醒任务B。使用【信号量】的示例代码如下:
1 | // RTOS 程序 |
第15行:任务B运行时,等待信号量,不成功时就会阻塞,不再参与任务调度。
第7行:任务A处理完复杂的事情后,释放信号量会唤醒任务B。
第16行:任务B被唤醒后,从这里继续运行。
在这个过程中,任务 A处理复杂事情的时候可以独占CPU资源,加快处理速度。
PS:这里的信号量有点类似于前两天学习的“大丙老师的的多线程课程中的互斥锁、条件变量”的作用。
2 创建 FreeRTOS 工程
2.1 创建 STM32CubeMX 工程
双击运行STM32CubeMX,在首页面选择“Access to MCU Selector”,如下图所示:
然后来到MCU选型界面,在序列号那里输入想要开发的芯片,例如 STM32F103C8T6:
2.2 配置时钟
先配置处理器的时钟,在“System Core”的“RCC”处选择外部高速时钟源和低速时钟源。DshanMCU-F103使用了外部高速时钟源,如下图所示:
另外,本实验使用了FreeRTOS,FreeRTOS的时基使用的是Systick,而STM32CubeMX中默认的HAL库时基也是 Systick,为了避免可能的冲突,最好将HAL库的时基换做其它的硬件定时器(比如TIM4):
最后去时钟配置界面配置系统时钟频率。直接在HCLK时钟那里输入MCU允许的最高时钟频率。F103的最高频率是 72Mhz,所以直接在那里输入72然后按回车:
回车后,STM32CubeMX会自动计算得到各个分频系数和倍频系数。
2.3 配置 GPIO
板载 LED的使用的 GPIO是PC13,所以在STM32CubeMX的引脚配置界面,找到 PC13,然后在芯片图中,使用鼠标左键点击 PC13,会弹出此 IO支持的模式,这里选择GPIO Output,让PC13配置为通用输出IO,以便用来驱动LED 的亮灭。
2.4 配置 FreeRTOS
STM32CubeMX 已经将 FreeRTOS 集成到工具中,并且将 RTOS 的接口进行了封装 CMSIS-RTOS V1/V2,相较之于 V1版本的CMSIS-RTOS API,V2 版本的API的兼容性更高,为了将来的开发和移植,建议开发者使用V2版本的API:
选择 CMSIS V2接口后,还要进一步配置 FreeRTOS的参数和功能。
2.4.1 配置参数
FreeRTOS 的参数包括时基频率、任务堆栈大小、是否使能互斥锁等等,需要开发者根据自己对FreeRTOS的了解以及项目开发的需求,来定制参数。 先如下图进行配置:
2.4.2 添加任务
使用 STM32CubeMX,可以手工添加任务、队列、信号量、互斥锁、定时器等等。但是本课程不想严重依赖 STM32CubeMX,所以不会使用STM32CubeMX来添加这些对象,而是手写代码来使用这些对象。
使用 STM32CubeMX 时,会自动创建一个默认任务,此任务无法删除,只能修改其名称和函数类型, 如下图所示:
2.5 生成 Keil MDK 的工程
当对外设配置完成后,就去“Project Manager”中设置工程的名称、存储路径和开发IDE:
随后去同界面的“Code Generator”设置、生成工程:
2.6 添加用户代码
STM32CubeMX 只是帮我们初始化了所配置的硬件模块,你要实现什么功能,需要自己添加代码。
2.6.1 打开工程
在工程的“MDK-ARM”目录下,双击以.uvprojx结尾的文件,就会使用Keil打开工程。
2.6.2 修改文件
双击打开freertos.c文件,找到void StartDefaultTask(void* argument)函数里的循环。我们编写的代码,需要位于USER CODE BEGIN xxx和USER CODE END xxx之间,否则以后再次使用STM32CubeMX 配置工程时,不在这些位置的用户代码会被删除。
2.6.3 创建新的任务/子线程
1 |
|
此函数每个参数的详细说明:
- pxTaskCode
- 类型:
TaskFunction_t(函数指针类型,定义为typedef void (*TaskFunction_t)( void * )) - 作用:指向任务函数的指针,该函数需为无限循环且无返回值的函数。
- 示例:
void vTaskFunction(void *pvParameters)
- 类型:
- pcName
- 类型:
const char* const - 作用:任务描述性名称,用于调试,长度不超过
configMAX_TASK_NAME_LEN宏定义的限制。 - 注意:系统不会主动使用该名称,仅开发者可见。
- 类型:
- usStackDepth
- 类型:
configSTACK_DEPTH_TYPE(通常为uint16_t) - 作用:指定任务栈深度,单位为字(WORD)而非字节。例如32位系统中传入100,实际分配400字节(100*4)。
- 关键限制:栈深度×栈宽度需小于
size_t类型的最大值(如16位系统需≤65535),怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。
- 类型:
- pvParameters
- 类型:
void* const - 作用:传递给任务函数的参数指针,若无参数可设为NULL。
- 传递机制:通过TCB中的成员传递给任务函数
- 类型:
- uxPriority
- 类型:
UBaseType_t(通常为unsigned short) - 作用:任务优先级,范围0(最低)到
configMAX_PRIORITIES-1(最高)。支持特权模式的任务可通过设置portPRIVILEGE_BIT位实现
- 类型:
- pxCreatedTask
- 类型:
TaskHandle_t* const - 作用:传出参数,返回新创建任务的句柄(即TCB指针),可用于后续API操作(如删除任务、修改任务优先级等)。
- 注意:若不需要句柄可设为NULL
- 类型:
- 返回值
- 类型:
BaseType_t(通常为int)- 当返回值为
pdPASS(即代表数字1),表示任务创建成功并加入就绪列表 - 当返回值为
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(即代表数字-1),表示内存不足创建失败
- 当返回值为
- 类型:
3 FreeRTOS 源码概述
3.1 FreeRTOS 目录结构
使用 STM32CubeMX 创建的FreeRTOS工程中,FreeRTOS相关的源码如下:
主要涉及2个目录:
- Core 目录
- Inc 目录下的
FreeRTOSConfig.h是配置文件 - Src 目录下的
freertos.c是STM32CubeMX 创建的默认任务
- Inc 目录下的
- Middlewares\Third_Party\FreeRTOS\Source 目录
- 根目录下是核心文件,这些文件是通用的
- portable 目录下是移植时需要实现的文件
- 目录名为:[compiler]/[architecture]
- 比如:RVDS/ARM_CM3,这表示 cortexM3 架构在 RVDS 工具上的移植文件
3.2 核心文件
FreeRTOS的最核心文件只有2个:FreeRTOS/Source/tasks.c和FreeRTOS/Source/list.c。
其他文件的作用也一起列表如下:
| FreeRTOS/Source/下的文件 | 作用 |
|---|---|
| tasks.c | 必需,任务相关的操作, 包括任务的创建、删除、调度及优先级控制等核。 |
| list.c | 必需,链表相关的操作 |
| queue.c | 基本必需,提供队列操作、信号量(semaphore)相关的操作 |
| timer.c | 可选,software timer |
| event_groups.c | 可选,提供时间组(event group)功能 |
| croutine.c | 可选,过时了 |
每个文件的作用可以询问AI,我问了一下【FreeRTOS的tasks.c文件的作用是什么?】,回答的还是比较好的。
3.3 移植时涉及的文件
移植FreeRTOS时涉及的文件放在FreeRTOS/Source/portable/[compiler]/[architecture]目录下, 比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS或Keil工具上的移植文件。 里面有2个文件: port.c、portmacro.h。
3.4 头文件相关
- FreeRTOS需要3个头文件目录:
- FreeRTOS本身的头文件:
Middlewares\Third_Party\FreeRTOS\Source\include - 移植时用的头文件:
Middlewares\Third_Party\FreeRTOS\Source\portable\[compiler]\[architecture] - 含有配置文件 FreeRTOSConfig.h 的目录:
Core\Inc
- FreeRTOS本身的头文件:
- 头文件的作用:
| 头文件 | 作用 |
|---|---|
| FreeRTOSConfig.h | FreeRTOS 的配置文件,比如选择调度算法:configUSE_PREEMPTION,本课程的每个 demo 都必定含有 FreeRTOSConfig.h,建议去修改 demo 中的 FreeRTOSConfig.h,而不是从头 |
| FreeRTOS.h | 使用 FreeRTOS API 函数时,必须包含此文件。在 FreeRTOS.h 之后,再去包含其他头文件,比如:task.h、queue.h、semphr.h、event_group.h |
3.5 内存管理
文件在 Middlewares\Third_Party\FreeRTOS\Source\portable\MemMang 下,它也是放在“portable”目录下,表示你可以提供自己的函数。 源码中默认提供了5个文件,对应内存管理的5种方法。
| 文件 | 优点 | 缺点 |
|---|---|---|
| heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
| heap_2.c | 动态分配、最佳匹配 | 存在内存碎片、时间不定 |
| heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
| heap_4.c | 相邻空闲内存可合并 | 可解决碎片问题、时间不定 |
| heap_5.c | 在 heap_4 基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
3.6 入口函数
在 Core\Src\main.c 的 main 函数里,初始化了 FreeRTOS 环境、创建了任务,然后启动调度器。源码如下:
1 | /* Init scheduler */ |
3.7 数据类型和编程规范
略。
4 ARM架构简要介绍
4.1 精简指令集
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
① 对内存只有读、写指令
② 对于数据的运算是在CPU内部实现
③ 使用RISC指令的CPU复杂度小一点,易于设计
对于上图所示的乘法运算a = a * b,在RISC中要使用4条汇编指令:
1 | - 读内存a |
问题:在CPU内部,用什么来保存a、b、a * b ?
4.2 CPU内部寄存器
无论是cortex-M3/M4,还是cortex-A7,CPU内部都有R0、R1、……、R15寄存器;它们可以用来“暂存”数据。
对于寄存器R13、R14、R15,还另有用途:
- R13:别名SP(Stack Pointer, SP),栈指针,一般来说就用来保存栈的地地址。
- R14:别名LR(Link Register, LR),用来保存返回地址,在调用函数处需要保存函数执行完之后的返回地址。
- R15:别名PC(Program Counter, PC),程序计数器,表示当前指令地址,写入新值即可跳转到指定的地址运行程序。
4.3 汇编指令
4.3.1 基础的汇编指令
- 读内存:Load
1 | LDRB R0, R1 ; 读地址"R1", 得到的1字节数据存入R0 |
- 写内存:Stroe
1 | STRB R0, R1 ; 把R0的1字节数据写入地址R1 |
- 加减
1 | ADD R0, R1, R2 ; 表示 R0=R1+R2 |
- 比较
1 | CMP R0, R1 ; 结果保存在PSR(程序状态寄存器,即上图中最后一个寄存器) |
- 跳转
1 | B main ; Branch表示令PC=main函数地址, 直接跳转,走了就不再回来了 |
4.3.2 C语言函数的反汇编
示例c语言函数:
1 | int add(volatile int a, volatile int b) |
使用指令fromelf --text -a -c --output=xxx.dis xxx.axf让Keil生成反汇编:
查看生成的汇编代码:这一部分代码的分析请观看视频反汇编代码讲解 - 韦东山,手写笔记太麻烦,而且不如动态视频好理解。
4.4 内存分析
4.4.1 C语言内存分区
C语言在内存中一共分为如下几个区域,分别是:
(一)栈区
- 栈区的特点:
- 栈区由编译器自动分配释放,由操作系统自动管理,无须手动管理。
- 栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。
- 栈区按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。
- 栈区是先进后出原则(LIFO),其操作方式数据结构中的栈是一样的。
- 存放内容:
- 临时创建的局部变量存放在栈区。
- 函数调用时,其入口参数存放在栈区。
- 函数返回时,其返回值存放在栈区。
- (const 定义的)局部变量存放在栈区。
栈的大小是有限的,通常 Visual C++ 编译器的默认栈的大小为 1MB,所以不要定义 int a[1000000] 这样的超大数组。
看下面这样一个例子,main()函数中调用了a()函数,a()函数中调用了b()和c()函数:
1 | main() |
根据这个例子,有如下3个问题需要理解和解答:
- ① 每次函数调用前都会将返回地址保存到
LR寄存器中,然后PC寄存器指向被调用的函数地址,但是上面这种情况会产生嵌套调用,LR寄存器的值被频繁更改,如何保证函数返回后程序回到正确的地址?- 答:处理方法是:每次进入被调用的函数,函数的一开头都会先把上一个在
LR中的返回地址保存起来,即压到栈中(栈位于内存SRAM中),函数调用完成后再把栈中LR的值弹出来,这样就能保证程序返回到正确的地址运行。
- 答:处理方法是:每次进入被调用的函数,函数的一开头都会先把上一个在
- ② 局部变量是如何在栈中分配的?
- 答:直接参考视频栈的概念☞局部变量 - 韦东山
需要说明的是,函数中的局部变量不一定会保存到栈(内存SRAM中),也有可能直接保存到寄存器中(cpu这样优化有一个原因是寄存器的速度更快)。这地方可以联系
volatile关键字的用法。
- ③ 为什么每个FreeRTOS任务都要有自己独有的栈空间?
- 答:因为FreeRTOS的本质还是顺序运行,通过极为快速的任务切换,使得看起来多个任务是并行执行的。所以说这其中涉及到任务的切换,那么就必然要在切换前保存正在执行的任务的“现场”(基本上就是保存所有的寄存器,除了
SP寄存器,SP的值保存到当前任务的结构体TCB中)。所以说每个任务都要有自己独享的栈空间。
- 答:因为FreeRTOS的本质还是顺序运行,通过极为快速的任务切换,使得看起来多个任务是并行执行的。所以说这其中涉及到任务的切换,那么就必然要在切换前保存正在执行的任务的“现场”(基本上就是保存所有的寄存器,除了
(二)堆区
- 堆区的特点:
- 堆区按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。
- 堆区用于存放程序运行中被动态分布的内存段,可增可减。
- 可以用
malloc等函数实现动态分配堆内存,但它的存储空间一般是不连续的,所以会产生内存碎片。 - 用
malloc函数分配的堆内存,必须用free进行内存释放,否则会造成内存泄漏。 - 注意它与数据结构中的堆是两回事,不过分配方式类似于链表。
1 | char* p = (char*)malloc(sizeof(char)*20); |
4.4.2 单片机/MCU内存分配解读
(一)FLASH与RAM的基本结构
单片机内存被总分为Flash(ROM)和SRAM(RAM),Flash里面的数据掉电可保存,SRAM中的数据掉电就丢失,SRAM的执行速度要快于Flash,Flash容量大于SRAM。
我们正常下载程序都是下载存储进Flash里面,这也是为什么断电可保存的原因。单片机的程序存储分为code(代码存储区)、RO-data(只读数据存储区)、RW-data(读写数据存储区) 和 ZI-data(零初始化数据区)。
Flash存储code和RO-data,SRAM存储RW-data和ZI-data。
一个进程运行时,所占用的内存,可以分为如下几个部分:
(1)栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。
(2)堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS释放。
(3)全局变量、静态变量:初始化的全局变量和静态变量放在一块区域,未初始化的全局变量和和未初始化的静态变量在相邻的另一块区域。程序结束后由系统自动释放。
(4)文字常量:常量字符串就是放在这里的。这些数据是只读的,分配在RO-data(只读数据存储区),则被包含在Flash中,程序结束后由系统自动释放。
(5)程序代码(code):存放函数体的二进制代码。
(二)keil 编译后的字段解析
图中:
Code 表示 程序代码部分 = 程序代码区(code)
RO-data 表示 程序定义的常量 = 文字常量区
RW-data 表示 已初始化的全局变量 = 栈区(stack)堆区(heap)全局区(静态区)(static)
ZI-data 表示 未初始化的全局变量。
5 内存管理
5.1 为什么要自己实现内存管理
后续的章节涉及这些内核对象:task、queue、semaphores和event group等。为了让FreeRTOS更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。使用内存的动态管理功能,简化了程序设计:不再需要小心翼翼地提前规划各类对象,简化API函数的涉及,甚至可以减少内存的使用。
内存的动态管理是C程序的知识范畴,并不属于FreeRTOS的知识范畴,但是它跟FreeRTOS关系是如此紧密,所以我们先讲解它。
在C语言的库函数中,有mallco、free等函数,但是在FreeRTOS中,它们不适用:,原因有以下几点:
- 不适合用在资源紧缺的嵌入式系统中
- 这些函数的实现过于复杂、占据的代码空间太大
- 并非线程安全的(thread-safe)
- 运行有不确定性:每次调用这些函数时花费的时间可能都不相同
- 内存碎片化
- 使用不同的编译器时,需要进行复杂的配置
- 有时候难以调试
注意:我们经常”堆栈”混合着说,其实它们不是同一个东西:
- 堆,heap,就是一块空闲的内存,需要提供管理函数
malloc:从堆里划出一块空间给程序使用free:用完后,再把它标记为”空闲”的,可以再次使用- 栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
- 可以从堆中分配一块空间用作栈
5.2 FreeRTOS 的 5 种内存管理方法
FreeRTOS 中内存管理的接口函数为:pvPortMalloc() 、vPortFree(),对应于 C 库的malloc()、free()。
文件在FreeRTOS/Source/portable/MemMang下,它也是放在portable目录下,表示你可以提供自己的函数。 源码中默认提供了5个文件,分别是heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,对应内存管理的5种方法。这五种方法的优劣请参考“3.5 内存管理”小节。
5.2.1 Heap_1
它只实现了pvPortMalloc()函数,没有实现vPortFree()。 如果你的程序不需要删除内核对象,那么可以使用heap_1.c:
- 实现最简单,没有碎片问题。
- 一些要求非常严格的系统里,不允许使用动态内存,就可以使用 heap_1
它的实现原理很简单,首先定义一个大数组:
1 | /* Allocate the memory for the heap. */ |
然后,对于pvPortMalloc()调用时,从这个数组中分配空间。
FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。 使用heap_1时,内存分配过程如下图所示:
- A:创建任务之前整个数组都是空闲的;
- B:创建第1个任务之后,蓝色区域被分配出去了;
- C:创建3个任务之后的数组使用情况;
5.2.2 Heap_2
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:
- Heap_2使用最佳匹配算法(best fit)来分配内存
- 它支持
vPortFree()
最佳匹配算法:
- 假设heap有3块空闲内存:5字节、25字节、100字节,
pvPortMalloc()想申请20字节,找出最小的、能满足pvPortMalloc()的内存:25字节,把它划分为20字节、5字节,返回这20字节的地址,剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc()使用。
与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的”碎片化”问题。
但是,如果申请、分配内存时大小总是相同的,这类场景下Heap_2没有碎片化的问题。 所以它适合这种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的(创建任务时, 需要分配TCB和栈,TCB总是一样的)。虽然不再推荐使用heap_2,但是它的效率还是远高于malloc、free。
使用heap_2时,内存分配过程如下图所示:
- A:创建了3个任务
- B:删除了一个任务,空闲内存有3部分:顶层的、被删除任务的TCB空间、被删除任务的Stack空间
- C:创建了一个新任务,因为TCB、栈大小跟前面被删除任务的TCB、栈大小一致,故刚好分配到原来的内存
5.2.3 Heap_3
Heap_3 使用标准 C 库里的malloc()、free()函数,所以堆大小由链接器的配置决定,配置项 configTOTAL_HEAP_SIZE不再起作用。
C库里的malloc()、free()函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
5.2.4 Heap_4
跟 Heap_1、Heap_2 一样,Heap_4 也是使用大数组来分配内存。
Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
首次适应算法(first fit):
假设堆中有3块空闲内存:5字节、200字节、100字节,pvPortMalloc()想申请20字节,找出第1个能满足pvPortMalloc()的内存:200字节,把它划分为20字节、180字节,返回这20字节的地址,剩下的180字节仍然是空闲状态,留给后续的pvPortMalloc()使用。
Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存。
Heap_4的使用过程举例如下:
- A:创建了3个任务
B:删除了一个任务,空闲内存有2部分:顶层的被删除任务的TCB空间、被删除任务的Stack空间合并起来的
C:分配了一个Queue,从第1个空闲块中分配空间
- D:分配了一个User数据,从Queue之后的空闲块中分配
- E:释放的Queue,User前后都有一块空闲内存
- F:释放了User数据,User前后的内存、User本身占据的内存,合并为一个大的空闲内存
Heap_4执行的时间是不确定的,但是它的效率高于标准库的malloc、free。
5.2.5 Heap_5
Heap_5 分配内存、释放内存的算法跟 Heap_4 是一样的。
相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。 在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。 既然内存是分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
- 在使用
pvPortMalloc()之前,必须先指定内存块的信息 - 使用
vPortDefineHeapRegions()来指定这些信息
怎么指定一块内存?使用如下结构体:
1 | typedef struct HeapRegion |
怎么指定多块内存?使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。
1 | HeapRegion_t xHeapRegions[] = |
vPortDefineHeapRegions函数原型如下:
1 | void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions ); |
把 xHeapRegions数组传给vPortDefineHeapRegions函数,即可初始化Heap_5。
5.3 Heap 相关的函数
5.3.1 pvPortMalloc/vPortFree
1 | /** |
5.3.2 xPortGetFreeHeapSize
1 | /** |
5.3.3 xPortGetMinimumEverFreeHeapSize
1 | /** |
5.3.4 pvPortMalloc 失败的钩子函数
在pvPortMalloc()函数内部:
1 | void * pvPortMalloc( size_t xWantedSize )vPortDefineHeapRegions |
所以,如果想使用这个钩子函数:
- 在FreeRTOSConfig.h 中,把
configUSE_MALLOC_FAILED_HOOK定义为1 - 提供
vApplicationMallocFailedHook()函数 pvPortMalloc()失败时,才会调用此函数
6 任务管理
对于整个单片机程序,我们称之为application,应用程序。使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
6.1 任务/子线程创建与删除
6.1.1 什么是任务
在 FreeRTOS中,任务就是一个函数,原型如下:
1 | void A_Task_Function_Example( void *pvParameters ); |
要注意的是:
这个函数不能返回;
同一个函数,可以用来创建多个任务/线程;换句话说,多个任务/线程可以运行同一个函数;
函数内部,尽量使用局部变量:
- 每个任务/线程都有自己的栈;
- 每个任务运行这个函数时,任务A的局部变量放在A的栈里、任务B的局部变量放在B的栈里;
- 不同任务的局部变量,有自己的副本;
函数使用全局变量、静态变量的话:
只有一个副本:多个任务使用的是同一个副本;
要防止冲突(后续会讲) 。
下面是一个示例:
1 | void A_Task_Function_Example( void *pvParameters ) |
FreeRTOS使用一个链表管理任务:
6.1.2 创建任务
创建任务时可以使用2个函数:动态分配内存、静态分配内存。
- 使用动态分配内存(TCB和栈)的函数如下:
1 |
|
该函数的参数的详细解析请看“2.6.3 创建新的任务/子线程”小节。
- 使用静态分配内存(TCB和栈)的函数如下:
1 |
|
该函数是FreeRTOS中用于静态创建任务的函数,与动态创建的xTaskCreate()不同,它要求程序员预先分配任务控制块(TCB)和任务栈内存,适用于内存受限或需要确定性内存管理的场景 。
相比于使用动态分配内存创建任务的函数,只有最后2个参数和返回值不一样,因此这里只介绍最后2个参数和返回值:
- puxStackBuffer
- 类型:
StackType_t * - 作用:静态分配的栈内存,比如可以传入一个数组, 它的大小要大于等于usStackDepth*4。
- 类型:
- pxTaskBuffer
- 类型:
StaticTask_t *(实质为指向用户提供的TCB内存块的指针) - 作用:用户提供的TCB内存(
StaticTask_t类型),需保证生命周期与任务一致。
- 类型:
- 返回值
- 类型:
TaskHandle_t(实质为StaticTask_t*的别名) - 作用:返回指向已初始化的TCB的指针(即任务句柄),用于后续操作任务(如删除、修改优先级等),若创建失败(如参数无效),返回NULL
- 返回值本质指向用户提供的pxTaskBuffer内存块,但该内存已被系统初始化为有效的TCB结构体,返回值与的参数
pxTaskBuffer指向的物理地址相同,但逻辑意义不同:pxTaskBuffer是“原材料”(未初始化的内存——TCB结构体)。- 返回值是“成品”(已初始化的TCB句柄)
- 类型:
从这两个参数就能大概看出动态和静态两种创建方法的区别:
- 动态方法:只需要提供任务的栈空间大小,函数内部自动动态分配内存给栈空间和TCB结构体
- 静态方法:不仅要提供任务的栈空间大小,还要手动提供具体的栈空间puxStackBuffer和TCB结构体空间pxTaskBuffer,并且还要保证这两个参数的生命周期与任务一致。
示例使用:
1 | // 任务函数定义(pxTaskCode将指向此函数) |
6.1.3 任务的删除
删除任务时使用的函数如下:
1 |
|
怎么删除任务?举个不好的例子:
- 自杀:
vTaskDelete(NULL),类似于Linux线程退出函数void pthread_exit(void *retval); - 被杀:别的任务执行
vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄 ; - 杀人:执行
vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄 。
6.2 任务优先级和 Tick
6.2.1 任务优先级
任务/线程的优先级的取值范围是:0 ~ (configMAX_PRIORITIES–1),数值越大优先级越高。FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES的取值有所不同。
- 通用方法
使用C函数实现,对所有的架构都是同样的代码。对configMAX_PRIORITIES的取值没有限制。但是configMAX_PRIORITIES的取值还是尽量小,因为取值越大越浪费内存,也浪费时间。 configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为0、或者未定义时,使用此方法。
- 架构相关的优化的方法
架构相关的汇编指令,可以从一个32位的数里快速地找出为1的最高位。使用这些指令,可以快速找出优先级最高的、可以运行的任务。使用这种方法时, configMAX_PRIORITIES的取值不能超过32。 configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为1时,使用此方法。
在学习调度方法之前,你只要粗略地知道:
- FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行;
- 对于相同优先级的、可运行的任务,轮流执行。
获取任务的优先级:
1 | /** |
修改任务的优先级:
1 | /** |
6.2.2 Tick
对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。 “一会”怎么定义?
就像人有心跳,心跳间隔基本恒定。 FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时钟中断。
如下图:
假设 t1、t2、t3发生时钟中断,两次中断之间的时间被称为时间片(time slice、tick period) ,时间片的长度由 configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就是10ms 。
相同优先级的任务怎么切换呢?请看下图:
上图的的流程可描述为:任务2从t1执行到t2,在t2发生tick中断,进入tick 中断处理函数,选择下一个要运行的任务执行完中断处理函数后,切换到新的任务任务1,任务1从t2执行到t3。从图中可以看出,任务运行的时间并不是严格精确地从t1,t2,t3时刻开始,中间的任务切换也需要时间。
有了Tick的概念后,我们就可以使用Tick来衡量时间了,比如:
1 | // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms |
注意,基于Tick实现的延时并不精确,比如
vTaskDelay(2)的本意是延迟2个Tick周期, 有可能经过1个Tick多一点就返回了。如下面这种特殊情况:
使用vTaskDelay()函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。
6.3任务状态
以前我们很简单地把任务的状态分为2种:运行(Runing)、非运行(Not Running)。但对于非运行的状态,还可以继续细分,比如前面的FreeRTOS_04_task_priority中:
Task3执行vTaskDelay 后:处于非运行状态,要过3 秒种才能再次运行,Task3运行期间,Task1、Task2也处于非运行状态,但是它们随时可以运行,这两种”非运行”状态就不一样,可以细分为:
- 阻塞状态(Blocked)
- 暂停状态(Suspended)
- 就绪状态(Ready)
一个任务被创建出来后处于就绪态(Ready),可以随时被调度器切换为运行态(Running)。
没有结合下面的章节内容,写到这里为止,我个人区分阻塞态和暂停态的方法是:
对于阻塞态,它是等待某个事件,当事件发生的时候,函数就会自动由阻塞态转变为就绪态,随时可运行,这个过程是自动的;而对于暂停态,必须由其他任务主动地调用
vTaskResume()函数来唤醒,是被动的。
6.3.1 阻塞状态(Blocked)
在日常生活的例子中,母亲在电脑前跟同事沟通时,如果同事一直没回复,那么母亲的工作就被卡住了、被堵住了、处于阻塞状态(Blocked)。重点在于:母亲在等待。
在FreeRTOS_04_task_priority实验中,如果把任务3中的vTaskDelay()调用注释掉,那么任务1、任务2根本没有执行的机会,任务1、任务2被”饿死”了(starve)。
实际产品中,不会让一个任务一直运行,而是使用”事件驱动“的方法让它运行:
1️⃣ 任务要等待某个事件,事件发生后它才能运行;
2️⃣ 在等待事件过程中,它不消耗CPU 资源;
3️⃣ 在等待事件的过程中,这个任务就处于阻塞状态(Blocked);
在阻塞状态的任务,它可以等待两种类型的事件:
- 一是:时间相关的事件
- 可以等待一段时间:我等2分钟
- 也可以一直等待,直到某个绝对时间:我等到下午3点
所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。
- 二是:同步事件,这事件由别的任务,或者是中断程序产生
- 例子1:任务A等待任务B给它发送数据
- 例子2:任务A等待用户按下按键
所谓同步事件,就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。
怎么”发送信息”?方法很多,同步事件的来源有很多(这些概念在后面会细讲): ① 队列(queue) ,② 二进制信号量(binary semaphores),③ 计数信号量(counting semaphores),④ 互斥量(mutexes),⑤ 递归互斥量、递归锁(recursive mutexes),⑥ 事件组(event groups),⑦ 任务通知(task notifications)。这些方法用来发送同步信息,比如表示某个外设得到了数据。
在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:
▶ 10ms之内有数据到来:成功返回
▶ 10ms到了,还是没有数据:超时返回
6.3.2 暂停状态(Suspended)
在日常生活的例子中,母亲正在电脑前跟同事沟通,母亲可以暂停:
💦 好烦啊,我暂停一会
💤 领导说:你暂停一下
FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend()函数。函数原型如下:
1 | /** |
要退出暂停状态,只能由别的函数来操作,已经处在暂停态的任务无法唤醒自己,可通过下面两种方法:
▶ 别的任务调用:vTaskResume()
1 | /** |
▶ 中断程序调用:xTaskResumeFromISR()
1 | /** |
- 返回参数的进一步说明:
- pdTRUE:恢复的任务优先级等于或高于当前运行任务,中断退出后可能需要执行任务切换 。
- pdFALSE:恢复的任务优先级低于当前运行任务,中断退出后无需执行任务切换。
在FreeRTOS实时操作系统中,被vTaskSuspend()挂起的任务不会占用CPU资源。这是FreeRTOS任务调度机制的基本特性,具体表现为:
- 调度排除:挂起状态的任务会被完全移出调度器的就绪列表,调度器在选择下一个运行任务时不会考虑这些任务;
- 状态隔离:挂起状态是独立于运行/就绪/阻塞状态的特殊状态,任务进入此状态后完全脱离调度系统。
实际开发中,暂停状态用得不多。
补充:阻塞和挂起状态的区分
在FreeRTOS实时操作系统中,任务挂起(vTaskSuspend)和任务阻塞虽然都使任务不占用CPU资源且不参与调度,但两者在触发机制、恢复方式和系统管理等方面存在本质区别:
- 任务挂起是主动行为,需要显式调用API将任务移出调度系统
- 任务阻塞是被动行为,由任务等待特定事件(如信号量、延时等)自动触发
- 两者都不占用CPU时间片,但挂起任务完全脱离调度管理,而阻塞任务仍被特定事件链表管理
特性 任务挂起(Suspended) 任务阻塞(Blocked) 触发方式 显式调用 vTaskSuspend()自动进入(如调用 vTaskDelay()、等待信号量)状态性质 主动暂停 被动等待 典型触发场景 调试暂停、系统资源管理 定时事件、同步事件(队列/信号量/互斥锁) 超时机制 无超时 可设置超时 恢复机制差异
挂起任务恢复:
- 必须显式调用
vTaskResume()或xTaskResumeFromISR()- 无自动恢复机制,即使等待的事件发生也不会唤醒任务
- 多次挂起只需一次恢复即可使任务重新就绪
阻塞任务恢复:
- 自动恢复,无需显式调用恢复函数
- 恢复条件包括:
- 定时事件到期(如
vTaskDelay()完成)- 同步事件发生(如信号量给出、队列收到数据)
- 超时时间到达(即使事件未发生)
调度器管理方式
- 挂起任务管理:
- 被移动到专门的
xSuspendedTaskList链表- 完全从调度器的就绪/阻塞链表中移除
- 调度器决策时完全忽略挂起任务
- 阻塞任务管理:
- 根据等待的事件类型进入不同链表(如延时链表、信号量等待链表)
- 仍被调度器的事件管理系统跟踪
- 超时或事件发生时自动转移到就绪链表
6.3.3 就绪状态(Ready)
这个任务完全准备好了,随时可以运行:只是还轮不到它,这时它就处于就绪态(Ready)。
6.4 Delay 函数
6.4.1 两个 Delay 函数
有两个Delay函数:
vTaskDelay():至少等待指定个数的Tick Interrupt才能变为就绪状态vTaskDelayUntil():等待到指定的绝对时刻,才能变为就绪态。
这 2个函数原型如下:
1 | /** |
关于
xTaskDelayUntil()函数的参数和返回值的详细说明:
- pxPreviousWakeTime
- 类型:
TickType_t* const(指向时间戳的指针)- 作用:
- 输入时:传递任务上次唤醒时刻的时钟节拍值(需初始化为当前时间,使用下面的函数
BaseType_t xTaskGetTickCount()。- 输出时:函数内部会更新该值为下一次预期的唤醒时间(即
*pxPreviousWakeTime + xTimeIncrement)。- 关键:必须为全局变量或静态变量,确保生命周期覆盖任务运行全程 。
- xTimeIncrement
- 类型:
const TickType_t(常量节拍数)- 作用:指定任务的固定周期间隔(单位:系统节拍,如1ms/节拍)。
- 示例:若需100ms周期,且系统节拍为1ms,则传入100。
- 返回值
- 类型:
BaseType_t(实际为pdTRUE/pdFALSE宏)- 含义:
- pdTRUE:任务因延迟时间已过而立即唤(可能因系统节拍溢出导致)。
- pdFALSE:任务正常进入阻塞状态,等待周期到。
- 典型用法:通常可忽略返回值,除非需处理极端时间同步场景。
下面进一步说明这两个Delay函数的区别,假设有下面这样一个函数,这个函数里面的do_something()的运行时间不定:
1 | void my_task() |
1 | void my_task() |
6.5 空闲任务及其钩子函数
6.5.1 介绍
空闲任务(prvIdleTask()任务)的作用之一:释放被删除的任务的内存。 原因如下:
如图所示,这个任务函数不是死循环,而是执行10次循环就退出了,但是退出后没有经过任何处理,这就会导致退出后函数进入prvTaskExitError()执行,在这个函数的最后两行代码的作用分别是关闭全部中断(包括系统Tick中断)、死循环,这样就会导致整个系统都无法运行了。
因此,要想正常退出任务,必须使用vTaskDelete()来自杀或者被他杀。如果是自杀,那么必须由空闲任务来收尸(回收资源),如果是他杀,则由执行杀任务的任务给被杀任务收尸(回收资源)。
除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务。空闲任务有如下两个特点:
- 空闲任务优先级为 0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务”抢占”(pre-empt)了空闲任务,这是由调度器实现的。
要注意的是:如果使用
vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
- 执行一些低优先级的、后台的、需要连续执行的函数 ;
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率;
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了;
空闲任务的钩子函数的限制:
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用
vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
6.5.2 使用钩子函数的前提
在FreeRTOS\Source\tasks.c中,可以看到如下代码,所以前提就是:
- 把这个宏定义为 1:configUSE_IDLE_HOOK
- 实现
vApplicationIdleHook()函数
6.6 调度算法
6.6.1配置调度算法
所谓调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态。
通过配置文件FreeRTOSConfig.h的两个配置项来配置调度算法:configUSE_PREEMPTION、configUSE_TIME_SLICING。
PS:还有第三个配置项:
configUSE_TICKLESS_IDLE,它是一个高级选项,用于关闭Tick中断来实现省电,后续单独讲解。现在我们假设configUSE_TICKLESS_IDLE=0,也即先不使用这个功能。
调度算法的行为主要体现在两方面:
① 高优先级的任务先运行
② 同优先级的就绪态任务如何被选中。
调度算法要确保同优先级的就绪态任务能”轮流”运行,策略是轮转调度(Round Robin Scheduling)。轮转调度并不保证任务的运行时间是公平分配的,我们还可以细化时间的分配方法。
从3个层级统一理解多种调度算法:
- 层级1:可否抢占?高优先级的任务能否优先执行(配置项:
configUSE_PREEMPTION)- 可以:被称作“可抢占调度”(Pre-emptive),高优先级的就绪任务马上执行,并且如果高优先级的任务不执行完成或者主动让出CPU资源,则低优先级永远无法执行,该层级下面进一步再细化。
- 不可以:不能抢就只能协商了,被称作“合作调度模式”(Co-operative Scheduling),当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。
- 层级2:可否轮流?可抢占的前提下,同优先级的任务是否轮流执行(配置项:
configUSE_TIME_SLICING)- 可以轮流执行:被称为“时间片轮转”(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片 。
- 不可以轮流执行:英文为”without Time Slicing”,当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占。
- 层级3:空闲任务是否让步于用户任务?(配置项:
configIDLE_SHOULD_YIELD)- 是,则意味着空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务。
- 否,则意味着空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊。
| 配置项 | 方案1 | 方案2 | 方案3 | 方案4 | 方案5 |
|---|---|---|---|---|---|
| configUSE_PREEMPTION | 1 | 1 | 1 | 1 | 0 |
| configUSE_TIME_SLICING | 1 | 1 | 0 | 0 | x |
| configIDLE_SHOULD_YIELD | 1 | 0 | 1 | 0 | x |
| 说明 | 常用 | 很少用 | 很少用 | 很少用 | 几乎不用 |
注:
- 方案1:可抢占+时间片轮转+空闲任务让步
- 方案2:可抢占+时间片轮转+空闲任务不让步
- 方案3:可抢占+非时间片轮转+空闲任务让步
- 方案4:可抢占+非时间片轮转+空闲任务不让步
- 方案5:合作调度
未完成,请结合vision和【任务管理与调度】视频补充笔记
































