目录
一、FreeRTOS结构
1.1.移植
重点: 1.Demo:CORTEX_STM32F103_Keil 芯片与编译器平台 2.Source:核心在同步通信与互斥的 .c 文件,队列 信号量 互斥量 事件组 任务通知等等;移植实现文件portable RVDS/ARM_CM3 工具链/平台处理器内核
移植的重点在于port.c(上下文切换 调度 定时器 )portmacro.h(中断管理 堆栈管理 特定编译器和架构) 其次就是 工具链/平台处理器内核 RVDS/ARM_CM3,这表示cortexM3架构在RVDS或Keil工具上的移植文件。
1.2.核心文件
最核心所在是tasks.c以及list.c,前者是任务调度管理,因为时间片轮转,任务调度管理都涉及,这是任务的本质,后者是链表管理,从内部机制出发,刨析队列,信号量互斥量等等,本质都是基于链表来操作的,任务状态切换也需要链表。
其他核心的文件如下
FreeRTOS/Source/XXX.c | function |
---|---|
list.c | 必须,列表 |
task.c | 必须,任务 |
queue.c | 可选,队列、信号量semaphore |
event_groups.c | 可选,事件组等 |
time.c | 可选,software timer |
croutine.c | 可选,过时了 协程 |
1.3.相关头文件
FreeRTOS本身的头文件:FreeRTOS/Source/include
移植时用到的头文件:FreeRTOS/Source/portable/[compiler]/[architecture] 含编译器和架构等
含有配置文件FreeRTOSConfig.h的目录
FreeRTOSConfig.h作用 比如任务配置,时间片和调度配置,内存分配等等,配置文件,比如选择调度算法:configUSE_PREEMPTION 每个demo都必定含有FreeRTOSConfig.h 建议去修改demo中的FreeRTOSConfig.h,而不是从头写一个
FreeRTOS.h使用task.h event_group.h semphr.h等事先要调用其头文件
1.4.内存管理
我们常说堆栈,要知道他的本质。
堆,heap,就是一块空闲的内存,需要提供管理函数,比如全局变量
-
malloc:从堆里划出一块空间给程序使用
-
free:用完后,再把它标记为"空闲"的,可以再次使用
栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
-
可以从堆中分配一块空间用作栈
后续聊到任务这个概念会根深蒂固。
文件在 FreeRTOS/Source/portable/MemMang 下,它也是放在 portable 目录下,表示你可以提供自己的函数。
源码中默认提供了5个文件,对应内存管理的5种方法。
heap_1.c 分配简单,时间确定 只分配、不回收
heap_2.c 动态分配、最佳匹配 碎片、时间不定
heap_3.c 调用标准库函数 速度慢、时间不定
heap_4.c 相邻空闲内存可合并 可解决碎片问题、时间不定
heap_5.c 在heap_4基础上支持分隔的内存块 可解决碎片问题、时间不定
详细刨析heap
1.4.1.Heap_1
-
它只实现了pvPortMalloc,没有实现vPortFree
-
如果你的程序不需要删除内核对象,那么可以使用heap_1
-
实现最简单 没有碎片问题 一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1
/* Allocate the memory for the heap. */ #if ( configAPPLICATION_ALLOCATED_HEAP == 1 ) /* The application writer has already defined the array used for the RTOS * heap - probably so it can be placed in a special segment or address. */ extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; #else static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; #endif /* configAPPLICATION_ALLOCATED_HEAP */
实际上很简单,就是定义了一个大数组,每次从此当中分配空间。
FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。
这里补充一下
1)TCB任务控制块:TCB是FreeRTOS用来管理每个任务的数据结构,它存储了与任务相关的所有元数据。每个任务都有自己的TCB,用于管理该任务的状态、优先级等信息。
任务的状态:比如任务是就绪状态、阻塞状态、挂起状态等。
任务的优先级:任务的调度优先级,用来决定任务在什么时候运行。
任务的上下文:包括任务的程序计数器(PC)、寄存器状态等,保证任务切换时能恢复到正确的执行点。
任务的堆栈指针:指向该任务的堆栈,用于保存任务执行时的局部变量和函数调用信息。
任务的名字:如果启用了任务名字功能,TCB中会存储任务的名字。
任务的事件对象:用于与事件、信号量、队列等对象的交互。
其次正好聊聊TCB 不是句柄本身,任务句柄实际上是指向TCB的一个指针,用户通过任务句柄来操作和管理任务,比如暂停、删除、查询任务状态等。TCB 是保存任务信息的数据结构,而 任务句柄 是指向TCB的指针。
TaskHandle_t xTaskHandle;
2)Stack:堆栈是任务执行时的运行时存储空间,每个任务都有自己独立的堆栈,用来存储局部变量、函数调用返回地址、CPU寄存器的内容等。任务在执行过程中,堆栈会随着函数调用、返回等操作进行动态增长和缩减。
局部变量:任务执行时在函数内部声明的局部变量。
返回地址:函数调用返回的地址信息。
任务的CPU寄存器内容:任务在切换时保存的CPU寄存器信息,用来在任务恢复时继续执行。
综上,开始讲述heap1的使用过程,内存分配过程。
A:创建任务之前整个数组都是空闲的
B:创建第1个任务之后,蓝色区域被分配出去了
C:创建3个任务之后的数组使用情况
1.4.2.Heap_2(最佳匹配算法)
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4 来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:
-
Heap_2使用最佳匹配算法(best fit)来分配内存
-
它支持vPortFree(heap1不支持)
最佳匹配算法: 假设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、栈大小一致,所以刚好分配 到原来的内存
1.4.3.Heap_3
Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项 configTOTAL_HEAP_SIZE不再起作用。 C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
系统内存资源较大、需要使用标准C库动态分配的情况,但暂停调度器的方式可能会带来性能损耗,因为在暂停期间无法进行任务切换。
1.4.4.Heap_4(首次适应算法)
跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。 Heap_4使用首次适应算法(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会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景: 频繁地分配、释放不同大小的内存。
1.4.5.Heap_5
Heap_5分配内存、释放内存的算法跟Heap_4是一样的。 相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。 在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。 既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
-
在使用pvPortMalloc之前,必须先指定内存块的信息
-
使用vPortDefineHeapRegions来指定这些信息
怎么指定一块内存?使用如下结构体:
typedef struct HeapRegion { uint8_t * pucStartAddress; // 起始地址 size_t xSizeInBytes; // 大小 } HeapRegion_t;
怎么指定多块内存?使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。 比如:
HeapRegion_t xHeapRegions[] = { { ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000 { ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000 { NULL, 0 } // 表示数组结束 };
vPortDefineHeapRegions函数原型如下:
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
把xHeapRegions数组传给vPortDefineHeapRegions函数,即可初始化Heap_5。
二、任务管理
2.1.什么是任务?任务的本质
从这个角度想:函数被暂停时,我们怎么保存它、保存什么?怎么恢复它、恢复什么?
任务是一个函数吗?
-
函数保存在Flash上
-
Flash上的函数无需再次保存
-
所以:任务不仅仅是函数
任务时变量吗?
-
单纯通过变量无法做事
-
所以:任务不仅仅是变量
任务是一个运行中的函数
-
运行中:可以曾经运行,现在暂停了,但是未退出
-
怎么描述一个运行中的函数
-
假设在某一个瞬间时间停止,你怎么记录这个运行中的函数
要立即任务的本质,需要理解ARM架构、汇编
2.2.ARM架构
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
-
对内存只有读、写指令
-
对于数据的运算是在CPU内部实现
-
使用RISC指令的CPU复杂度小一点,易于设计
比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:
细看这几个步骤,有些疑问:
-
读a,那么a的值读出来后保存在CPU里面哪里?
-
读b,那么b的值读出来后保存在CPU里面哪里?
-
a+b的结果又保存在哪里?
我们需要深入ARM处理器的内部。简单概括如下,我们先忽略各种CPU模式(系统模式、用户模式等等)。
CPU运行时,先去Flash上取得指令,再执行指令:
-
把内存a的值读入CPU寄存器R0
-
把内存b的值读入CPU寄存器R1
-
把R0、R1累加,存入R0
-
把R0的值写入内存a
怎么理解Flash上的指令?看下一节。
2.3.汇编指令
-
读内存:Load,LDR
-
写内存:Store,STR
-
加法:ADD
-
入栈:PUSH,实质上就是写内存STR
-
出栈:POP,实质上就是读内存LDR
要读内存:读内存哪个地址?读到的数据保存在哪里?读多少字节?
-
LDR R0, [R1, #0x00]
-
源地址:R1+0x00,注意:不是读R1,是把R1的值当做内存的地址
-
目的:R0,CPU的寄存器
-
长度:4字节,LDR指令就是读4字节,LDRH是读2字节,LDRB是读1字节
-
要写内存:写内存哪个地址?从哪里得到数据?写多少字节?
-
STR R0, [R1, #0x00]
-
目的地址:R1+0x00,注意:不是写R1,是把R1的值当做内存的地址
-
源:R0,CPU的寄存器
-
长度:4字节,STR指令就是读4字节,STRH是读2字节,STRB是读1字节
-
入栈:把CPU的寄存器的值,写到内存上
-
PUSH {R3, LR}
-
源:CPU的寄存器R3、LR的值
-
目的:内存,内存哪里?使用CPU的SP寄存器指定内存地址
-
长度:大括号里所有寄存器的数据长度,每个寄存器4字节
-
注意:低编号的寄存器,保存在内存的低地址处
-
执行结果如下
-
出栈:把内存中的数值,写到CPU的寄存器
-
POP {R3, PC}
-
源:内存,内存哪里?使用CPU的SP寄存器指定内存地址
-
目的:CPU的寄存器R3、PC的值
-
长度:大括号里所有寄存器的数据长度,每个寄存器4字节
-
注意:内存的低地址处的数据,写到CPU低编号的寄存器
-
执行结果如下
-
其他知识:
-
CPU内部有R0、R1、……、R15共16个寄存器
-
某些寄存器有特殊作用
-
R13,别名SP,栈寄存器,保存着栈的地址
-
R14,别名LR,返回地址,保存着函数的返回地址
-
R15,别名PC,程序计数器,也就是当期程序运行到哪了
-
补充:SP、LR、PC
SP:堆栈指针,指向堆栈的顶部。
LR:链接寄存器,保存函数返回的地址。
PC:程序计数器,保存当前执行的指令地址。 这些寄存器在任务切换、函数调用和中断处理等操作中扮演着至关重要的角色。
2.4.怎么保存函数的现场
2.5.要保存什么 保存现场的几种场景 任务的重点
-
程序运行到了哪里?PC寄存器的值
-
R2的值:我辛辛苦苦从内存里读到的值放在R2里,函数继续运行时,R2的值不要被破坏了
-
只需要保存R2吗?切换任务的话,所有的寄存器都要保存
-
保存在哪里?内存里! 内存哪里?栈里!
-
函数调用 比如利用R1,R0这些用来传参的,就不用保存,不用全部保存,有些不用保存的。
-
中断处理 对于F103,M3,M4,硬件保存一部分,软件保存一部分用到的寄存器。
-
任务切换 全部寄存器都要保存,让CPU给另一个任务用嘛,不能破坏原本的东西。
2.6.创建任务
任务被切换的瞬间都保存在栈里,这是栈的本质,函数调用,局部变量都是。
堆:一块内存空间,可以从中分配一个小buffer,用完之后再放回去,全局变量
栈:也是一块内存空间,CPU的SP寄存器指向他,他可以用于函数调用,局部变量,多任务保存现场
void ATaskFunction( void *pvParameters );
这个函数不能返回 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
函数内部,尽量使用局部变量: 每个任务都有自己的栈 每个任务运行这个函数时 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
不同任务的局部变量,有自己的副本 函数使用全局变量、静态变量的话 只有一个副本:多个任务使用的是同一个副本 要防止冲突(后续会讲 引入队列等)
2.7.任务优先级和Tick
优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
-
确保高优先级的、可运行的任务,马上就能执行
-
对于相同优先级的、可运行的任务,轮流执行
Tick:对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。
FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时 钟中断。
-
假设t1、t2、t3发生时钟中断
-
两次中断之间的时间被称为时间片(time slice、tick period)
-
时间片的长度由configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就 是10ms
补充:例如陆续创建任务1以及任务2时并且优先级相同,他们的调度过程是任务2任务1任务2任务1任务2。。。
为什么会是任务2优先?因为创建的任务同一个优先级都会陆续放入一个链表当中,链表尾部先执行,再到头部。
怎么管理?
-
怎么取出要运行的任务?
-
找到最高优先级的运行态、就绪态任务,运行它
-
如果大家平级,轮流执行:排队,链表前面的先运行,运行1个tick后乖乖地去链表尾部排队
-
谁进行调度?
-
TICK中断!
2.8.任务状态
任务状态转换图
以前我们很简单地把任务的状态分为2中:运行(Runing)、非运行(Not Running)。
对于非运行的状态,还可以继续细分,如下
Task3执行vTaskDelay后:处于非运行状态,要过3秒种才能再次运行
Task3运行期间,Task1、Task2也处于非运行状态,但是它们随时可以运行
这两种"非运行"状态就不一样,可以细分为:
-
阻塞状态(Blocked)
-
暂停状态(Suspended)
-
就绪状态(Ready)
阻塞状态
在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:
-
任务要等待某个事件,事件发生后它才能运行
-
在等待事件过程中,它不消耗CPU资源
-
在等待事件的过程中,这个任务就处于阻塞状态(Blocked)
在阻塞状态的任务,它可以等待两种类型的事件:
-
时间相关的事件
可以等待一段时间:我等2分钟
也可以一直等待,直到某个绝对时间:我等到下午3点
-
同步事件:这事件由别的任务,或者是中断程序产生
例子1:任务A等待任务B给它发送数据
例子2:任务A等待用户按下按键
同步事件的来源有很多(这些概念在后面会细讲):
队列(queue)
二进制信号量(binary semaphores)
计数信号量(counting semaphores)
互斥量(mutexes)
递归互斥量、递归锁(recursive mutexes)
事件组(event groups)
任务通知(task notifications)
暂停状态 唯一的方法是通过vTaskSuspend函数
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
就绪状态
这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。
三、两个Delay函数
vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给 Tick */ /* pxPreviousWakeTime: 上一次被唤醒的时间 * xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement) * 单位都是Tick Count */ BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );
使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会 所以可以使用xTaskDelayUntil来让任务周期性地运行
四、空闲任务和钩子函数
除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部 分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运 行的任务:所以,我们要提供空闲任务。在使用 vTaskStartScheduler() 函数来创建、启动调度器 时,这个函数内部会创建空闲任务:
-
空闲任务优先级为0:它不能阻碍用户任务运行
-
空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为0,这以为着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让 这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现 的。
要注意的是:如果使用 vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无 法释放被删除任务的内存。
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环没执行一次,就会 调用一次钩子函数。钩子函数的作用有这些:
-
执行一些低优先级的、后台的、需要连续执行的函数
-
测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任 务占据的时间,就可以算出处理器占用率。
-
让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式 了。
空闲任务的钩子函数的限制:
-
不能导致空闲任务进入阻塞状态、暂停状态
-
如果你会使用 vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植 卡在钩子函数里的话,它就无法释放内存。
把这个宏定义为1:configUSE_IDLE_HOOK 实现 vApplicationIdleHook 函数
例如代码优先级为0 与空闲任务同级 调度的流程体现是;123;至于为什么?当没有空闲任务时就是按顺序执行,可以思考下,这是链表的思想。
空闲任务礼让:如果有同是优先级0的其他就绪任务,空闲任务主动放弃一次运行机会
五、分析任务中途放弃运行情况
比如中途主动放弃;等待;读队列 但是队列没有数据 只能等待,同步通信与互斥。
比如中途被动放弃;位于Task1的中途的时刻,GPIO唤醒一个高优先级的任务;他会马上抢占,不管Task运行完美没有,任务优先级。
任务切换;tick中断。
六、同步通信与互斥_重点
可以把多任务系统当做一个团队,里面的每一个任务就相当于团队里的一个人。 团队成员之间要协调工作进度(同步)、争用会议室(互斥)、沟通(通信)。多任务系统中所涉及的概念,都 可以在现实生活中找到例子。
各类RTOS都会涉及这些概念:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。我们先站在更高角度来讲解这些概念。
什么叫同步?就是:哎哎哎,我正在用厕所,你等会。
什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。
这里补充一下这里的互斥锁mutex,理论上来说是谁上锁谁打开,可是在这里是谁上锁任何人都可以打开,是工程师人为需要遵守的规则。
6.1.各类方法的对比
能实现同步、互斥的内核方法有:队列(queue)、互斥量(mutex)、信号量(samaphore)、事件组(event_group)、任务通知(task notification)
它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:
-
A获取资源,用完后A释放资源
-
A获取不到资源则阻塞,B释放资源并把A唤醒
-
A获取不到资源则阻塞,并定个闹钟;A要么超时返回,要么在这段时间内因为B释放资源而被唤 醒。
内核对象 | 生产者 | 消费者 | 数据/状态 | 说明 |
---|---|---|---|---|
队列 | ALL | ALL | 数据:若干个数据 谁都可以往队列里扔数据谁都可以从队列里读数据 | 用来传递数据 发送者、接收者无限制, 一个数据只能唤醒一个接收者 |
互斥锁 | A开锁 | A上锁 | 位:0、1 我上锁:1变为0, 只能由我开锁:0变为1 | 就像一个空厕所, 谁使用谁上锁, 也只能由他开锁 |
信号量 | ALL | ALL | 数量:0~n 谁都可以增加一个数量, 谁都可消耗一个数量 | 用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者 |
任务通知 | ALL | 只有我 | 数据、状态都可以传输, 使用任务通知时, 必须指定接受者,句柄 | N对1的关系: 发送者无限制, 接收者只能是这个任务 |
事件组 | ALL | ALL | 多个位:或、与 谁都可以设置(生产)多个位, 谁都可以等待某个位、若干个位 | 用来传递事件, 可以是N个事件, 发送者、接受者无限制, 可以唤醒多个接收者:像广播 |
使用图形对比如下:
队列:
-
里面可以放任意数据,可以放多个数据
-
任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
事件组:
-
一个事件用一bit表示,1表示事件发生了,0表示事件没发生
-
可以用来表示事件、事件的组合发生了,不能传递数据
-
有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
信号量:
-
核心是"计数值"
-
任务、ISR释放信号量时让计数值加1
-
任务、ISR获得信号量时,让计数值减1
任务通知:
-
核心是任务的TCB里的数值
-
会被覆盖
-
发通知给谁?必须指定接收任务
-
只能由接收任务本身获取该通知
互斥量:
-
数值只有0或1
-
谁获得互斥量,就必须由谁释放同一个互斥量
七、队列的核心
1)关中断
2)环形buffer
3)链表 -> 放数据 取数据
整个队列流程如下:
a.有中断关中断
b.有Data 读Data
没Data 返回 ERR
休眠
c.终于有Data了 copy Data
唤醒 Queue 拿数据移除任务 把它从DeayList -> ReadyList
一个任务写队列时,如果队列已经满了,它会被挂起,何时被唤醒?
超时:
-
任务写队列不成功时,它会被挂起:从ready list移到delayed list中
-
在delayed list中,按照"超时时间"排序
-
系统Tick中断不断发生,在Tick中断里判断delayed list中的任务时间到没?时间到后就唤醒它
别的任务读队列:
八、信号量的核心
本质还是队列的套壳。
信号量就是特殊的队列。
队列里使用环形缓冲区存放数据,
信号量里只记录计数值。
九、互斥量的核心
互斥量就是特殊的队列。
互斥量更是特殊的信号量,
互斥量实现了优先级继承。