FreeRTOS教程 - 任务间通信:使用队列(Queue)
📚 本文是【FreeRTOS从入门到精通】系列教程的一部分
🔗 教程完整代码已开源:GitHub - Despacito0o/FreeRTOS
⭐ 如果觉得有帮助,欢迎点个Star支持一下!
一、队列简介
队列是FreeRTOS数据传输方式的一种,可以用于任务间数据传输,也可用于任务与中断间数据传输。在之前的教程中,我们在每个任务中都直接使用串口打印信息,这种结构存在一定问题:每个任务既需要产生数据(如处理传感器数据等),又需要关心数据的显示,导致任务逻辑层次不清晰。
使用队列可以将程序分层:
- 把显示相关程序单独拿出来,组成一个显示任务
- 其他任务只需要将各自要显示的数据写入队列
- 显示任务只需要从队列中读取数据进行显示
这样整体结构就变得很清晰:
- 任务1到3可以专注自己的数据生成,而不需要关心如何显示数据
- 显示任务只关心如何显示数据,而不需要关心数据如何产生
下面我们就使用队列实现这种结构。
二、准备工作
首先,打开Despacito文件夹,把工程模板005复制,粘贴并重命名为008,然后打开008。
这是之前多任务的代码,我们先删除多余的程序,每个任务只保留打印程序,然后保存编译一下,没有错误就可以开始本次实验了。
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); //开启GPIOC的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //GPIO模式,赋值为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; //GPIO引脚,赋值为第13号引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //GPIO速度,赋值为50MHz
GPIO_Init(GPIOC, &GPIO_InitStructure); //将赋值后的构体变量传递给GPIO_Init函数
// GPIO_SetBits(GPIOC, GPIO_Pin_13); //将PC13引脚设置为高电平
GPIO_ResetBits(GPIOC, GPIO_Pin_13); //将PC13引脚设置为低电平
uint8_t i = 0;
i++;
if(i == 1)
{
vTaskDelete(myTaskHandler3);
}
if(i == 2)
{
vTaskDelete(NULL);
}
三、创建队列
我们需要使用队列,所以要先包含队列的头文件:
#include "queue.h"
本次实验实现一下任务一、任务二写队列,任务三读数据并打印显示。首先我们需要创建一个队列,使用xQueueCreate()
函数可以动态创建一个队列。
右键转到定义,看一下它的参数。它使用了一个宏定义,定义了这个函数,方便我们使用:
- 第一个参数是队列的长度,即队列可以存放多少个数据
- 第二个参数是每个数据的大小,单位是字节
由于有两个任务需要写数据,我们将队列长度设置为2。每个任务要写的数据是一个字符串,我们设置20个字节的数据大小:
xQueueCreate(2, 20);
函数的返回值就是我们创建的队列句柄,我们可以看一下函数原型:
QueueHandle_t
在main.c上方定义一个变量来接收创建好的队列句柄:
QueueHandle_t myPrintfQueueHandler;
然后在主函数中接收返回值:
myPrintfQueueHandler = xQueueCreate(2, 20);
更严谨的写法还需要判断返回值是否为空(队列创建是否成功),这里简化处理。
四、向队列发送数据
队列创建好后就可以向队列写入数据了。我们使用xQueueSend
函数:
右键转到定义看一下参数,也是一个宏定义函数:
- 第一个参数是队列句柄
- 第二个参数是数据的地址,这个数据的值会被复制进队列
- 第三个参数是阻塞时间,队列满无法写入新数据时会阻塞设定的时间
#define xQueueSend(xQueue, pvItemToQueue, xTicksToWait)
在任务1中,先定义一个字符数组,数组大小就是创建队列时指定的数据大小20字节:
char data[20] = "myTask1 runnig";
然后向队列发送数据:
xQueueSend(myPrintfQueueHandler, data, 0);
第三个参数为0,表示无法写入数据时函数会立即返回。
任务2也做同样的修改,将字符串修改为任务2相关内容:
五、从队列接收数据
我们修改任务3,用于从队列读取数据。读队列使用xQueueReceive
函数:
参数与写队列类似:
- 第一个参数是队列句柄
- 第二个参数是读取的数据存放地址
- 第三个参数是阻塞时间
我们定义一个20字节字符数组用来接收读取的数据,并将阻塞时间设为portMAX_DELAY
,表示读不到数据就一直阻塞在这里,直到有数据可读:
读队列函数会返回状态,我们需要判断读取是否成功。查看返回值类型:
定义一个BaseType_t
类型的变量,用来接收返回的状态,读取成功会返回pdTRUE
:
在判断中,将显示代码放到if语句中,打印接收到的数据。由于读队列没有数据时会一直阻塞,所以下方的delay可以不需要了。
六、编译运行
现在编译一下,确保没有错误:
进入debug,点击运行,可以看到串口正常打印数据,这样就实现了任务间传输字符串:
七、使用结构体传输多种数据类型
如果需要传输很多不同类型的数据,可以使用结构体。具体实现如下:
首先定义一个结构体类型,包含要传输的数据:
修改队列创建时的数据大小,使用sizeof
计算结构体大小:
myPrintfQueueHandler = xQueueCreate(2, sizeof(struct print));
然后修改任务函数。在任务1中定义结构体变量并初始化:
任务2也做同样的修改:
void myTask2(void *arg)
{
while(1)
{
struct print data = {
.data = "myTask2 runnig"
};
// taskENTER_CRITICAL();
// printf("myTask2 runnig\n");
// taskEXIT_CRITICAL();
data.cnt++;
xQueueSend(myPrintfQueueHandler, &data, 0);
vTaskDelay(500);
}
}
最后修改显示任务。定义结构体变量接收数据,并打印结构体中的内容:
void myTask3(void *arg)
{
struct print data;
BaseType_t xStatus;
while(1)
{
xStatus = xQueueReceive(myPrintfQueueHandler, &data, portMAX_DELAY);
if(xStatus == pdPASS)
{
taskENTER_CRITICAL();
printf("%s:%d\n", data.data, data.cnt);
taskEXIT_CRITICAL();
}
// vTaskDelay(500);
}
}
编译运行,可以看到正确显示结果,这样就实现了任务间传递多种参数。
八、总结
通过本次实验,我们学习了:
- FreeRTOS队列的基本使用方法
- 如何使用队列分离数据生成和显示逻辑
- 如何通过结构体传递多种类型的数据
队列是FreeRTOS中最基础也是最常用的任务通信方式,掌握它对进一步学习更复杂的RTOS应用至关重要。
📢 更多精彩内容
- 🔗 完整代码:GitHub - Despacito0o/FreeRTOS
- 📚 更多FreeRTOS教程:查看我的GitHub仓库
- ⭐ 如果这篇文章对你有帮助,别忘了在GitHub上给我点个Star!