|____2.7 FreeRTOS 深度解析--消息队列
消息队列
- 1. 消息队列控制块 Queue_t
- 2. 创建消息队列 xQueueCreate()
- 3. 删除队列 xQueueCreate()
- 4. 写队列操作
- 4.1 向队列尾部发送一个消息 xQueueSend() / xQueueSendToBack() / xQueueGenericSend()
- 4.2 向队列队首发送一个消息 xQueueSendToFront() / xQueueGenericSend()
- 4.3 在中断服务程序中向队列尾部发送一个队列消息 xQueueSendToBackFromISR() / xQueueGenericSendFromISR()
- 4.4 在中断服务程序中向消息队列队首发送一个消息 xQueueSendToFrontFromISR() / xQueueGenericSendFromISR()
- 4.5 通用消息队列发送函数 xQueueGenericSend()
- 4.6 消息队列发送函数 xQueueGenericSendFromISR()
- 5. 读队列操作
- 5.1 从一个队列中接收消息 xQueueReceive() / xQueuePeek() / xQueueGenericReceive()
- 5.2 在中断服务程序中接收一个队列消息 xQueueReceiveFromISR() / xQueuePeekFromISR()
- 5.3 从队列读取消息函数 xQueueGenericReceive()
在实际项目开发中,经常需要在任务与任务之间或任务与中断之间进行"沟通交流",这种沟通本质上就是消息传递的过程。在不使用操作系统的情况下,函数与函数之间或函数与中断之间的通信通常通过一个或多个全局变量来实现。然而,在操作系统中,由于涉及"资源管理"问题(如读写冲突),使用全局变量在任务间或任务与中断间传递消息并非理想方案。为此,FreeRTOS 提供了"队列"机制。
队列是一种用于任务到任务、任务到中断、中断到任务之间数据交换的机制。队列中可以存储数量有限、大小固定的多个数据项,每个数据项称为队列项目。队列能够存储的最大项目数量称为队列长度,创建队列时需要指定队列长度和项目大小。由于队列主要用于任务间或任务与中断间的消息传递,因此也常被称为消息队列。基于队列机制,FreeRTOS 实现了多种功能模块,包括队列集、互斥信号量、计数型信号量、二值信号量以及递归互斥信号量。
1. 消息队列控制块 Queue_t
(1)pcHead 指向队列消息存储区起始位置,即第一个消息空间(2)pcHead pcTail 指向队列消息存储区结束位置地址(3)pcWriteTo 指向队列消息存储区下一个可用消息空间(4)pcReadFrom 与 uxRecursiveCallCount 是一对互斥变量,使用联合体用来确保两个互斥的结构体成员不会同时出现。当结构体用于队列时,pcReadFrom 指向出队消息空间的最后一个,见文知义,就是读取消息时候是从 pcReadFrom 指向的空间读取消息内容。(5)当结构体用于互斥量时,uxRecursiveCallCount 用于计数,记录递归互斥量被“调用”的次数(6)当结构体用于互斥量时,xTasksWaitingToSend 是一个发送消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,如果队列已满,想要发送消息的任务无法发送消息(7)xTasksWaitingToReceive 是一个获取消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,如果队列是空的,想要获取消息的任务无法获取到消息(8)uxMessagesWaiting 用于记录当前消息队列的消息个数,如果消息队列被用于信号量的时候,这个值就表示有效信号量个数(9)uxLength 表示队列的长度,也就是能存放多少消息(10)uxItemSize 表示单个消息的大小(11)队列上锁后,储存从队列收到的列表项数目,也就是出队的数量,如果队列没有上锁,设置为 queueUNLOCKED(12)队列上锁后,储存发送到队列的列表项数目,也就是入队的数量,如果队列没有上锁,设置为 queueUNLOCKED
2. 创建消息队列 xQueueCreate()
每创建一个新的队列都需要为其分配 RAM,一部分用于存储队列的状态,剩下的作为队列消息的存储区域。使用 xQueueCreate()创建队列时,使用的是动态内存分配,所以要想使用该函数必须在 FreeRTOSConfig.h 中把 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1 来使能。
创建消息队列主要就是为队列分配 RAM 内存空间并初始化消息队列控制块Queue_t 结构体
(1)如果 uxItemSize 为 0,也就是单个消息空间大小为 0,就不需要申请内存,那么xQueueSizeInBytes也设置为 0 即可,设置为 0 是可以的,用作信号量的时候这个就可以设置为 0(2)uxItemSize 并不是为 0,那么需要分配足够存储消息的空间,内存的大小为队列长度*单个消息大小(3)调用 pvPortMalloc()函数向系统申请内存空间(内存大小 = 消息队列控制块大小 + 消息存储空间大小),这段内存空间是需要保证连续(4)计算出消息存储内存空间的起始地址(5)调用 prvInitialiseNewQueue()函数将消息队列进行初始化(初始化消息队列控制块Queue_t 结构体)
- 消息队列长度
- 单个消息大小
- 存储消息起始地址
- 消息队列类型
queueQUEUE_TYPE_BASE:表示队列
queueQUEUE_TYPE_SET:表示队列集合
queueQUEUE_TYPE_MUTEX:表示互斥量
queueQUEUE_TYPE_COUNTING_SEMAPHORE:表示计数信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE:表示二进制信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX :表示递归互斥量- 消息队列控制块
- 如果没有为消息队列分配存储消息的内存空间,而且 pcHead 指针不能设置为 NULL,因为队列用作互斥量时,pcHead 要设置成 NULL,这里只能将 pcHead指向一个已知的区域,指向消息队列控制块 pxNewQueue
- 如果分配了存储消息的内存空间,则设置 pcHead 指向存储消息的起始地址 pucQueueStorage
- 初始化消息队列控制块的其他成员,消息队列的长度与消息的大小
- 重置消息队列(初始化消息队列控制块Queue_t 结构体中的部分成员)
3. 删除队列 xQueueCreate()
(1)对传入的消息队列句柄进行检查,如果消息队列是有效的才允许进行删除操作(2)将消息队列从注册表中删除,我们目前没有添加到注册表中,暂时不用理会(3)因为用的消息队列是动态分配内存的,所以需要调用 vPortFree()函数来释放消息队列的内存
4. 写队列操作
任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。
发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。
4.1 向队列尾部发送一个消息 xQueueSend() / xQueueSendToBack() / xQueueGenericSend()
消息以拷贝的形式入队,而不是以引用的形式。该函数绝对不能在中断服务程序里面被调用,中断中必须使用带有中断保护功能的 xQueueSendFromISR()来代替
4.2 向队列队首发送一个消息 xQueueSendToFront() / xQueueGenericSend()
4.3 在中断服务程序中向队列尾部发送一个队列消息 xQueueSendToBackFromISR() / xQueueGenericSendFromISR()
4.4 在中断服务程序中向消息队列队首发送一个消息 xQueueSendToFrontFromISR() / xQueueGenericSendFromISR()
4.5 通用消息队列发送函数 xQueueGenericSend()
如果阻塞时间不为 0,则任务会因为等待入队而进入阻塞,在将任务设置为阻塞的过程中,系统不希望有其它任务和中断操作这个队列的
xTasksWaitingToReceive列表和xTasksWaitingToSend列表,因为可能引起其它任务解除阻塞,这可能会发生优先级翻转。比如任务 A 的优先级低于当前任务,但是在当前任务进入阻塞的过程中,任务 A 却因为其它原因解除阻塞了,这显然是要绝对禁止的。因此 FreeRTOS 使用挂起调度器禁止其它任务操作队列,因为挂起调度器意味着任务不能切换并且不准调用可能引起任务切换的 API 函数。但挂起调度器并不会禁止中断,中断服务函数仍然可以操作队列事件列表,可能会解除任务阻塞、可能会进行上下文切换,这也是不允许的。于是,解决办法是不但挂起调度器,还要给队列上锁,禁止任何中断来操作队列。
队列未满,将消息拷贝到消息队列中,并查看消息队列中
xTasksWaitingToReceive列表是否为空(空表示有任务在等待消息)
1.1如果有任务在等待获取此消息,将等待的任务从xTasksWaitingToReceive列表中删除,并添加到就绪列表中(如果恢复的任务优先级比当前运行任务的优先级高,那么需要进行一次任务切换)
1.2如果没有任务在等待获取此消息,也需要进行一次任务切换队列已满
2.1不指定阻塞超时时间,直接退出,不会发送消息
2.2指定阻塞超时时间,超时时间未到,则阻塞一下任务(将任务添加到xTasksWaitingToSend列表),并队列解锁和恢复调度器(如果调度器挂起期间有任务解除阻塞,并且接触阻塞的任务优先级比当前任务高,就需要进行一次任务切换)
2.3指定阻塞超时时间,超时时间到,返回一个 errQUEUE_FULL 错误代码,退出挂起调度器和给队列上锁,防止优先级翻转
(1)消息队列句柄(2)指针,指向要发送的消息(3)指定阻塞超时时间(4)发送数据到消息队列的位置,有以下 3 个选择,在 queue.h 中有定义,queueSEND_TO_BACK:发送到队尾;queueSEND_TO_FRONT:发送到队头;queueOVERWRITE:以覆盖的方式发送(5)进入临界段(6)判断队列是否已满,而如果是使用覆盖的方式发送数据,无论队列满或者没满,都可以发送(7)如果队列没满,可以调用 prvCopyDataToQueue()函数将消息拷贝到消息队列中(8)消息拷贝完毕,那么就看看有没有任务在等待消息(9)如果有任务在等待获取此消息,就要将任务从阻塞中恢复,调用 xTaskRemoveFromEventList() 函数将等待的任务从队列的等待接收列表 xTasksWaitingToReceive 中删除,并且添加到就绪列表中(10)将任务从阻塞中恢复,如果恢复的任务优先级比当前运行任务的优先级高,那么需要进行一次任务切换(11)如果没有等待的任务,拷贝成功也需要进行一次任务切换(12)退出临界段(13)(7)-(12)是队列未满的操作,如果队列已满,又会不一样的操作过程(14)如果用户不指定阻塞超时时间,则直接退出,不会发送消息(15)而如果用户指定了超时时间,系统就会初始化阻塞超时结构体变量,初始化进入阻塞的时间 xTickCount 和溢出次数 xNumOfOverflows,为后面的阻塞任务做准备(16)因为前面进入了临界段,所以应先退出临界段,并且把调度器挂起,因为接下来的操作系统不允许其他任务访问队列,简单粗暴挂起调度器就不会进行任务切换,但是挂起调度器并不会禁止中断的发生,所以还需给队列上锁,因为系统不希望突然有中断操作这个队列的 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表(17)检查一下用户指定的超时时间是否已经过去了。如果没过则执行(18)-(21)(18)如果队列还是满的,系统只能根据用户指定的超时时间来阻塞一下任务(19)当前任务添加到队列的等待发送列表中,以及阻塞延时列表,阻塞时间为用户指定时间 xTicksToWait(20)队列解锁,恢复调度器,如果调度器挂起期间有任务解除阻塞,并且解除阻塞的任务优先级比当前任务高,就需要进行一次任务切换(21)队列有空闲消息空间,允许入队,就重新发送消息(22)超时时间已过,返回一个 errQUEUE_FULL 错误代码,退出
4.6 消息队列发送函数 xQueueGenericSendFromISR()
队列未满,将消息拷贝到消息队列中
1.1队列上锁,则队列的等待接收列表xTasksWaitingToReceive不能被访问,就记录上锁次数,等到任务解除队列锁时,从这个记录次数就可以知道有多少数据入队
1.2队列未上锁,,如果有任务在等待获取此消息,将等待的任务从队列的等待接收列表xTasksWaitingToReceive中删除,并且添加到就绪列表中(如果恢复的任务优先级比当前运行任务的优先级高,那么需要记录上下文切换请求,等发送完成后,就进行一次任务切换)队列已满,因为 API 执行的上下文环境是中断,所以不能阻塞,直接返回队列已满错误代码 errQUEUE_FULL
(1)消息队列句柄(2)指针,指向要发送的消息(3)如果入队导致一个任务解锁,并且解锁的任务优先级高于当前运行的任务,则该函数将 *pxHigherPriorityTaskWoken 设置成 pdTRUE。如果 xQueueSendFromISR()设置这个值为 pdTRUE,则中断退出前需要一次上下文切换。从FreeRTOS V7.3.0 起,pxHigherPriorityTaskWoken 称为一个可选参数,并可以设置为 NULL(4)发送数据到消息队列的位置,有以下 3 个选择,在 queue.h 中有定义,queueSEND_TO_BACK:发送到队尾;queueSEND_TO_FRONT:发送到队头;queueOVERWRITE:以覆盖的方式发送(5)判断队列是否已满,而如果是使用覆盖的方式发送数据,无论队列满或者没满,都可以发送(6)如果队列没满,可以调用 prvCopyDataToQueue()函数将消息拷贝到消息队列中(7)判断队列是否上锁,如果队列上锁了,那么队列的等待接收列表就不能被访问(8)消息拷贝完毕,那么就看看有没有任务在等待消息,如果有任务在等待获取此消息,就要将任务从阻塞中恢复(9)调用 xTaskRemoveFromEventList()函数将等待的任务从队列的等待接收列表 xTasksWaitingToReceive 中删除,并且添加到就绪列表中(10)如果恢复的任务优先级比当前运行任务的优先级高,那么需要记录上下文切换请求,等发送完成后,就进行一次任务切换(11)如果队列上锁,就记录上锁次数,等到任务解除队列锁时,从这个记录次数就可以知道有多少数据入队(12)队列是满的,因为 API 执行的上下文环境是中断,所以不能阻塞,直接返回队列已满错误代码 errQUEUE_FULL
5. 读队列操作
当任务试图读队列中的消息时,可以指定一个阻塞超时时间,当且仅当消息队列中有消息的时候,任务才能读取到消息。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
5.1 从一个队列中接收消息 xQueueReceive() / xQueuePeek() / xQueueGenericReceive()
5.2 在中断服务程序中接收一个队列消息 xQueueReceiveFromISR() / xQueuePeekFromISR()
5.3 从队列读取消息函数 xQueueGenericReceive()
(1)消息队列句柄(2)指针,指向接收到要保存的数据(3)队列空时,用户指定的阻塞超时时间。如果该参数设置为 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)(4)xJustPeeking 用于标记消息是否需要出队,如果是 pdFALSE,表示读取消息之后会进行出队操作,即读取消息后会把消息从队列中删除;如果是 pdTRUE,则读取消息之后不会进行出队操作,消息还会保留在队列中(5)进入临界段(6)看看队列中有没有可读的消息(7)如果有消息,先记录读消息位置,防止仅仅是读取消息,而不进行消息出队操作(8)拷贝消息到用户指定存放区域 pvBuffer,pvBuffer 由用户设置的,其空间大小必须不小于消息的大小(9)判断一下 xJustPeeking 的值,如果是 pdFALSE,表示读取消息之后会进行出队操作(10)因为上面拷贝了消息到用户指定的数据区域,当前消息队列的消息个数需要减一(11)判断一下消息队列中是否有等待发送消息的任务(12)如果有任务在等待发送消息到这个队列,就要将任务从阻塞中恢复,调 用 xTaskRemoveFromEventList()函数将等待的任务从队列的等待发送列表 xTasksWaitingToSend 中删除,并且添加到就绪列表中(13)将任务从阻塞中恢复,如果恢复的任务优先级比当前运行任务的优先级高,那么需要进行一次任务切换(14)任务只是读取消息(xJustPeeking 为 pdTRUE),并不出队(15)因为是只读消息,所以还要还原读消息位置指针(16)判断一下消息队列中是否还有等待获取消息的任务,将那些任务恢复过来,如果恢复的任务优先级比当前运行任务的优先级高,那么需要进行一次任务切换(17)退出临界段(18)如果当前队列中没有可读的消息,那么系统会根据用户指定的阻塞超时时间 xTicksToWait 进行阻塞任务(19)xTicksToWait 为 0,那么不等待,直接返回 errQUEUE_EMPTY(20)而如果用户指定了超时时间,系统就会初始化阻塞超时结构体变量,初始化进入阻塞的时间 xTickCount 和溢出次数 xNumOfOverflows,为后面的阻塞任务做准备(21)因为前面进入了临界段,所以应先退出临界段,并且把调度器挂起,因为接下来的操作系统不允许其他任务访问队列,简单粗暴挂起调度器就不会进行任务切换,但是挂起调度器并不会禁止中断的发生,所以还需给队列上锁,因为系统不希望突然有中断操作这个队列的 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表(22)检查一下用户指定的超时时间是否已经过去了。如果没过则执行(22)-(24)(23)如果队列还是空的,就将当前任务添加到队列的等待接收列表中以及阻塞延时列表,阻塞时间为用户指定的超时时间 xTicksToWait,然后恢复调度器,如果调度器挂起期间有任务解除阻塞,并且解除阻塞的任务优先级比当前任务高,就需要进行一次任务切换(24)如果队列有消息了,就再试一次获取消息(25)超时时间已过,退出(26)返回错误代码 errQUEUE_EMPTY
