FreeRTOS---基础知识4

消息队列(Message Queue)(以下简称队列)是 FreeRTOS 中一种 线程安全 的通信机制,允许任务或中断服务程序(ISR)之间以 异步方式 传递数据。它通过 FIFO(先进先出) 的缓冲区管理数据,支持变长消息、阻塞访问和超时控制,是多任务系统中数据交换的核心组件之一。

1. 为什么要引入队列?

在 FreeRTOS 或多任务系统中,“消息队列可用于数据共享 指的是通过队列机制,实现不同任务或中断服务程序(ISR)之间 安全、有序、异步地传递数据。其核心在于解决传统全局变量共享数据时的线程安全问题,并提供阻塞/唤醒机制以优化资源利用。

问题:若多个任务或中断直接读写同一全局变量或数组,任务A在修改变量时被任务B打断,导致数据不一致。

int counter = 0; // 全局变量

void Task1(void) {
    counter++; // 可能被打断
}

void Task2(void) {
    counter--; // 导致counter最终值不确定
}

任务A和任务C均将传递同一变量数据给任务B时,引入队列

2. 如何实现将任务中数据写入队列?

xQueueSend() 是 FreeRTOS 中用于向消息队列发送数据的核心 API(应用程序编程接口),实现任务间或任务与中断之间的线程安全数据传递

xQueueSend()向队列中写入数据,可简化为一下3个步骤:
(1)短暂关闭中断或调度器,确保拷贝数据时不被中断;

(2)向队列中写入数据;

(3)打开中断或调度器

xQueueSend() 是 FreeRTOS 中用于向消息队列发送数据的核心 API,实现任务间或任务与中断之间的线程安全数据传递。以下是其工作原理、使用方法和注意事项的全面解析:


2.1 函数原型
BaseType_t xQueueSend(
    QueueHandle_t xQueue,        // 目标队列句柄
    const void *pvItemToSend,    // 待发送数据的指针
    TickType_t xTicksToWait      // 阻塞超时时间
);
2.2 参数说明
参数类型说明
xQueueQueueHandle_t队列句柄,由 xQueueCreate() 创建。
pvItemToSendconst void*指向待发送数据的指针,数据会被拷贝到队列中。
xTicksToWaitTickType_t队列满时的最大阻塞时间:
• 0:立即返回(非阻塞)
• portMAX_DELAY:永久阻塞
2.3 返回值
返回值含义
pdPASS (1)数据成功写入队列。
errQUEUE_FULL (0)队列已满且未在超时时间内等到空闲空间(非阻塞或超时)。

3. 队列的休眠与唤醒机制

在 FreeRTOS 中,队列的休眠与唤醒机制 是任务阻塞和调度的核心部分,它通过 任务阻塞列表 和 事件触发机制 实现高效的任务同步。

3.1 休眠(阻塞)的触发条件

当任务调用队列操作函数(如 xQueueSend() 或 xQueueReceive())时,若队列不满足操作条件,任务将进入休眠(阻塞)状态:

  • xQueueSend() 阻塞:队列已满,无法写入新数据。

  • xQueueReceive() 阻塞:队列为空,无数据可读。

QueueHandle_t xQueue = xQueueCreate(5, sizeof(int)); // 队列容量为5

void vSenderTask(void *pvParameters) {
    int data = 42;
    // 若队列满,任务将阻塞(休眠)直到超时或队列有空间
    xQueueSend(xQueue, &data, portMAX_DELAY); 
}

3.2 休眠的底层操作流程

(1) 任务挂起到阻塞列表
  • 步骤

    1. 任务从 就绪列表(Ready List) 移除。

    2. 根据阻塞原因,挂载到队列的 发送阻塞列表(链表)xTasksWaitingToSend)或 接收阻塞列表xTasksWaitingToReceive)。

    3. 记录唤醒时间(若设置了超时 xTicksToWait)。

  • 关键数据结构(队列内部):

    typedef struct QueueDefinition {
        List_t xTasksWaitingToSend;    // 发送阻塞列表
        List_t xTasksWaitingToReceive; // 接收阻塞列表
        // ...其他字段(缓冲区、头尾指针等)
    } Queue_t;
(2) 任务状态切换
  • 任务状态由 运行态(Running) 变为 阻塞态(Blocked)

  • 调度器触发上下文切换,执行其他就绪任务。


3.3 唤醒的触发条件

当队列状态变化时,系统检查阻塞列表并唤醒任务:

  • xQueueSend() 唤醒:队列有空间后,唤醒接收阻塞列表中的任务(若有数据可读)。

  • xQueueReceive() 唤醒:队列有数据后,唤醒发送阻塞列表中的任务(若有空间可写)。

示例场景
  1. 队列初始为空

    • 任务A调用 xQueueReceive() 被阻塞,挂到 xTasksWaitingToReceive

  2. 任务B发送数据

    • 任务B调用 xQueueSend() 写入数据,并检查 xTasksWaitingToReceive

    • 发现任务A在等待,将其从阻塞列表移回 就绪列表


4. 唤醒的底层操作流程

(1) 检查阻塞列表
  • 发送数据时xQueueSend()):

    if (listLIST_IS_EMPTY(&xQueue->xTasksWaitingToReceive) == pdFALSE) {
        xTaskRemoveFromEventList(&xQueue->xTasksWaitingToReceive); // 唤醒接收任务
    }

  • 接收数据时xQueueReceive()):

    if (listLIST_IS_EMPTY(&xQueue->xTasksWaitingToSend) == pdFALSE) {
        xTaskRemoveFromEventList(&xQueue->xTasksWaitingToSend); // 唤醒发送任务
    }
(2) 任务状态恢复
  • 被唤醒的任务状态由 阻塞态(Blocked) 变为 就绪态(Ready)

  • 若任务优先级高于当前任务,触发调度器切换(抢占式调度)。


5. 超时唤醒机制

若任务设置了阻塞超时(如 xTicksToWait = 100),系统会在以下情况唤醒任务:

  1. 正常唤醒:队列条件满足(如队列从满变为非满)。

  2. 超时唤醒xTickCount 到达 xTicksToWait + 阻塞开始时间,任务被移回就绪列表,API 返回 errQUEUE_FULL 或 errQUEUE_EMPTY

举例说明:休眠与唤醒机制
假设TaskB中设置某一条件,当达到该条件时,令flag=1;在TaskA中设置:当flag=1时,进行下面的操作,否则不执行。
如果在裸机情况下(无RTOS),那么每次运行完TaskB后,TaskA都要运行判断TaskB中的flag是否等于1,只有满足flag=1时,TaskA才能继续运行下去。那么入TaskB要运行100万次,则相应的TaskA也要进行100万次判断,这可能占用CPU很大的资源。
在RTOS情况下,引入队列机制,当运行TaskB达到设置将flag置1的条件时,之间将其写入队列中,而TaskA中达到flag=1条件时,选择读队列那么如果TaskB中如果未能达到条件将flag=1,没有写入队列时,队列内容为空,则将TaskA进行阻塞列表;TaskB中如果达到条件将flag=1,写入队列时,则将TaskA进行唤醒,移回就绪列表。这样就会只在TaskB中flag=1时才会运行TaskA,节省CPU资源。

休眠与唤醒的流程图解

6. 队列中的环形缓冲区

环形缓冲区(Circular Buffer 或 Ring Buffer)是 FreeRTOS 队列(Queue)的核心底层数据结构,用于高效管理任务间传递的数据。它通过 循环复用固定大小的内存块,实现 FIFO(先进先出)的数据存取,同时避免动态内存分配的开销。

动态图解过程如下:

  • 循环复用:读写指针到达尾部后自动绕回头部(读:可设置为R=(R+1)%LEN;写:可设置为W=(W+1)%LEN,其中LEN为队列空间的大小),避免内存浪费。

  • 高效操作:插入/删除时间复杂度为 O(1)

  • 线程安全:FreeRTOS 通过临界区保护指针和计数器。

通过此环形缓冲区设计,FreeRTOS 队列实现了高效、确定性的数据传递,非常适合嵌入式实时系统。

7. 队列的组成

队列由队列的头部(也称为队列控制块)队列缓冲区组成。

7.1 队列头部的定义

  • 队列头部 = 队列控制块(Queue_t 结构体)
    它是 FreeRTOS 内部用于管理队列状态的核心数据结构,包含队列的元信息(如读写指针、阻塞列表等),而非实际存储数据的缓冲区。

  • 数据缓冲区的起始位置
    队列的 数据存储区 是另一块独立内存(动态或静态分配),由队列控制块中的 pcHead 指针指向。


7.2 队列控制块(Queue_t)的关键字段

FreeRTOS 中队列控制块的定义(简化版)

typedef struct QueueDefinition {
    int8_t *pcHead;           // 指向数据缓冲区起始地址
    int8_t *pcTail;           // 指向数据缓冲区结束地址
    int8_t *pcWriteTo;        // 下一个写入位置(移动后自动循环)
    int8_t *pcReadFrom;       // 下一个读取位置(移动后自动循环)
    List_t xTasksWaitingToSend;    // 发送阻塞任务列表
    List_t xTasksWaitingToReceive; // 接收阻塞任务列表
    UBaseType_t uxMessagesWaiting; // 当前队列中的消息数
    UBaseType_t uxLength;          // 队列最大长度(uxQueueLength)
    UBaseType_t uxItemSize;        // 单条消息大小(字节)
    // ...其他字段(如队列类型、互斥量等)
} Queue_t;

7.3 队列头部(控制块)与数据缓冲区的关系

 动态创建队列时
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
  • 内存分配
    FreeRTOS 会 同时分配队列控制块和数据缓冲区,并将 pcHead 指向缓冲区起始地址。


7.4 为什么需要区分“队列头部”和“数据缓冲区”?

组件作用是否用户可见
队列头部管理队列状态(如读写指针、阻塞任务),由 FreeRTOS 内核维护。❌ 不直接操作(通过句柄访问)
数据缓冲区实际存储消息内容的内存区域,用户数据通过 API 间接读写。✅ 需指定大小

5. 实际应用中的注意事项

  1. 队列句柄(QueueHandle_t)的本质
    它是指向 队列控制块(头部) 的指针,而非数据缓冲区。

    QueueHandle_t xQueue = xQueueCreate(5, sizeof(int)); // xQueue 指向控制块
  2. 直接操作缓冲区的风险
    用户不应绕过 xQueueSend()/xQueueReceive() 直接访问 pcHead 指向的内存,否则会破坏线程安全。

  3. 调试技巧
    通过 uxQueueMessagesWaiting() 和 uxQueueSpacesAvailable() 监控队列状态,而非直接检查控制块

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值