如果我对本翻译内容享有所有权。允许任何人复制使用本文章,不会收取任何费用。如有平台向你收取费用与本人无任何关系
第四章 . 队列管理
章节介绍和范围
队列提供,任务到任务,任务到中断,中断到任务的消息机制
范围
本章指在教会读者:
- 怎样创建队列
- 队列如何管理数据
- 如何发送数据给队列
- 如何从队列接收数据
- 阻塞在队列上是什么意思
- 怎样阻塞在多个队列上
- 如何超写队列
- 如何清除队列
- 读写队列对任务优先级影响
本章只包含任务到任务交流。任务到中断,中断到任务在第6章介绍。
队列特征
数据存储
队列可以存储固定大小的数据项目。队列最大可存储数据项条目叫"length"。"length"和每条数据项尺寸都是队列创建时设置的。
队列是最常见的FIFO缓存——先进先出缓存,数据是写入到队尾,删除是从队列头开始。图31展示了数据的读写过程,它就像FIFO一样使用。也可以从队头开始写,超写数据就是在队列头。
# 队列数据读,写顺实例。图31
---------- ----------------------- ----------
|Task A | |Queue | |Task B |
|int x; | | ___ ___ ___ ___ ___ | |int y; |
| | ----------------------- | |
---------- ----------
创建一个队列,允许任务A任务B进行交流。队列最多可以保存5个整数。创建后队列是空的不包含任何数据。
# 下一步
---------- ----------------------- ----------
|Task A | |Queue | |Task B |
|int x; | | ___ ___ ___ ___ _10_| |int y; |
|x=10; | ----------------------- | |
---------- x发送到队列 ----------
任务A写一个本地变量值到队列。由于队列之前是空的,所以只有一个数据项,这个值的位置即是队列头也是队列尾。
# 下一步
---------- ----------------------- ----------
|Task A | |Queue | |Task B |
|int x; | | ___ ___ ___ 20_ _10_| |int y; |
|x=20; | ----------------------- | |
---------- x发送到队列 ----------
任务A再次写入队列前改变本地变量值,然后写入队列。目前队列中就包含两个值的副本。第一个值保存在队列头,新值插入到队列尾。队列还剩3个空位。
# 下一步
---------- ----------------------- ----------
|Task A | |Queue | |Task B |
|int x; | | ___ ___ ___ 20_ _10_| |int y; |
|x=20; | ----------------------- |y等于10 |
---------- y从队列接收数据 ----------
任务B从队列中读(接收)一个数据。接收的数据是队列头的数据,就是任务A第一次写入的数据——10。
# 下一步
---------- ----------------------- ----------
|Task A | |Queue | |Task B |
|int x; | | ___ ___ ___ ___ _20_| |int y; |
|x=20; | ----------------------- |y等于10 |
---------- ----------
任务B删除一个项,只留下任务A写的第二个数据——20。下次任务B读会接收到的数据。目前队列还有4个空位。
实现队列的方式有两种:
- 复制队列:复制方式意味着发送给队列的数据,会被复制一份保存于队列之中。
- 引用队列:引用队列意味着发送给队列的数据,队列只保存数据指针,而不保存数据本身。
FreeRTOS使用复制的方式。复制队列相比于引用队列更加强大和简单:
- 栈变量可以写入队列,即使在函数退出,变量已经释放之后,复制的变量也会在队列之中。
- 发送给队列的数据可以不用首先分配缓存保存数据,可以后面复制数据到缓存中。
- 写入数据的任务可以立即重复使用这个变量或缓存,而不会影响已经发送给队列的值。
- 发送任务和接收任务完全是双向的。程序设计者不用考虑数据的所有者,哪个任务应该负责释放数据。
- 复制队列不会阻碍队列用于引用队列。例如,当数据太大不适合直接放到队列中时,数据的指针确可以复制到队列中。
- RTOS全权负责内存分配用于存储数据。
- 内存保护系统中,任务可以访问的内存会被限制。因此只能使用读写任务都可以访问的有数据的RAM引用队列。复制队列不受这种限制。内核总是拥有所有权限,允许队列通过受保护内存传递数据。
多任务访问
队列是拥有自身权限的实体,知道它存在的任务或中断处理程序都能访问。任何任务都可以写同一个队列,任何任务也可以读队列。实际上,一个队列有多个写任务非常常见,相比而言一个队列多个读任务就要少一些了。
阻塞在读队列
当试图读一个队列,可以随意指定一个阻塞时间。队列为空时,可以指定等待队列数据可用在阻塞态保持的时间。一个等待可用数据,处于阻塞态的任务会自动转变为就绪态,当其他任务或中断写入数据到队列时。当期待等待数据的时间到期等待数据的任务也会自动转为就绪态。
队列可以由多个读任务,所以可能有多个任务在等一个队列数据,而处于阻塞。这时候,只会有一个任务变成可用的。接触阻塞状态的任务应该是优先级最高得哪一个。如果阻塞的任务们优先级相同,等于最久哪个任务就会被解除阻塞。
阻塞在写队列
和读队列一样。可以随意指定写队列阻塞时间。因此,当队列已经满时,可以指定等待可用空间时最大阻塞时间。
队列可以有多个写入者,所以多个任务等待一个写入队列操作而阻塞有可能发生。这时候只有一个任务会解除阻塞。解除阻塞的任务总是优先级最高的任务。如果优先级相同的任务阻塞在写同一个队列,阻塞时间最长的任务会解除阻塞。
阻塞在多个队列上
队列可以设置成组,允许一个任务进入阻塞态,等待成组队列可用。队列集会在4.6章节(多个队列读)展示。
队列的使用
xQueueCreate()接口
队列可能会在使用前创建。
队列使用句柄引用,是一个QueueHandle_t
的类型。xQueueCreate()接口创建一个队列,返回一个QueueHandle_t
类型引用的队列。
FreeRTOS V9.0.0也包含了xQueueCreateStatic()函数,它会静态的分配队列空间。FreeRTOS在队列创建时从堆中分配队列空间。RAM空间既包括队列数据结构也包括队列包含的项目。xQueueCreate()函数在没有足够的堆空间分配队列时会返回NULL指针。第2章有更加详细的堆管理介绍。
// xQueueCreate()原型
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
参数:
uxQueueLength: 队列一次可以保存的项目最大数量
uxItemSize: 队列可以保存的每个项目的字节大小
返回值: 返回NULL指针,队列不能被创建。没有足够的堆空间分配给队列数据结构
返回非NULL表示成功创建队列。这个返回值保存了分配的队列数据结构句柄
队列成功创建后,xQueueReset()函数可以用来返回队列初始空状态。
xQueueSendToBack()和xQueueSendToFront()函数
如你所想,xQueueSendToBack()是将写入数据存储到队列最后,xQueueSendToFront()是将写入数据存储到队列头。
xQueueSend()和xQueueSendToBack()是相同的。
注意不要在中断处理程序中使用xQueueSendToBack()和xQueueSendToFront(),可以使用中断安全版本xQueueSendToBackFromISR()和xQueueSendToFrontFromISR()。它们会在第6章介绍。
// 写入队列函数原型
BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
// 参数
// xQueue: 数据要写入的队列句柄。这个队列句柄是调用xQueueCreate()函数创建队列的返回值。
// pvItemToQueue: 指向要复制进队列的数据指针。队列每个项目的大小是在队列创建的时候设置的,会将pvItemToQueue指向数据的这些为复制到队列中,就是写入操作。
// xTicksToWait: 如果队列已经写满,队列会进入阻塞态等待空间可用(有空闲空间保存数据)的最大等待tick数量。如果这个参数是0 xQueueSendToBack()和xQueueSendToFront()都会立即返回。这里的最大阻塞时间使用tick数量指定的。而非绝对的时钟时间,tick数量和绝对时钟时间之间比例取决于系统频率。pdMS_TO_TICKS()宏可以将时钟时间转换成tick数量,用于这个函数的第三个参数,注意这里的时钟时间是ms作为单位。将xTicksToWait设置成portMAX_DELAY会使任务一直等待下去(没有超时时间),FreeRTOS.h中的`INCLUE_vTaskSuspend`需要设置为1才支持这个特性。
// 返回值: 有两个可能返回值。
pdPASS:写入成功。如果超时时间不是0,函数返回前任务可能进入阻塞态,等待队列空间空闲。但数据在超时之前写入成功,就返回写入成功。
errQUEUE_FULL:因为队列已满数据不能写入到队列。如果超时值不为0,任务等待其他任务或中断释放队列空间而进入阻塞态,设置的超时时间在队列空间被释放前,就返回这个错误。
xQueueReceive()函数
xQueueReceive()用于从一个队列上读取数据。读取的项会从队列中删除。
注意不要在中断处理函数中使用xQueueReceive()函数,中断处理函数中应该使用它的中断版本xQueueReceiveFromISR(),它会在第6章介绍。
// xQueueReceive()原型
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
// 参数
// xQueue: 要读取数据队列句柄。这个句柄是由xQueueCreate()函数创建返回的。
// pvBuffer: 接收数据复件的内存指针地址。队列存储数据项大小是在队列创建时设定的。pvBuffer指针指向内存必须足够保存队列项。
// xTicksToWait: 队列为空时,任务等待有数据可以读取而进入阻塞状态,维持这样的阻塞状态的最大tick数量。如果xTicksToWait为0,xQueueReceive()会立即返回。阻塞时间是用tick数量表示的,而非时钟时间,tick数量的多少和系统频率有关。pdMS_TO_TICKS可以用来将时钟时间转换成tick数量,刚好总在这里。设置xTicksToWait为portMAX_DELAY会引起xQueueReceive()一直阻塞(没有超时时间)——直到有数据可以读取。但需要FreeRTOS.h文件中的INCLUE_vTaskSuspend设置为1。
// 返回值: 有两种可能返回值
pdPASS: 读取成功。如果xTicksToWait不为0,调用任务可能进入阻塞态等待队列数据可用。但在超时tick数量到来之前读取操作成功,就返回这个值。
errQUEUE_EMPTY: 因为队列为空,不能从队列读取到数据。如果xTicksToWait不为0,调用任务可能进入阻塞态等待其他任务或中断处理程序向队列写入数据。但写入数据在超时后到来,就返回这个errQUEUE_EMPTY。
uxQueueMessagesWaiting()函数
uxQueueMessagesWaiting()用于获取队列中保存的数据数量。
注意uxQueueMessagesWaiting不能用于中断处理程序,可以使用uxQueueMessagesWaitingFromISR()中断版本。
// uxQueueMessagesWaiting()原型
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
// 参数
// xQueue: 待计算队列句柄。这个队列句柄是使用xQueueCreate()创建队列时返回的句柄。
// 返回值; 当前队列中保存的项目数量。如果返回0表示对队列为空。
例10:队列读阻塞
这个例子演示了创建队列,多任务写队列,读队列。队列是用来存储uint32_t
。写队列任务不会阻塞,读队列任务会阻塞。
写队列任务优先级比读队列任务优先级更低。意味着队列中项目数量不会超过1项。因为一旦有任务向队列写数据,读队列任务就会抢先写队列任务,读取数据,删除刚写入数据,队列再次变成空队列。
列表45展示了任务写队列实现。这个任务又两个实例,一个向队列写100,另外一个向队列写200。任务参数用来向每个人物实例传递这个值。
// 例10写队列任务实现,列表45
static void vSenderTask(void *pvParameters)
{
int32_t lValueToSend;
BaseType_t xStatus;
//通过这个变量传递两个任务实例参数,写入队列中。通过这个方式,每个实例可以使用不同的值。队列存储int32_t类型值,所以需要这种格式变量
lValueToSend =(int32_t)pvParameters;
for(;;){
// 向队列写数据
// 第一个参数是数据保存的队列。这个队列会在调度器启动前创建,所以更是在任务执行前
// 第二个参数是写入数据保存地址,这里就是lValueToSend地址
// 第三个参数是阻塞时间。就是队列满了,任务进入阻塞态等待队列有空闲位置最大可以等待的时间。这里不指定等待时间,因为队列中不会超过2个项目,因此不可能会满。
xStatus = xQueueSend(xQueue, &lValueToSend, 0);
if(xStatus != pdPASS)
vPrintString("Could not send to the queue\r\n");
}
}
列表46展示了读队列任务实现。读队列任务阻塞时间设置为100ms,任务会等待数据可用而进入阻塞状态。队列接收到数据或100ms时间到而没有数据可用,就会离开阻塞态。这个例子中不会出现100ms超时的情况,因为有两个写任务不听写队列。
// 例10,读队列任务实现。列表46
static void vReceiverTask(void pvParameters){
// 声名变量用于保存从队列接收的值
int32_t lReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
for(;;){
// 这个任务会发现队列常为空,因为一旦队列有数据,就会立即被删除
if(uxQueueMessagesWaiting(xQueue) != 0)
vPrintString("Queue shuld have been empty!\r\n");
// 安妮队列读数据
// 第一个参数是读取数据的队列。这个队列会在调度器启动前更在任务运行前创建
// 第二个参数是接收数据将会被保存的地方。这里只是一个保存接收数据的地址,但请确保它有足够的空间接收数据。
// 第三个参数是阻塞时间。当队列为空,任务进入阻塞态,等待队列有可用数据,最大保持在阻塞态的tick数量。
xStatus = xQueueReceive(xQueue, &lReceivedValue, xTicksToWait);
if(xStatus == pdPASS)
vPrintStringAndNumber("Received = ", lReceivedValue);
else
//即使等待了100ms也没有从队列接收到数据。这肯定是一个错误,因为写入任务是自由,连续向队列写入的
vPrintString("Could not receive from the queue!\r\n");
}
}
列表47包括了main()函数的定义。它在启动调度器之前只是简单的创建了队列和3个任务。队列创建来存储最多5个int32_t
类型的值。3个任务的优先级设置时保证队列中不会包含超过1个值。
// 例10,main()函数实现。列表47
/* 声名一个QueueHandle_t类型的变量。这个变量用来保存3个任务访问队列句柄*/
QueueHandle_t xQueue;
int main(void){
/* 队列创建来保存5个值,每个值都足够用于保存int32_t类型的值*/
xQueue = xQueueCreate(5, sizeof(int32_t));
if(xQueue != NULL){
/* 创建两个写队列任务实例。任务参数用于传递向队列写入的数据值,因此1个任务会连续的向队列写100,另外一个任务会连续的向队列写200。它们都以优先级1创建*/
xTaskCreate(vSenderTask1, "Sender1", 1000, (void *)100, 1, NULL);
xTaskCreate(vSenderTask2, "Sender2", 1000, (void *)200, 1, NULL);
/* 创建一个读队列任务。任务优先级是2,优先级比写队列任务高*/
xTaskCreate(vReceiverTask, "receiver1", 1000, NULL, 2, NULL);
/* 开启调度器 */
vTaskStartScheduler();
}
else{
/* 创建队列失败 */
}
/* 如果一切顺利,程序不会运行到这里,调度器应该正在运行任务。如果运行到这里可能是没有足够的堆分配给空闲任务。可以查看第2章堆管理获取更多相关信息 */
for(;;);
}
两个写队列任务有相同优先级。会导致两个写任务顺序执行。例10的输出结果表现出了这一点。
# 例10输出结果。 图32
Received = 100
Received = 200
Received = 100
Received = 200
Received = 100
Received = 200
Received = 100
Received = 200
Received = 100
Received = 200
Received = 100
Received = 200
图33的时序图也展示了两个写任务交替执行。
Receiver |--- --- --- --- |
Sender2 | --- --- |
Sender1 | --- ---|
t1 time
1. 读取队列任务有最高优先级首先执行。它试图从队列读取数据,但队列时空的,所以进入阻塞态等待数据可用。写队列任务2在读队列任务进入阻塞态后运行。
2. 写队列任务2向队列写一个数据,引起读队列退出阻塞态。读队列任务有最高优先级,因此立即抢占写队列任务2。
3. 读队列任务读取数据,清空队列,然后又一次进入阻塞态。这一次写队列任务1在读队列任务阻塞后运行。
4. 写队列任务1向队列写入数据,引起读队列任务退出阻塞状态,抢占写队列任务1。之后再次循环如此操作。
多个资源中接收数据
在FreeRTOS设计中,从多个资源中接收数据时很常见的。接收任务需要知道数据从哪里而来,如何处理。一个简单的解决方法是用一个所有数据源包含在结构域中的单独的队列结构。这种框架图在图34中有展示。
# 结构体中包含数据源来源,队列包含多种数据源实例框架。图34
```bash
-------------
|CAN总线任务 | -
------------- \eDataID=eMotorSpeed
\lDataValue=10
------------- \ ------------------------ -----------
|其他任务 | ---->|Queue X X X X X X | -> | 控制器 |
------------- / ------------------------ -----------
/eDataID=eSpeedSetPoint
------------- /lDataValue=5
|HMI任务 | -
-------------
-------------------------
| typedef struct |
| { |
| ID_t eDataID; |
| int32_t lDataValue |
| }Data_t; |
-------------------------
图34说明如下:
- 创建一个队列保存
Data_t
类型数据。这个数据结构的成员包含一个数据类型和数据值。 - 一个控制器任务中心,用于管理主要的系统功能。它需要对输入无处反应,改变系统状态,利用队列进行信息交流。
- 一个CAN总线任务用于封装CAN总线接口功能。当CAN总线任务接收到消息就解析它。在用
Data_t
结构将解析好的数据发送给中心控制器任务。Data_t
结构体中的eDataID成员可以让控制器任务知道是什么数据,在这个实例中它是一个电机速度的值。结构体中的lDataValue成员用来让控制器直到确切的电机速度值是多少。 - 一个人类机器接口任务用来封装所有HMI功能。这个机器可能会输入命令,以各种方式获取值,这些值必需由HMI任务决定和解释它们。当输入一个新命令,HMI任务负责以
Data_t
的形式传给中心控制任务。结构体中的eDataID用来让控制任务知道数据时什么类型,这个实例中是一个新的指针值。结构体中的lDataValue成员用于告诉控制任务具体的指针值是多少。
例11,写入队列阻塞和给队列中写入结构体
例11和例10类似,但各任务优先级相反,读队列任务的优先级比写队列任务优先级更低。同时,这个队列时用来传递结构体的,而不是整数。
列表48展示了例11使用的结构体定义。
// 队列将要使用的结构体定义,扩展定义了5个例11会用到的变量
/* 定义一个用于识别数据类型的枚举变量格式*/
typedef enum {
eSender1,
eSender2
}DataSource_t;
/* 定义传递给队列的结构体格式*/
typedef struct {
uint8_t ucValue;
DataSource_t eDataSource;
}Data_t;
/* 定义两个传递给队列的Data_t结构数据 */
static const Data_t xStructsToSend[2] = {
{100, eSender1}, //Sender1 使用
{200, eSender2} //Sender2 使用
}
例10中读任务有最高优先级,所以队列中最多只能有一个项目。这导致一旦队列中放入数据,读队列任务就会抢先写队列任务。在例11中,写队列任务有最高优先级,所以队列将会经常是满的,读队列任务会被一个写队列任务抢先,从而立即让队列保持满状态。之后写任务再次进入阻塞态,等待队列空间中再次出现空闲位置。
列表49展示了写队列任务的实现。写队列任务指定了超时时间100ms,因此每次队列满后,写队列任务就会阻塞等待队列出现空余空间。当队列中有空间可用或者100ms超时时间到期后,它就会离开阻塞状态。这个例子中,100ms超时时间永远不会到期,因为读队列任务会持续删除队列中的项目。
// 例11,写队列任务实现。列表49
static void vSendTask(void *pvParameters){
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
for(;;){
/* 向队列写数据
* 第二个参数是要写入结构的变量地址。这个地址是用任务参数传递的,这里就是指pvParameters
* 第三个参数是最长阻塞时间,如果队列已满,写队列任务阻塞以等待队列有可用空间的最长阻塞时间。这里需要设置阻塞时间,因为写队列任务的优先级比读队列任务的优先级高,队列总是处于满状态。当两个写队列任务都处于阻塞状态是读队列任务就会删除一个队列中的项目。
*/
xStatus = xQueueSendToBack(xQueue, pvParameters, xTicksToWait);
if(xStatus != pdPASS)
vPrintString("Could not send to the queue\r\n");
}
}
读队列任务拥有最低优先级,所以它只会在两个写队列任务都是阻塞态时才会运行。只有队列满后写队列任务才会进入阻塞状态,所以只有在队列满后,读队列任务才会运行。因此即使读队列任务不设置超时时间,它也可以读取到队列数据。
列表50展示了读队列任务实现。
// 例11读队列任务实现。列表50
static void vReceiverTask(void *pvParameters){
/* 定义一个结构体用于保存从队列接收到的数据 */
Data_t xReceiverStructure;
BaseType_t xStatus;
for(;;){
/* 因为读队列任务优先级最低,所以只有在两个写队列任务都进入阻塞态时它才会运行。写队列任务只有在队列满后才会进入阻塞态,因此队列中数据数量总是等于队列长度,这个例子中是3*/
if(uxQueueMessagesWaiting(xQueue) != 3)
vPrintString("Queue should have been full!\r\n");
/* 从队列读取数据
* 第二个参数是用于保存接收数据的缓存地址。这个实例中只是一个简单的变量地址,注意这个产量地址需要有足够的空间保存结构体的值
* 第三个参数是阻塞时间,如果队列为空,读队列任务将会维持阻塞态的最大tick数量。这个例子中不需要阻塞时间,因为这个任务只会在队列已满后运行
*/
xStatus = xQueueReceive(xQueue, &xReceiverStructure, 0);
if(xStatus == pdPASS){
/* 从队列接收数据成功,打印接收数据值和接收数据类型*/
if(xReceiverStructure.eDataSource == eSender1)
vPrintStringAndNumber("From Sender 1 = ", xReceiverStructure.ucValue);
else
vPrintStringAndNumber("From Sender 2 = ", xReceiverStructure.ucValue);
}
else{
/* 没有从队列接收到数据,这肯定发生了错误,因为这个任务只有在队列已满的时候才能运行*/
vPrintString("Could not receive from the queue.\r\n");
}
}
}
main()函数相比上一个例子改变很少。队列创建来保存3个Datab_t
结构的数据,写队列任务和读队列任务的优先级交换。列表51展示了main()函数的实现。
// 例11main()函数实现。列表51
/* 定义一个各任务传递数据的队列*/
QueueHandle_t xQueue;
int main(void){
/* 队列创建用于保存3个Data_t结构数据*/
xQueue = xQueueCreate(3, sizeof(Data_t));
if(xQueue != NULL){
/* 创建两个写队列任务实现。任务参数用于传递要写入队列的数据结构,这里一个任务会连续写xStructsToSend[0]进队列,另外一个任务将连续写xStructsToSend[1]进队列。两个任务优先级都是2,它们的优先级比读队列任务高*/
xTaskCreate(vSenderTask, "Sender1", 1000, &(xStructsToSend[0]), 2, NULL);
xTaskCreate(vSenderTask, "Sender2", 1000, &(xStructsToSend[1]), 2, NULL);
/* 创建读队列任务。这个任务优先级为1,比写队列任务优先级低*/
xTaskCreate(vReceiverTask, "Receiver", 1000, NULL, 1, NULL);
/* 开启调度器 */
vTaskStartScheduler();
}
else{
/* 队列创建失败 */
}
/* 一切顺利程序不会运行到这里,调度器应该正在执行任务。如果运行到这里,可能是没有足够的堆分配给空闲任务。可以查看第2章堆管理查看更多细节*/
for(;;);
}
例11输出如图35所示
# 例11输出。图35
From Sender 1 = 100
From Sender 1 = 100
From Sender 1 = 100
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
图36展示了高优先级写队列任务和低优先级读队列任务运行时序图。图后面有对图36更多的解释,描述了为什么首先会有4个相同消息来自同一个写队列任务。
# 例11执行时序图
Receiver | --- --- --- --- --- ---|
Sender 2 | - --- --- |
Sender 1 |------------ --- --- --- |
t1 t2 t4 t5 t6 t7
t3
时序图说明:
t1: 写队列任务1向队列写入3个数据结构。
t2: 队列写满写队列任务1进入阻塞态等待他的下次写入。写队列任务2是最高优先级任务,进入运行状态。
t3: 写任务队列2发现队列已满,所以进入阻塞态等待第一次写入完成。读队列人现在是最高优先级任务进入运行状态。
t4: 两个拥有最高优先级的写队列任务都进入阻塞态,等待队列拥有空闲空间,导致读队列任务删除队列数据后立马被抢占。写队列任务1和写队列任务2有相同优先级,因此调度器选择想要进入运行状态等待最久的哪个任务进入运行状态,这里就是写队列任务1。
t5: 写队列任务1向队列写入另外一个数据项目。之前队列中只有一个空闲位置,一次写入操作后队列再次满状态,因此写队列任务1进入阻塞态,等待下一次写入操作。读队列任务再次成为最高优先级任务进入运行状态。写队列任务到此为止进行了4次写队列操作,写队列任务2任然在等待它的第一次写队列操作。
t6: 两个最高优先级的写队列任务都进入阻塞态,等待队列释放空间,因此读队列任务成为最高优先级任务运行。读队列任务读取数据后,删除队列一个数据项目,留出一个队列空闲位置,写队列任务立即抢占读队列任务。这次写队列任务2比写队列任务1等待时间更长,被调度器选中执行。
t7: 写队列任务2向队列写一个数据项目。因为写入前队列只有一个空闲位置,因此写入后写队列任务2进入阻塞态等待下一次写入操作。写任务1和写任务2都进入阻塞态,读队列任务成为最高优先级任务进入运行状态。
处理大型或可变大小数据
队列指针
如果要存放在队列中的数量大小太大,更好的方法的队列中存放数据的指针而不是数据本身。存放数据指针在处理时间和RAM使用数量上都更加有优势。但队列存放指针时有以下注意:
- RAM中的指针所有者需要记得释放:
当任务通过的指针共享内存时,需要保证多个任务不能同时更改内存内容,注意其它会引起内存内容不可用或不一致情况。理想情况是只用写队列任务有权访问队列指向的内存,只有读队列任务在队列接收到指针后访问内存内容。 - 被指向RAM保持可用:
如果指向内存是动态分配的,或在一个内存池中。就只应有一个任务有责任释放这个内存。不会有其它任务在拇指释放后试图访问这个内存。任务不能通过指针访问一个已经被分配的任务栈。如果这个傻栈被更改就不再可用了。
通过列表52,列表53,列表54的例子,展示了如何用队列指针从一个任务给另一个任务发送信息:
- 列表52创建一个能保存5个指针的队列
- 列表53分配内存,写字符串到内存,然后发送指向内存指针给队列。
- 列表54从队列中读取指针,然后读取指针指向数据。
// 创建一个保存指针的队列。 列表52
/* 声明一个QueueHandle_t的队列句柄,用于保存指针 */
QueueHandle_t xPointerQueue;
/* 创建一个最大保存5个指针的队列,这个例子中是保存字符串指针 */
xPointerQueue = xQueueCreate(5, sizeof(char *));
// 用队列发送指针。列表53
/* 一个包含缓存的任务,向缓存写一个字符串,然后发送缓存地址给列表52创建的队列 */
void vStringSendingTask(void *pvParameters){
char *pcStringToSend;
const size_t xMaxStringLength = 50;
BaseType_t xStringNumber = 0;
for(;;){
/* 包含一个最大xMaxStringLength的缓存。prvGetBuffer()没有实现--它可以从一个池里获取缓存,也可以自动分配缓存。也可以使用pvPortMalloc()动态分配 */
pcStringToSend = (char *)pvPortMalloc(xMaxStringLength);
/* 向缓存写字符串 */
snprintf(pcStringToSend, xMaxStringLength, "String number %d\r\n", xStringNumber);
/* 增加数量,使每次写入缓存数据不同 */
xStringNumber++;
/* 向队列中写入缓存地址指针,队列是在列表52中创建的。缓存地址就是pcStringToSend变量的地址。*/
xQueueSend(xPointerQueue, &pcStringToSend, portMAX_DELAY);
}
}
// 用一个队列读取缓存指针。列表54
/* 一个读取队列缓存指针的任务,这具队列是由列表52创建,缓存指针由列表53写入。缓存中有一个字符串。*/
void vStringReveivingTask(void *pvParameters){
char *pcReceivedString;
for(;;){
/* 读取缓存指针 */
xQueueReceive(xPointerQueue, &pcReceivedString, portMAX_DELAY);
/* 打印缓存中的字符串 */
vPrintString(pcReceivedString);
/* 缓存已经不再使用,释放它*/
vPortFree(pcReceivedString);
}
}
用队列发送不同格式,长度的数据
前面的例子展示了2个有用的设计模型;向队列发送结构体和指针。合并这些技术可以允许任务一个任务用一个单独的队列接收任意格式数据从任意数据源。FreeRTOS+TCP TCP/IP栈的实现应时一个如何实现它的典型实例。
TCP/IP栈运行在一个单独的任务上,需要处理来自不同源的事件。不同的事件格式关系到不同格式和长度的数据。所有相对于TCP/IP任务的外部事件都是用一个IPStackEvent_t
的格式表述,并写入到一个TCP/IP任务上的一个队列中。IPStackEvent_t
结构在列表55中展示。其中的pvData成员是用来保存确定值的指针,这个指针指向保存数据的缓存。
// TCP/IP任务使用到的结构。列表55
/* 一个用于识别TCP/IP栈事件的枚举类型子集 */
typedef enum {
eNetworkDownEvent = 0, /* 风络已经丢失或需要重新连接 */
eNetworkRxEvent, /* 从网络上接收到一个数据包 */
eTCPAcceptEvent, /* 调用FreeRTOS_accept()接收或等待一个新的连接 */
}eIPEvent_t;
/* 用于描述事件的结构,它会被写入队列传递给TCP/IP任务 */
typedef struct IP_TASK_COMANDS {
/* 用于识别事件的枚举格式。可以查看上面的eIPEvent_t定义 */
eIPEvent_t eEventType;
/* 一个存储数据的通用指针,指针指向数据缓存 */
void *pvData;
}IPStackEvent_t;
TCP/IP事件例子,和它们相关数据包括:
- eNetworkRxEvent: 从网络接收到的数据包。从网络接收到的数据包会通过
IPStackEvent_t
格式传递给TCP/IP任务。这个结构的eEventType成员𠍼设置为eNetworkRxEvent,这个结构的pvData成员会指向包含接收数据缓存的指针。列表56展示了它的伪代码。
// 一个IPStackEvent_t结构如何用于发送从网络接收的数据到TCP/IP任务的伪代码。列表56
void vSendRxDataToTheTCPTask(NetworkBufferDescriptor_t *pxRxedData){
IPStackEvent_t xEventStruct;
/* 填写IPStackEvent_t结构,接收到的数据存储在pxRxedData中*/
xEventStruct.eEventType = eNetworkRxEvent;
xEventStruct.pvData = (void *)pxRxedData;
/* 将IPStackEvent_t结构写入到TCP/IP任务 */
xSendEventStructToIPTask(&xEventStruct);
}
- eTCPAcceptEvent:一个接受或等待的套接字,来自于客户端。接受事件是由调用
FreeRTOS_accept()
函数写入队列,从而传递给TCP/IP任务的。结构中的eEventType设置为eTCPAcceptEvent,pvData成员设置为接受套节字名柄。列表57展示了伪代码。
// 一个IPStackEvent_t结构如何用于传递一个已经连接的套节字句柄给TCP/IP任务的伪代码。列表57
void vSendAcceptRequestToTheTCPTask(Socket_t xSocket){
IPStackEvent_t xEventStruct;
/* 填充IPStackEvent_t结构 */
xEventStruct.eEventType = eTCPAcceptEvent;
xEventStruct.pvData = (void *)xSocket;
/* 将IPStackEvent_t结构写入到TCP/IP任务 */
xSendEventStructToIPTask(&xEventStruct);
}
- eNetworkDownEvent: 网络需要连接或重新连接。网络掉线事件由网络界面写入,使用一个
IPStackEvent_t
结构发送给TCP/IP任务。结构中的eEventType设置为eNetworkDownEvent,网络掉线事件不包含任何数据,因此结构中的pvData成员不会被使用。列表58展示了它的伪代码。
// 一个IPStackEvent_t结构如何被用于传递一个网络掉线事件任务给TCP/IP任务的伪代码。列表58
void vSendNetworkDownEventToTheTCPTask(Socket_t xSocket){
IPStackEvent_t xEventStruct;
/* 填充IPStackEvent_t结构 */
xEventStruct.eEventType = eNetworkDownEvent;
xEventStruct.pvData = NULL;
/* 将IPStackEvent_t发送给TCP/IP任务 */
xSendEventStructToIPTask(&xEventStruct);
}
列表59展示了读和处理这些事件的TCP/IP任务代码。可以看到它从队列接收到数据,再通过IPStackEvent_t
结构中的eEventType成员决定如何解释结构中的pvData成员。
// 一个IPStackEvent_t结构是如何接收和处理的伪代码展示。列表59
IPStackEvent_t xReceiverEvent;
/* 阻塞在网络事件上,等待接收到一个网络事件或xNextIPSleep ticks到期还没有收到数据。如果是因为超时,而不是接收到数据eEventType会被设置为eNoEvent。*/
xReceiverEvent.eEventType = eNoEvent;
xQueueReceive(xNetworkEventQueue, &xReceiverEvent, xNextIPSleep);
/* 接收到那种事件 */
switch(xReceiverEvent.eEventType){
case eNetworkDownEvent:
/* 试图重新建立一个连接。这个事件不会有关联数据 */
prvProcessNetWorkDownEvent();
break;
case eNetworkRxEvent:
/* 网络接口已经接收到一个数据包。一个指向接收数据缓存的指针保存在IPStackEvent_t结构的pvData成员中。处理接收到的数据。 */
prvHandleEthernetPacket((NetworkBufferDescriptor_t *)(xReceiverEvent.pvData));
break;
case eTCPAcceptEvent:
/* 调用了FreeRTOS_accept()接口函数。已经建立连接的套节字句柄被保存在IPStackEvent_t结构中的pvData成员中。*/
xSocket = (FreeRTOS_Socket_t *)(xReceiverEvent.pvData);
// 怀疑这里的pxSocket应该是xSocket
xTCPCheckNewClient(pxSocket);
break;
/* 其它事件格式会以相同的方法处理,这城不再显示 */
}
从多个队列读
队列集
程序中经常需要一个单独的队列读取不同来源的数据。前面的章节展示了如何用一个整洁,有效和方式实现它。但有时候程序员同其它人合作限制他们的设计赞扬,需要对一些数据源使用单独的队列。比如,比如第3部分代码用整数设计可能更改适用于这个队列(没有传递参数,只有事件类型),这种情况下就可以使用队列集。
队列集允许任务在不依次轮询第个队列的情况下读多个队列决定一个或多个队列包含数据。
相比同样功能使用一个单独队列,通过结构传递数据。使用队列集从多个队列源接收数据更不整洁和高效。因此,队列集只有大部分设计都都觉得需要时才推荐使用。
下面描述了如何使用一个队列集:
- 创建一个队列集。
- 添加队列到队列集。信号量也可以添加到信号集。信号量会在本书接下来章节说明。
- 读取队列集,决定那些队列包含数据。当队列集中的队列接收到数据,接收到数据的队列句柄会发送给队列集,读取队列集的任务调用就会返回。因此,如果一个队列句柄从一个队列集返回,那么这个句柄引用的队列就是有数据的队列,任务就可以从这个明确的队列上读数据。
注意:如果队列是队列集的成员,还要从队列上读数据。除非队列句柄已经被队列集先读取了
队列集功能只有在FreeRTOSConfig.h文件中的configUSE_QUEUE_SETS
设置为1时才可以使用。
xQueueCreateSet()接口
一个队列集使用前必须明确的创建。
队列集是用句柄引用的,它的格式是QueueSetHandle_t
。xQueueCreateSet()函数会创建一个队列队列集,并返回它创建队列集的引用。
// xQueueCreateSet()函数原型。列表60
QueueSetHandle_t xQueueCreateSets(const UBaseType_t uxEventQueueLength);
// 参数说明
// uxEventQueueLength:
/* 当队列是队列集的一部分时,队列句柄会发送给队列集。uxEventQueueLength定义了队列集中可以保存的最大队列句柄数量。
* 队列句柄只会在队列加入到队列集中时才会被发送给队列集。如果队列集中所有队列都满了,就不能发送队列句柄给队列集了。因此队列集的最大成员数必须是队列集中每个队列长度之和。
* 作为一个例子,如果队列集中有3个空队列,第个队列长度是5,那么队列集中队列满之前总共可以接收15个项目(3个队列*每个5个项目)。为保证队列集能接收到每个发送给他的项目,例子中uxEventQueueLength就必须设置为15。
* 信号量也可以加入到队列集。二进制和普通信号量会在本书下面介绍。为了计算uxQueueLength,二进制信号量长度是1,普通信号量长度是信号量最大值。
* 作为另一个例子,如果一个队列集包含一个长度为3的队列,和一个二进制信号量(长度为1),uxEventQueueLength就必须设置为4(3+1)。
*/
// 返回值
/* 如果返回NULL,那么创建信号集失败。因为没有足够的堆空间让FreeRTOS分配给队列集结构和存储空间。
* 返回一个非NULL值,表示信号集创建成功。返回值应该保存信号集句柄
*/
xQueueAddToSet()接口
xQueueAddToSet()将一个队列或信号量加入到信号集。信号量会在本书接下来介绍。
// xQueueAddToSet()原型。列表61
BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);
// 参数
/* xQueueOrSemaphore:
* 将被加入到信号集的队列或信号量句柄。
* 队列句柄和信号量句柄都能被转换成QueueSetMemberHandle_t格式
* xQueueSet:
* 队列和信号量要被加入到的队列集句柄。
*/
// 返回值
/* 可能有两个返回值
* pdPASS: 如果队列或信号量成功加入到队列集中,就返回pdPASS
* pdFAIL: 如果队列和信号量不能加入到队列集,就返回pdFAIL.
* 队列和二进制信号量只能在为空时才能被加入到队列集。普通信号量只有在为0时才能加入到队列集。队列和信号量在一个队列中只能被设置为一个成员。
*/
xQueueSelectFromSet()接口
xQueueSelectFromSet()从队列集中读取一个队列句柄。
当队列集中的一个队列和信号量接收到数据。接收到数据的队列或信号量会发送给队列集。一个任务调用xQueueSelectFromSet()会返回。如果xQueueSelectFromSet()返回一个句柄,那么这个队列或信号量引用句柄会知道包含的数据,调用任务必须从这个队列或信号量上读取数据。
注意:不要从一个队列集中的队列或信号量上读数据,除非这个队列或信号量句柄从一个xQueueSelectFromSet()调用都返回。只可以读一个由xQueueSelectFromSet()返回的队列和信号量句柄
// xQueueSelectFromSet()原型。列表62
QueueSetMemberHandle_t xQueueSelectFromSet(QueueSetHandle_t xQueueSet, const TickType_t xTicksToWait);
// 参数
/* xQueueSet:
* 那个包含队列或信号量被读取的队列集句柄。这个句柄是通过调用xQueueCreateSet()创建队列集返回的。
* xTicksToWait:
* 如果所有队列集中的队列和信号量都为空,任务调用可以保持在阻塞态,等待队列集中的队列或信号量接收到数据的最大时间数。
* 如果xTicksToWait为0,队列集中所有队列和信号量都为空就立即返回。
* 阻塞时间是用tick数量表示。因此具体时钟时间和tick周期有关。可以使用pdMS_TO_TICKS()宏将ms时钟时间转化为tick数量。
* 设置xTicksToWait为portMAX_DELAY会引起任务一直等待(不使用超时时间),要用这个功能要将FreeRTOS.h中的INCLUE_vTaskSuspend设置为1。
*/
// 返回值
/* 一个非NULL返回值表示一个接收到数据的队列或信号量句柄。如果阻塞时间不为0,那么调用任务可能会进入阻塞等待队列集中队列或信号量数据可用。但在超时前会成功返回队列集中的队列或信号量。句柄会以QueueSetMemberHandle_t格式返回,QueueHandle_t和SemaphoreHandle_t都可以转化为QueueSetMemberHandle_t格式。
* 如果返回NULL,不能从队列集读数据。如果超时时间不为0,那么调用任务会进入阻塞状态,等待另外的任务或中断程序发送数据给队列集中的队列或信号量,但在超时时间过期之后。
*/
例12使用队列集
这个例子创建了2个写任务和1个读任务。写任务通过2个不同的队列传递数据给读任务,每个任务一个队列。2个队列加入到一个队列集,读任务从队列集中读数据,决定那个队列中有可用数据。
这些任务,队列,队列集都在main()中创建,它们的实现展示在列表63。
// 例12 main()实现。列表63
/* 定义2个QueueHandle_t变量。两个队列都加入到同一个队列集*/
static QueueHandle_t xQueue1 = NULL, xQueue2 = NULL;
/* 定义1个QueueSetHandle_t变量。这个队列集就是两个队列要加入的队列集*/
static QueueSetHandle_t xQueueSet = NULL;
int main(void){
/* 创建2个队列,它们都用于保存字符指针。接收任务优先级比发送任务优先级高,因此所有队列中一次最多只会存在最多一个数据*/
xQueue1 = xQueueCreate(1, sizeof(char *));
xQueue2 = xQueueCreate(1, sizeof(char *));
/* 创建队列集。2个队列会被加入到这个队列集,第个队列有1个项,因此队列集中队列句柄最大数量必须设置为一次2个(2个队列 * 每个队列1项)*/
xQueueSet = xQueueCreateStatic(1 * 2);
/* 将2个队列加入到队列集 */
xQueueAddToSet(xQueue1, xQueueSet);
xQueueAddToSet(xQueue2, xQueueSet);
/* 创建写队列任务 */
xTaskCreate(vSenderTask1, "Sender1", 1000, NULL, 1, NULL);
xTaskCreate(vSenderTask2, "Sender2", 1000, NULL, 1, NULL);
/* 创建读队列任务,此任务读队列集,再决定那个队列中有数据*/
xTaskCreate(vReceiverTask, "Receiver", 1000, NULL, 2, NULL);
/* 开启调度器 */
vTaskStartScheduler();
/* 正常情况,vTaskStartScheduler()不会返回,因此下面的代码不应执行 */
for(;;);
return 0;
}
第一个发送任务使用xQueue1发送一个字符指针给读队列任务第隔100ms。第二个发送任务使用xQueue2发送一个字符指针给读队列任务每隔200ms。这些字符指针设置来区分2个写入任务。列表64展示了2个写队列任务实现。
// 例12 写队列任务。图64
void vSenderTask1(void *pvParameters){
const TickType_t xBlockTime = pdMS_TO_TICKS(100);
const char * pcMessage = "Message from vSenderTask1\r\n";
for(;;){
/* 阻塞100ms */
vTaskDelay(xBlockTime);
/* 发送任务字符串到xQueue1。因为队列只能保存一个项目,因此没有必要设置阻塞时间。因为读队列任务优先级比此任务高。一旦些任务向队列写入数据,读队列任务就会抢占此任务,使队列再次为空。阻塞时间设置为0*/
xQueueSend(xQueue1, &pcMessage, 0);
}
}
/*-----------------------------------------------------------*/
void vSenderTask2(void *pvParameters){
const TickType_t xBlockTime = pdMS_TO_TICKS(200);
const char * pcMessage = "Message from vSenderTask2\r\n";
for(;;){
/* 阻塞200ms */
vTaskDelay(xBlockTime);
/* 发送任务字符串到xQueue2。因为队列只能保存一个项目,因此没有必要设置阻塞时间。因为读队列任务优先级比此任务高。一旦些任务向队列写入数据,读队列任务就会抢占此任务,使队列再次为空。阻塞时间设置为0*/
xQueueSend(xQueue2, &pcMessage, 0);
}
}
写队列任务使用的队列都是同一个队列集的成员。每次一个任务写一个队列,队列句柄将会发送给队列集。读队列任务调用xQueueSelectFromSet()函数从队列集中读队列句柄。读队列任务从队列集中取得队列句柄后,它就知道那个队列中接收到了数据,因此从具体的队列中读取数据。从队列中读取的指针指向一个字符串,读队列任务会打印出来。
如果调用xQueueSelectFromSet()超时,它会返回NULL。在例12中,xQueueSelectFromSet()调用有一个未定义的阻塞时间,因此不会超时,只有在有队列句柄可以使用时返回。因此,xQueueSelectFromSet()返回前,读队列任务不用检查xQueueSelectFromSet()是否返回NULL。
xQueueSelectFromSet()只会在队列集中有队列有数据时返回,因此读队列时不用设置阻塞时间。
读队列任务实现展示在列表65。
//例12读队列任务实现。列表65
void vReceiverTask(void *pvParameters){
QueueHandle_t xQueueThatContainsData;
char *pcReceivedString;
for(;;){
/* 阻塞在这个队列集上,等待队列集中有队列已经包含数据。
* 将xQueueSelectFromSet()返回的QueueSetMemberHandle_t值强制转换为QueueHandle_t,因为已知队列集中所有成员都是队列(队列集中没有信号量)*/
xQueueThatContainsData = (QueueHandle_t)xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
/* 从队列上读取数据不用定义阻塞时间,因为xQueueSelectFromSet()中如果没有队列有数据就不会返回,xQueueThatContainsData又不是NULL.从这个队列上读取数据不用指定阻塞时间,因为队列上必然包含数据。阻塞时间设置为0*/
xQueueReceive(xQueueThatContainsData, &pcReceivedString, 0);
/* 打印读取到的数据 */
vPrintString(pcReceivedString);
}
}
图37展示了例12的输出结果。可以看到读队列任务从2个写队列任务获取数据。vSenderTask1()的阻塞时间是vSenderTask2()的一半,因此vSenderTask1打印2次,vSenderTask2才打印1次。
Message from vSenderTask1
Message from vSenderTask2
Message from vSenderTask1
Message from vSenderTask1
Message from vSenderTask2
Message from vSenderTask1
Message from vSenderTask1
Message from vSenderTask2
Message from vSenderTask1
Message from vSenderTask1
Message from vSenderTask2
Message from vSenderTask1
Message from vSenderTask1
Message from vSenderTask2
Message from vSenderTask1
更多关于队列集的使用实例
例12展示了一个简单的队列集使用;队列集中只包含队列,它包含的2个队列都是用于传递字符串。在一个实际的应用中,队列集可能包含队列和信号量,队列可能不只传递相同数据格式。这个时候,在使用返回在值之前,需要测试xQueueSelectFromSet()返回值。列表66展示了当队列集中有以下成员时,如何使用xQueueSelectFromSet()返回值。
- 一个二进制信号量
- 一个读字符串指针队列
- 一个读
uint32_t
队列
列表66假设队列和信号量已经创建,并加入到队列集中。
// 使用包含队列和信号量和队列集。列表66
/* 接收字符指针队列句柄 */
QueueHandle_t xCharPointerQueue;
/* 接收uint32_t队列句柄 */
QueueHandle_t xUint32tQueue;
/* 二进制信号量句柄 */
SemaphoreHandle_t xBinarySemaphore;
/* 包含2个队列和1个二进制信号量的队列集 */
QueueSetHandle_t xQueueSet;
void vAMoreRealistickReceiverTask(void *pvParameters){
QueueSetMemberHandle_t xHandle;
char *pcReceivedString;
uint32_t ulReceivedValue;
const TickType_t xDelay100ms = pdMS_TO_TICKS(100);
for(;;){
/* 最多阻塞100ms在队列集上,等待一个队列集成员包含数据 */
xHandle = xQueueSelectFromSet(xQueueSet, xDelay100ms);
if(xHandle == NULL){
/* 调用xQueueSelectFromSet()超时 */
}
else if(xHandle == (QueueSetMemberHandle_t) xCharPointerQueue)
{
/* 调用xQueueSelectFromSet()返回的是读取字符指针队列句柄。从队列上读数据。已知队列中已有数据,阻塞时间设置为0*/
xQueueReceive(xCharPointerQueue, &pcReceivedString, 0);
/* 可以在这里处理接收到的数据 */
}
else if(xHandle == (QueueSetMemberHandle_t)xUint32tQueue){
/* 调用xQueueSelectFromSet()返回的是读取uint32_t格式数据队列句柄。从队列上读数据。书籍队列中已有数据,阻塞时间设置为0*/
xQueueReceive(xUint32tQueue, &ulReceivedValue, 0);
/* 可以在这里处理接收到的数据 */
}
else if(xHandle == (QueueSetMemberHandle_t)xBinarySemaphore){
/* 调用xQueueSelectFromSet()返回的是二进制信号量句柄。获取这个信号量。已知信号量可用,阻塞时间设置为0*/
xSemaphoreTake(xBinarySemaphore, 0);
/* 无论要做什么,都可以在这里获取信号量了*/
}
}
}
用队列创建一个消息盒子
嵌入式社区关于术语没有达成共识。消息各种意味着在不同的实时系统中的各种事项。这本书中的消息盒子术语用来特指一个长度为1的队列。队列可能被叫做消息盒子因为它用于一个程序,而不是它和队列功能不一样。
- 队列时用来从一个任务发送数据到另外一个任务,或者从中断处理函数到任务。发送任务将数据写入队列,接收者读出数据,并从队列中删除数据。数据就完成了从发送者到接受者的传递。
- 消息盒子用于保存可以被任意任务或中断处理函数读取的数据。数据不是通过消息盒子传递的,而是在它被覆盖之前一直都在消息盒子中。发送者可以覆盖消息盒子中的值。接收者从消息盒子读取数据值,但不会删除数据。
这一节描述了2个函数可以将一个队列用作一个消息盒子。
列表67显示创建一个队列用于消息盒子。
// 创建一个队列用作消息盒子。列表67
/* 一个消息盒子可以保存固定数量的数据项。消息盒子创建时会给出消息盒子数据项大小。在这个例子中消息盒子是用来保存Example_t结构的数据。Example_t包含一个时间戳,用于保存消息盒子最后更新时间。这个例子中的时间戳只是起到一个演示作用,真实的消息队列可以保存应用需要的任何数据,也可以不用包含时间戳。*/
typedef struct xExampleStructure {
TickType_t xTimeStamp;
uint32_t ulValue;
} Example_t;
/* 消息盒子是一个队列,因此它的句柄格式是队列句柄格式*/
QueueHandle_t xMailBox;
void vAFunction(void){
/* 创建用于消息盒子的队列。这个队列长度为1,可以使用xQueueOverwrite()接口,这个函数马上会介绍。*/
xMailBox = xQueueCreate(1, sizeof(Example_t));
}
xQueueOverwrite()函数
和xQueueSendToBack()类似,xQueueOverwrite()函数会发送数据到队列。和xQueueSendToBack()不同的是如果队列已满,那么xQueueOverwrite()会覆盖已经在队列里面的数据。
xQueueOverwrite()函数只应该用在长度为1的队列上。这个限制避免了函数实现做出决定,一个满队列中究竟覆盖那一项数据。
注意:中断函数中不要调用xQueueOverwrite(),应当调用它的中断安全版本xQueueOverwriteFromISR()。
// xQueueOverwrite()原型。列表68
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void * pvItemToQueue);
// 参数
/* xQueue:
* 数据要写入的队列句柄。这个队列句柄是使用xQueueCreate()函数创建返回的。
* pvItemToQueue: 一个指向数据的指针,这个数据将被复制到队列中。数据项目的大小是在队列创建时指定的,所以pvItemToQueue指针指向数据的这些位就会被复制到队列存储区域之中。
* 返回值: xQueueOverwrite()函数在队列已满的情况下还是会向队列写数据,因此这个函数只会有一个返回值就是pdPASS。
*/
列表69展示了使用xQueueOverwrite()函数向列表67创建的消息盒子写入数据。
// xQueueOverwrite()使用。列表69
void xUpdateMainbox(uint32_t ulNewValue){
/* 列表67定义了Example_t */
Example_t xData;
/* 向Example_t结构写新数据 */
xData.ulValue = ulNewValue;
/* 将当前tick时间戳存入Example_t结构中*/
xData.xTimeStamp = xTaskGetTickCount();
/* 发送结构到消息盒子,覆盖之前的数据*/
xQueueOverwrite(xMailBox, &xData);
}
xQueuePeek()函数
xQueuePeek()用来从一个队列接收一个项目,而不删除队列中的这个项目。它会从队列头接收数据,不会更改队列中保存的数据,也不会改变队列中数据顺序。
注意不要在中断处理函数中调用xQueuePeek()函数,而应该使用它的中断安全版本xQueuePeekFromISR()。
xQueuePeek()函数的参数,返回值和xQueueReceive()函数一模一样,可以查看xQueueReceive()函数介绍了解。
// xQueuePeek()函数原型。列表70
BaseType_t xQueuePeek(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
列表71展示了用xQueuePeek()接收发送给消息盒子的数据。
// 使用xQueuePeek()接收消息盒子数据。列表71
```C
// xQueuePeek()使用。列表71
BaseType_t vReadMailbox(Example_t *pxData){
TickType_t xPreviousTimeStamp;
BaseType_t xDataUpdated;
/* 这个函数使用信息盒子接受的最新数据更新Example_t结构。在被新数据覆盖前记录pxData中的时间戳*/
xPreviousTimeStamp = pxData->xTimeStamp;
/* 获取消息盒子中的数据更新pxData结构。这里如果使用xQueueReceive()函数会删除队列中的数据,消息盒子中的数据就不能被其他任务获取了。用xQueuePeek()代替xQueueReceive()确保数据依然在消息盒子中。这里指定一个无穷的阻塞时间,如果消息盒子为空,调用任务会一直阻塞直到消息盒子中数据可用为止。也不需要检测xQueuePeek()函数的返回值,因为xQueuPeek()只能在消息盒子有数据时才会返回。*/
xQueuePeek(xMailBox, pxData, portMAX_DELAY);
/* 如果自这个函数最紧调用读取消息盒子更新数据,则返回pdPASS,否则返回pdFALSE。*/
if(pxData->xTimeStamp != xPreviousTimeStamp)
xDataUpdated = pdPASS;
else
xDataUpdated = pdFALSE;
return xDataUpdated;
}