FreeRTOS基础学习(三)-消息队列

目录

一. 基础概念

1.概念

2.消息队列实现的原理

3.队列的使用场景

二. 队列相关的函数及示例代码

1. xQueueCreate()创建队列(指定长度和每项大小)

(1)函数原型

(2)返回值

(3)代码示例

(4)什么时候只要创建一个队列,什么时候需要创建多个队列?

2. xQueueSend() / xQueueSendToBack():向队列尾部发送数据(阻塞可选)

(1)函数原型

(2)返回值

(3)代码示例

3. xQueueReceive() 从队列接收数据(阻塞可选)

(1)函数原型

(2)返回值

(3)代码示例

(4)队列接收或者发送函数中,xTicksToWait什么时候用portMAX_DELAY,什么场景?什么时候需要设置具体时间?

4. xQueueSendFromISR()从 ISR 向队列发送(专用于中断)

(1)函数原型

(2)返回值

(3)示例代码

5. xQueueReceiveFromISR()中断服务函数(ISR)中接收队列数据

(1)函数原型

(2)返回值

(3)示例代码

6. uxQueueMessagesWaiting()获取当前队列中已有多少条数据

(1)函数原型

(2)返回值

(3)示例代码


一. 基础概念
1.概念

队列是FreeRTOS中最常见的任务通讯机制之一,它充当一个缓冲区的角色,允许:

  1. 一个任务“发送”数据进去;

  2. 另一个任务“接收”数据出来;

  3. 数据自动排队,先进先出(FIFO);

  4. 多个任务/中断也可以共用同一个队列(需要注意互斥);

队列是一种先进先出的数据结构,就像我们在学校食堂里排队打饭:前面先来的人先打饭(先被处理),而后面新来的人只能排在队伍的最后(等待);所以,对于队列来说,数据是从队列头(Front)被取出的(xQueueReceive),在队列尾(Back)被添加进去的(xQueueSend),这样能保证发送顺序,数据不会“插队”;

2.消息队列实现的原理

FreeRTOS 中的消息队列本质上是一个内存结构,主要包括:

结构部分
作用
循环缓冲区
实际存放消息的数据空间(FIFO结构);
读写指针
控制读和写的位置(实现先进先出);
计数器
跟踪当前存储了多少条消息、队列总容量;
等待任务链表
等待写入/读取的任务挂起链表;
互斥访问机制
保证多任务访问时线程安全(使用临界区、调度器挂起等);
3.队列的使用场景

队列主要用于 任务之间发送“具体数据”。下面是常见应用场景:

应用场景
说明
中断任务传数据
ISR采集数据后,通过xQueueSendFromISR发送给任务处理,任务避免做过多处理
多个任务集中处理
多个任务把“命令”或“日志”放入队列,由一个集中任务处理,例如日志记录
模块解耦通信
比如UI任务通过队列向控制任务发送消息,不直接调用函数,降低耦合
缓冲作用
数据接收频率高于处理频率时,队列可缓冲避免数据丢失
生产者-消费者模型
一个任务采集/生成数据(生产者),另一个任务处理(消费者)
二. 队列相关的函数及示例代码
1. xQueueCreate()创建队列(指定长度和每项大小)
(1)函数原型
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize); 
//参数解释:
 // 1. uxQueueLength:队列最多能存多少个元素(不是字节,单位是第2个参数); 
// 2. uxItemSize:每个元素的大小(字节),比如你要存 int 类型就写 sizeof(int);
(2)返回值

成功:返回一个队列句柄(后面所有函数都用这个); 失败:返回 NULL(说明内存不够);

(3)代码示例
QueueHandle_t xQueue; 
xQueue = xQueueCreate(10, sizeof(int)); // 创建一个可存 10 个 int 的队列
(4)什么时候只要创建一个队列,什么时候需要创建多个队列?

FreeRTOS 的每个队列只能传输固定大小的一种数据类型(例如 int、float、结构体等),所有发送和接收这个队列的任务都必须按照这个类型来处理,所以争对不同的用法区分如下:

①只创建一个队列的情况:数据类型一致,任务功能一致,如:

  • 多个传感器上报温度值到同一个处理任务;

  • 同一个类型的控制命令传递(如 LED 控制命令);

②需要多个队列的情况:数据类型不同(int vs float vs struct),或者通信方向不同,避免交叉干扰,或者数据源或用途完全不同,比如:

  • 一个任务处理温度,另一个处理按键,数据结构不一样;

  • 一个任务接收传感器数据,另一个任务收网络数据;

③创建一个队列复用的情况:系统资源有限(如在某些阶段复用同一个队列),或数据类型一致 + 使用场景严格区分 + 不会并发访问,比如:

  • 在不同状态机状态中,切换使用同一个队列;

  • 某个任务先传感器采集用,后期用于其他目的,但中间清空队列、确认无冲突;

类别

场景说明

队列个数推荐

队列内容类型示例

说明与建议

传感器采集任务

周期性读取温度、湿度、光照等传感器

每类传感器一个队列

SensorData结构体

同类数据可共享队列,便于统一处理

外部输入任务

按键输入、编码器、UART等

每种设备一个队列

uint8_tchar或自定义结构体

区分不同源输入,避免混淆处理逻辑

控制/状态任务

控制继电器、电机、模式切换等

一个或多个

带命令码的结构体

控制中心任务接收各任务消息,统一调度

网络通信任务

上传传感器数据、接收控制指令

上行+下行各一个

DataMsg/CtrlMsg结构体

一般分离处理上传和下发

日志/串口输出任务

所有任务统一打印/调试信息

一个

LogMsg结构体(字符串)

减少各任务直接 printf带来的阻塞风险

UI显示任务

OLED/LCD显示数据/状态信息

一个

UiMsg结构体(带来源字段)

UI任务统一更新,显示逻辑集中

OTA更新 / 系统控制

OTA触发、系统重启、低功耗切换等

一个

控制指令结构体或枚举类型

专用控制通道,防止误触发

ISR中断通信

中断中将事件通知给任务

一个短小队列

简单标志或ID(uint8_t

短小快速,不可阻塞

2. xQueueSend() / xQueueSendToBack():向队列尾部发送数据(阻塞可选)
(1)函数原型
BaseType_t xQueueSend( QueueHandle_t xQueue,
                       const void *pvItemToQueue, 
                       TickType_t xTicksToWait); 
//参数1. xQueue:你创建好的队列句柄;
 //参数2.pvItemToQueue:要发送的数据地址(是指针!); 
//参数3. 如果队列满了,最多等待多少 Tick 时间(0 表示不等待);
(2)返回值
pdPASS:发送成功; errQUEUE_FULL:发送失败(队列满了 & 设置不等待);
(3)代码示例
int value = 42; xQueueSend(xQueue, &value, pdMS_TO_TICKS(100)); // 最多等待100ms
3. xQueueReceive() 从队列接收数据(阻塞可选)
(1)函数原型
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
 //参数1. xQueue:队列句柄;
 //参数2. pvBuffer:用来保存接收到数据的地址;
 //参数3. xTicksToWait:如果队列为空,最多等多久(0 表示不等);
(2)返回值

pdPASS:发送成功; errQUEUE_FULL:接收失败;

(3)代码示例
//实现:任务 A 每秒发送一个数字,任务 B 接收并打印;
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

QueueHandle_t xQueue;

void TaskA(void *pvParameters) {
    int count = 0;
    while (1) {
        count++;
        xQueueSend(xQueue, &count, portMAX_DELAY);
        printf("Task A sent: %d\n", count);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void TaskB(void *pvParameters) {
    int receivedValue;
    while (1) {
        if (xQueueReceive(xQueue, &receivedValue, portMAX_DELAY)) {
            printf("Task B received: %d\n", receivedValue);
        }
    }
}

void app_main() {
    xQueue = xQueueCreate(5, sizeof(int)); // 队列长度5,每项是 int 类型
    if (xQueue == NULL) {
        printf("Queue creation failed!\n");//检查队列是否创建成功,失败就退出;
        return;
    }

    xTaskCreate(TaskA, "Sender", 2048, NULL, 1, NULL);
    xTaskCreate(TaskB, "Receiver", 2048, NULL, 1, NULL);
}

(4)队列接收或者发送函数中,xTicksToWait什么时候用portMAX_DELAY,什么场景?什么时候需要设置具体时间?

xTicksToWait 是 在队列空(接收)或满(发送)时,任务愿意等待的最大时间(以 Tick 为单位),它决定了任务是 阻塞、非阻塞还是定时等待。下面区分一下不同的使用场景:

①使用 0(非阻塞)的场景

场景类别
示例
使用理由
任务不应该因为队列阻塞
状态检测、周期任务
快速尝试,无数据就跳过
在中断中使用队列函数
ISR中用 xQueueSendFromISR
中断不能阻塞,只能立即返回
任务定期检查某事物状态
检查设备是否发回响应
若无数据立即返回,执行其他逻辑

②使用具体时间(如 pdMS_TO_TICKS(100))的场景:

场景类别
示例
使用理由
实时性要求高,不能长时间阻塞
UART 数据收发
防止任务卡住系统,需适时超时处理
有备选操作或周期性行为
传感器采集任务尝试发送数据,失败就记录
不能一直卡在队列操作上,否则影响采集节奏
处理非关键数据
日志打印任务
如果队列满,可以丢弃不重要日志,保证系统流畅
系统初始化阶段
等待其他模块准备就绪
超时后可报告错误、重试或进入降级模式

③ 使用 portMAX_DELAY 的场景(一直等下去):

场景类别
示例
使用理由
任务接收关键数据,不可漏掉
传感器上传数据任务一直等队列有数据
数据必须接收,不能错过,因此等到为止
系统空闲时等待事件发生
按键检测任务或状态机任务
等事件触发,不用浪费 CPU 资源
接收队列作为任务的触发机制
控制任务仅在接收到命令时运行
使用队列“唤醒”任务是一种高效的事件驱动方式
4. xQueueSendFromISR()从 ISR 向队列发送(专用于中断)

它是中断服务程序(ISR)中往队列里发送数据的函数。作用就像 xQueueSend(),但是普通的 xQueueSend()不能在 ISR 中使用,否则会造成系统崩溃或不确定行为,所以是 xQueueSendFromISR()函数是专为中断场景设计的,线程安全且效率更高。

(1)函数原型
BaseType_t xQueueSendFromISR( QueueHandle_t xQueue,
                              const void *pvItemToQueue, 
                              BaseType_t *pxHigherPriorityTaskWoken);
 //参数1. xQueue:队列句柄(之前用 xQueueCreate() 创建的) 
//参数2.pvItemToQueue:要发送的数据地址(指针)
//参数3. pxHigherPriorityTaskWoken: 
//指向一个变量,如果发送数据后唤醒了一个更高优先级任务,这个变量会被设为 pdTRUE,让我们中断后立即切换去执行被唤醒的这个更高优先级任务;
(2)返回值

pdPASS(值为 1)表示发送成功,数据已成功放入队列; errQUEUE_FULL(值为 0):队列已满,发送失败;

(3)示例代码
//在一个中断函数中:
void IRAM_ATTR gpio_isr_handler(void* arg)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;//先设置成FALSE;
    int val = 123
    // 从中断发送数据到队列
    xQueueSendFromISR(xQueue, &val, &xHigherPriorityTaskWoken);
    // 如果有高优先级任务被唤醒,就请求上下文切换
    if (xHigherPriorityTaskWoken == pdTRUE) {
        portYIELD_FROM_ISR();
    }
}
5. xQueueReceiveFromISR()中断服务函数(ISR)中接收队列数据
(1)函数原型
BaseType_t xQueueReceiveFromISR(
    QueueHandle_t xQueue,
    void *pvBuffer,
    BaseType_t *pxHigherPriorityTaskWoken
);
//参数1. xQueue:队列的句柄,即之前用 xQueueCreate() 创建的队列;
//参数2.pvBuffer:指向变量的指针,用于接收从队列中取出的数据;
//参数3. pxHigherPriorityTaskWoken:
//指向一个变量的指针,用来告知是否需要在中断退出时让系统切换到更高优先级的任务。可以为 NULL(但建议使用);
(2)返回值

pdPASS(值为1)表示接收成功,已经从队列中取出了一个数据项; pdFALSE(值为0)表示队列为空,接收失败;

(3)示例代码
void IRAM_ATTR gpio_isr_handler(void* arg) {
    static uint32_t value = 123;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 向队列发送数据(假设你在别处已经创建好了队列 xQueue)
    xQueueSendFromISR(xQueue, &value, &xHigherPriorityTaskWoken);
    // 接收数据
    uint32_t received = 0;
    if (xQueueReceiveFromISR(xQueue, &received, &xHigherPriorityTaskWoken) == pdPASS) {
        // 接收到的数据 stored in 'received'
    }

    // 如果需要切换上下文(抢占),通知调度器
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR(); // 退出中断后立刻切换任务
    }
}
6. uxQueueMessagesWaiting()获取当前队列中已有多少条数据
(1)函数原型
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
//参数:xQueue是要查询的队列句柄(通过 xQueueCreate() 得到);
(2)返回值

0:队列中没有可接收的消息 ; >0:当前队列中可用的消息数量(最大不会超过队列长度);

(3)示例代码
//示例:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include <stdio.h>

QueueHandle_t xQueue;

void sender_task(void *pvParameters)
{
    int count = 0;
    while (1) {
        xQueueSend(xQueue, &count, portMAX_DELAY);
        count++;
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void monitor_task(void *pvParameters)
{
    while (1) {
        UBaseType_t messages_waiting = uxQueueMessagesWaiting(xQueue);
        printf(" 队列中有 %d 条消息等待接收。\n", messages_waiting);
        vTaskDelay(pdMS_TO_TICKS(1000));
    } 
}

void receiver_task(void *pvParameters)
{
    int value;
    while (1) {
        if (xQueueReceive(xQueue, &value, portMAX_DELAY)) {
            printf("接收到消息:%d\n", value);
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    xQueue = xQueueCreate(10, sizeof(int));

    xTaskCreate(sender_task, "Sender", 2048, NULL, 2, NULL);
    xTaskCreate(receiver_task, "Receiver", 2048, NULL, 2, NULL);
    xTaskCreate(monitor_task, "Monitor", 2048, NULL, 1, NULL);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值