当前位置: 首页 > news >正文

FreeRTOS队列

目录

  • 1.特性
  • 2.传输数据的两种方法
  • 3.队列的阻塞访问
  • 4.函数
    • 4.1 创建
    • 4.2 复位
    • 4.3 删除
    • 4.4 写操作
      • 4.4.1 写入到尾部
      • 4.4.2 写入到头部
    • 4.5 读操作
    • 4.6 覆盖操作(Overwrite)
    • 4.7 窥视操作(Peek)
    • 4.8 队列状态查询
  • 5.常规使用
  • 6.队列集
    • 6.1 概念
    • 6.2 相关函数
    • 6.3 示例
  • 7.队列的好处
    • 7.1 实现互斥访问
    • 7.2 休眠唤醒_提高CPU利用率
  • 8.队列的内部源码分析
    • 8.1 结构体
    • 8.2 创建函数
    • 8.3 读操作
    • 8.4 写函数
    • 8.5 休眠唤醒
  • 疑问
    • 疑问1
    • 疑问2
    • 疑问3

img

1.特性

队列是一种用于存储多个数据项的数据结构,每个数据项大小固定,队列的容量在创建时就已确定,这个容量就是队列的“长度”。

由于每个数据项的大小固定,因此在创建队列时需要明确指定:

  • 队列的总长度(能存放多少个数据项)
  • 每个数据项的大小

队列的核心操作遵循 先进先出 (FIFO, First In First Out) 的原则,即:

  • 写数据:数据总是插入到队列的尾部。
  • 读数据:数据总是从队列的头部取出。
  • 这种顺序保证了数据按照进入队列的顺序被处理,保持了数据的顺序一致性。
  • 队列内部保存的数据项个数即为当前队列的“长度”。当队列满时,就不能再写入新的数据,除非进行覆盖或者先读取数据腾出空间。

在某些应用场景中,可能需要强制写入数据到队列的头部,这种操作会直接覆盖队列头部已有的数据。

img

2.传输数据的两种方法

使用队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进队列里

    • 数据存储在队列内部,不受发送端变量生命周期的影响。例如,局部变量的值在发送后可以立即重新使用,且不会因为变量被销毁而影响队列中的数据。
    • 队列在创建时就分配了足够的缓冲区,无需任务单独分配或管理内存,减少了内存管理的复杂性。
    • 发送任务与接收任务之间通过队列传递数据后,不需要关心数据的所有权或释放问题,降低了任务之间的耦合度。
    • 在具备内存保护机制的系统中,拷贝方法无需担心双方任务对同一内存地址的访问权限问题,因为数据已经由内核复制到受保护的队列缓冲区中。
    • 但是当数据量较大或传输频繁时,拷贝整个数据块可能会带来较高的CPU消耗,因为每次操作都涉及数据的复制。
  • 引用:把数据、把变量的地址复制进队列里

    • 对于大块数据,仅传递地址而非整个数据块,可以显著降低CPU的拷贝负担和内存消耗。
    • 队列中只存储指针,占用的空间远小于完整数据。
    • 如果发送的是局部变量的地址,一旦函数返回,该局部变量可能会被销毁,导致队列中的地址无效,从而引发访问错误或未定义行为。
    • 对于启用了内存保护的系统,使用引用方式时,必须确保发送和接收任务对数据所在的内存区域都有访问权限,否则会产生权限冲突问题。
    • 发送任务和接收任务之间需要对数据的所有权和生命周期达成一致,接收任务可能需要知道如何正确释放数据或管理数据的生命周期,增加了系统设计的复杂度。

FreeRTOS 默认采用数据拷贝的方法传输数据,其优势如下:

  • 简单易用:发送任务可以将局部变量的值直接传送到队列中,后续即使局部变量被销毁,队列中的数据也不会受到影响。
  • 内核分配缓冲区:队列在创建时内核会分配好缓冲区,无需任务自行管理,降低出错风险。
  • 解耦任务:发送任务与接收任务完全解耦,接收任务无需知道数据的具体来源,也不需负责数据的释放。

3.队列的阻塞访问

只要获得了队列的句柄,无论是任务还是中断服务程序(ISR)都可以对队列进行读和写操作。

注意:ISR通常采用非阻塞方式访问队列,因为中断中不能进入阻塞状态。

多个任务可以同时尝试读或写同一个队列,队列由内核统一管理,这就需要协调各个任务之间的访问顺序和优先级。

读队列时的阻塞:

  • 当一个任务试图从队列读取数据,但此时队列中没有数据可读,就会触发阻塞操作。

    • 任务可以指定一个超时时间。
    • 如果在这个超时时间内,其他任务或者ISR向队列写入了数据,等待该数据的任务就会立即被唤醒并进入就绪状态,准备运行。
    • 如果超时时间过去,队列仍然没有数据,该任务也会退出阻塞状态(通常返回超时错误),此时任务进入就绪态等待再次调度。
  • 当多个任务同时读取一个空队列时,这些任务都会进入阻塞状态等待数据。

    • 优先唤醒优先级最高的任务;
    • 如果等待任务优先级相同,则唤醒等待时间最久的那个任务(即先进先出)。

写队列时的阻塞:

  • 当一个任务试图向队列写入数据,而队列已经满时,该任务同样可以选择阻塞等待,直到队列中有空间可以写入数据。

    • 同样可以设置一个超时时间。
    • 如果在超时时间内有其他任务从队列读取数据,腾出空间,那么等待写入的任务会被唤醒,并进入就绪状态进行写入操作。
    • 如果超时时间到了而队列依然满,该任务则会退出阻塞状态,通常返回写入失败或超时的错误信息。
  • 如果多个任务因队列满而同时阻塞等待写入,系统会按照相同规则进行调度:

    • 首先唤醒优先级最高的任务;
    • 若优先级相同,则唤醒等待时间最长的任务。

通过阻塞机制,任务在无法完成读写操作时不会一直忙等待(轮询),而是进入睡眠状态,不占用CPU资源,直到条件满足或超时后被唤醒。

4.函数

  • 创建队列
    根据是否需要动态分配内存选择 xQueueCreate(动态)或 xQueueCreateStatic(静态),在创建时需要指定队列长度和每个数据项的大小。
  • 复位与删除
    使用 xQueueReset 可以将队列恢复到初始状态;使用 vQueueDelete 释放动态分配的队列内存(静态分配的内存由用户管理)。
  • 写操作
    根据写入位置(队列尾部或头部)及执行环境(任务或 ISR),选择相应的写函数(例如 xQueueSendToBackxQueueSendToFront 及它们的 ISR 版本)。写操作时,可以指定阻塞时间等待空间。
  • 读操作
    使用 xQueueReceivexQueueReceiveFromISR 从队列中读取数据,读取成功后数据会从队列中移除。同样支持阻塞等待数据到达。
  • 覆盖与窥视
    覆盖操作(当队列长度为1时)允许在队列满时直接替换数据;而窥视操作允许查看队列数据而不移除数据。
  • 状态查询
    使用 uxQueueMessagesWaitinguxQueueSpacesAvailable 分别查询队列中当前数据项的数量和剩余可用空间。

4.1 创建

在 FreeRTOS 中,队列可以通过两种方式创建:动态分配内存和静态分配内存。

动态分配内存:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
    • uxQueueLength: 队列长度,表示队列中最多能存放多少个数据项。
    • uxItemSize: 每个数据项的大小(以字节为单位)。
    • 成功时返回一个非 NULL 的句柄,后续所有操作均通过该句柄进行;如果内存不足,则返回 NULL。

静态分配内存:

QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength, UBaseType_t uxItemSize,uint8_t *pucQueueStorageBuffer, StaticQueue_t *pxQueueBuffer );
    • uxQueueLength: 队列长度,即数据项的最大数量。
    • uxItemSize: 每个数据项的大小(字节)。
    • pucQueueStorageBuffer: 指向一个 uint8_t 数组的指针,此数组的大小至少为 uxQueueLength * uxItemSize,用来存储队列中实际的数据。
    • pxQueueBuffer: 指向一个 StaticQueue_t 类型的结构体,此结构体用于保存队列的数据结构。
    • 成功时返回非 NULL 的队列句柄;如果 pxQueueBuffer 为 NULL,则返回 NULL。

4.2 复位

BaseType_t xQueueReset( QueueHandle_t pxQueue );
  • 将队列恢复到初始状态(队列被创建后没有数据),即清除队列中所有数据。
  • 成功时返回 pdPASS(表示复位成功)。

4.3 删除

  • 函数原型:
void vQueueDelete( QueueHandle_t xQueue );
  • 删除队列并释放由动态分配内存创建队列所占用的内存。
  • 注意: 仅能删除使用动态分配内存创建的队列,静态创建的队列内存由用户管理,不会被该函数释放。

4.4 写操作

向队列写入数据时,可以选择写入到队列的尾部或头部(覆盖头部数据),并有适用于任务和 ISR 的不同版本。写操作时,如果队列满了,可以阻塞指定时间等待空间出现。 写只会因为队列满而阻塞,不会因为队列被读访问而阻塞。

4.4.1 写入到尾部

  • 在任务中使用:
BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
// 或者使用更明确的函数名:
BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
  • 在中断服务程序(ISR)中使用:
BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
    • xQueue: 队列句柄,指定要写入哪个队列。
    • pvItemToQueue: 指向待写入数据的指针,函数会将指定大小(在创建队列时确定)的数据复制进队列。
    • xTicksToWait: 如果队列已满,任务会阻塞等待最多 xTicksToWait 个 Tick。设为 0 时,不阻塞;设为 portMAX_DELAY 则一直等待。
    • pxHigherPriorityTaskWoken(仅ISR版本): 指向一个变量的指针,用于在中断中决定是否需要进行上下文切换(如果写入数据后有更高优先级任务被唤醒)。
    • 成功返回 pdPASS;如果写入失败(例如队列满且阻塞时间结束),返回 errQUEUE_FULL。

4.4.2 写入到头部

  • 在任务中使用:
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
  • 在 ISR 中使用:
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
  • **作用:**写入到队列头部时,如果队列满则覆盖头部数据。该方法通常用于需要优先处理最新数据的情况。

4.5 读操作

读取队列中的数据同样提供任务版本和 ISR 版本,读操作成功后会将数据从队列中移除。

  • 在任务中使用:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );
  • 在 ISR 中使用:
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxTaskWoken );
    • xQueue: 队列句柄,指定要从哪个队列读取数据。
    • pvBuffer: 指向缓冲区的指针,函数将队列中数据复制到该缓冲区,复制的数据大小在队列创建时已确定。
    • xTicksToWait: 如果队列为空,任务会阻塞等待最多 xTicksToWait 个 Tick。设为 0 时,函数会立即返回;设为 portMAX_DELAY 则一直等待直到有数据可读。
    • 成功返回 pdPASS;如果队列为空且超时时间结束,则返回 errQUEUE_EMPTY。

4.6 覆盖操作(Overwrite)

当队列长度为 1 时,可以使用覆盖操作将新的数据直接写入队列,覆盖原有数据。覆盖操作不会引起阻塞,因为当队列满时总是直接覆盖。

在任务中使用:

BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void *pvItemToQueue );

在 ISR 中使用:

BaseType_t xQueueOverwriteFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
    • pdTRUE 表示覆盖成功,pdFALSE 表示失败。

4.7 窥视操作(Peek)

当需要查看队列中的数据但不想将其移除时,可以使用窥视操作。
如果队列为空,等待一定时间后才返回;一旦队列中有数据,之后的窥视操作会一直成功,数据不会被删除。

在任务中使用:

BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );

在 ISR 中使用:

BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer );
    • pvBuffer: 用来保存从队列中复制出来的数据,但操作后队列内数据仍然存在。
    • 成功返回 pdTRUE,失败(例如超时或队列为空)返回 pdFALSE。

4.8 队列状态查询

查询队列中已存数据的个数,返回当前队列中存储的数据项个数:

UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );

查询队列剩余的空闲空间,返回队列中剩余的可以存放数据项的个数,即空闲空间:

UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

5.常规使用

// 全局变量和队列句柄
static int sum = 0;                        // 全局变量,用于累加计算结果
static volatile int flagCalcEnd = 0;         // 计算结束标志(用于逻辑分析仪观察)
static volatile int flagUARTused = 0;        // UART使用标志(用于逻辑分析仪观察)
static QueueHandle_t xQueueCalcHandle;       // 用于任务1与任务2同步的队列,传递计算结果
static QueueHandle_t xQueueUARTcHandle;        // 用于实现UART互斥的队列,相当于一个二值信号量// 初始化UART锁
// 利用队列来模拟互斥量,队列长度为1,存放一个int类型的“令牌”
int InitUARTLock(void)
{	int val;  // 令牌变量(值无关紧要,只是占个位置)// 创建队列,队列长度为1,每个数据项的大小为 sizeof(int)xQueueUARTcHandle = xQueueCreate(1, sizeof(int));if (xQueueUARTcHandle == NULL){printf("can not create queue\r\n");return -1;}// 初始状态:发送令牌到队列,表示UART当前空闲可用xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);return 0;
}// 获取UART锁
// 通过从队列中接收令牌来获得对UART的独占访问权
void GetUARTLock(void)
{	int val;  // 接收令牌的变量// 阻塞等待,直到成功从队列中取出令牌(表示已获得锁)xQueueReceive(xQueueUARTcHandle, &val, portMAX_DELAY);
}// 释放UART锁
// 通过将令牌发送回队列,允许其他任务访问UART
void PutUARTLock(void)
{	int val;  // 令牌变量// 将令牌发送回队列(释放锁)xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);
}// 任务1:计算任务(生产者)
// 模拟耗时计算后,将结果通过队列发送给任务2
void Task1Function(void * param)
{volatile int i = 0;while (1){// 模拟耗时计算:循环累加,将全局变量 sum 增加for (i = 0; i < 10000000; i++)sum++;// 计算完成后,将结果发送到 xQueueCalcHandle 队列中,// 队列发送操作如果队列满则会阻塞,直到有空间xQueueSend(xQueueCalcHandle, &sum, portMAX_DELAY);// 重置 sum 为 1,准备下一次计算sum = 1;}
}// 任务2:显示任务(消费者)
// 阻塞等待来自任务1的计算结果,收到后打印显示
void Task2Function(void * param)
{int val;while (1){// 这里设置 flagCalcEnd 只是作为示例标志flagCalcEnd = 0;// 阻塞等待接收队列中的数据(计算结果)xQueueReceive(xQueueCalcHandle, &val, portMAX_DELAY);flagCalcEnd = 1;// 打印从队列中收到的 sum 值printf("sum = %d\r\n", val);}
}// 通用任务函数:用于UART输出
// 任务3和任务4均运行此函数,利用 UART 锁实现互斥访问
void TaskGenericFunction(void * param)
{while (1){// 获取UART锁,确保当前任务独占UART访问GetUARTLock();// 打印任务参数所指定的字符串,说明当前任务正在使用UARTprintf("%s\r\n", (char *)param);// 释放UART锁,让其他任务有机会访问UARTPutUARTLock();// 延时1个Tick,避免频繁占用CPUvTaskDelay(1);}
}/*-----------------------------------------------------------*/// 主函数:程序入口
int main( void )
{TaskHandle_t xHandleTask1;#ifdef DEBUGdebug();
#endif// 硬件初始化prvSetupHardware();printf("Hello, world!\r\n");// 创建用于任务1和任务2之间传递计算结果的队列// 队列长度为2,每个数据项大小为 sizeof(int)xQueueCalcHandle = xQueueCreate(2, sizeof(int));if (xQueueCalcHandle == NULL){printf("can not create queue\r\n");}// 初始化UART锁,创建一个用于互斥的队列InitUARTLock();// 创建任务1:执行计算并发送结果到队列xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);// 创建任务2:接收队列中的计算结果并打印xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);// 创建任务3和任务4:两个任务将竞争UART资源,打印各自的消息// 通过 UART 锁确保同一时刻只有一个任务能进行 UART 输出xTaskCreate(TaskGenericFunction, "Task3", 100, "Task 3 is running", 1, NULL);xTaskCreate(TaskGenericFunction, "Task4", 100, "Task 4 is running", 1, NULL);// 启动调度器,开始任务切换vTaskStartScheduler();// 如果执行到这里,通常表示没有足够的堆内存创建空闲任务return 0;
}

同步部分(任务1和任务2)

  • 任务1 负责进行长时间计算,并通过队列 xQueueCalcHandle 将计算结果发送出去。
  • 任务2 阻塞等待队列中的数据,一旦收到计算结果,就打印出来。
  • 这种设计实现了生产者-消费者模式,任务1和任务2之间通过队列进行数据传递(同步)。

互斥部分(任务3和任务4)

  • 使用 xQueueUARTcHandle 队列来模拟一个二值信号量(只有一个令牌),实现对 UART 的互斥访问。
  • 任务3任务4 均调用 GetUARTLock() 获取 UART 锁,打印信息后调用 PutUARTLock() 释放锁,从而保证同时只有一个任务能够访问UART进行打印。

6.队列集

6.1 概念

队列集是 FreeRTOS 提供的一种机制,可以将多个队列(甚至信号量)组合到一个集合中,然后用一个阻塞调用等待集合中任一成员的事件发生。这样,单个任务就可以同时监控多个队列,当其中任一队列有数据可读时,任务会被唤醒,并返回哪个队列有数据。

写操作与事件计数:

  • 如果某个队列(例如队列 A)连续写入了 N 次数据,那么队列集中会记录 N 个“事件”。每个事件代表队列 A 中有一条待处理的数据。
  • 这意味着同一个队列的写入次数会累加到队列集中,可能导致队列集的事件数大于单个队列的长度。

读取事件的限制:

  • 当任务调用 xQueueSelectFromSet() 时,会返回一个队列(或其他队列集成员)的句柄,该句柄代表该成员队列至少有一个事件。
  • 读取该返回的队列时(调用 xQueueReceive()),只能读取该队列中的一项数据,从而“消费”掉一个事件。
  • 如果没有读取,事件仍然留在队列集中,下次仍会返回相同的队列句柄。

队列集容量的配置:

  • 在创建队列集时,需要指定队列集的长度。这个长度必须等于被管理的所有队列(或其他成员)最大可能存放的事件总数。
  • 例如,如果你要管理队列 A、B、C,那么队列集的长度应至少为“队列 A 长度 + 队列 B 长度 + 队列 C 长度”。
  • 这样确保队列集中可以记录所有成员队列的写入事件,防止事件丢失。

6.2 相关函数

xQueueCreateSet 用于创建一个队列集,指定能容纳的事件数。

xQueueAddToSetxQueueRemoveFromSet 用于管理队列集成员(队列或信号量)。

xQueueSelectFromSet 用于等待队列集中的任一成员发生事件,并返回产生事件的成员句柄,从而使任务能有选择性地处理多个队列或信号量的事件。


QueueSetHandle_t xQueueCreateSet( UBaseType_t uxEventQueueLength );
  • 创建一个队列集,其容量由参数 uxEventQueueLength 指定。这个参数表示队列集中能记录的“事件”总数。

    • 该值必须足够大,以容纳所有添加到队列集中的队列或信号量的所有可能事件。
    • 如果你将 3 个队列添加到队列集,且各自长度分别为 A、B、C,则建议 uxEventQueueLength 至少为 A+B+C。

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet );
  • 将一个队列或信号量(统称为队列集成员)添加到一个已经创建好的队列集中。

    • 每个队列或信号量在任何时刻只能被添加到一个队列集中。
    • 添加成功后,当该成员队列中有数据或信号量发生变化时,队列集会记录一个事件。

BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet );
  • 将先前添加到队列集中的队列或信号量从队列集中移除。

    • 移除后,该队列将不再产生队列集中的事件。
    • 移除操作通常用于动态管理队列集成员或在不再需要队列集通知时进行清理。

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, TickType_t xTicksToWait );
  • 阻塞等待队列集中任一成员发生事件(例如:队列中有数据、信号量被释放)。
  • 使用流程:
    1. 等待事件: 调用该函数后,如果队列集中没有任何成员有事件(没有数据或信号量没有释放),则任务会阻塞,直到等待时间到达或某个成员产生事件。
    2. 返回句柄: 当集合中至少有一个成员有数据时,该函数返回一个对应的成员句柄。
    3. 消费事件: 返回句柄后,需要调用对应的队列或信号量的读取函数(如 xQueueReceive())从该成员中获取数据,这样才能消费掉该事件,否则该事件仍然保留在队列集中。
  • 使用要点:

    • 此函数只能在任务上下文中调用,不能在中断服务例程(ISR)中使用。
    • 返回的成员句柄可能重复(如果连续写入多次),调用者需要在读取后确保数据已被消费。

6.3 示例

建了两个队列(xQueueHandle1 和 xQueueHandle2),并将它们添加到一个队列集中(xQueueSet)。

  • Task1Function 向队列1写入数据;
  • Task2Function 向队列2写入数据;
  • Task3Function 通过阻塞等待队列集中的任一队列有数据,并读取数据后打印出来。
// 全局变量和队列、队列集句柄
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;
static QueueHandle_t xQueueHandle1;      // 队列1句柄
static QueueHandle_t xQueueHandle2;      // 队列2句柄
static QueueSetHandle_t xQueueSet;         // 队列集句柄/*-----------------------------------------------------------* Task1Function:* - 向队列1写入数据,每次写入后 i 自增,并延时 10 个 Tick。* - 写入的数据通过 xQueueSend() 传入队列1,如果队列满则阻塞等待。*/
void Task1Function(void * param)
{int i = 0;while (1){// 向队列1写入当前数值 i,阻塞等待直到发送成功xQueueSend(xQueueHandle1, &i, portMAX_DELAY);i++;                         // 自增,为下一次写入准备数据vTaskDelay(10);              // 延时 10 个 Tick}
}/*-----------------------------------------------------------* Task2Function:* - 向队列2写入数据,每次写入后 i 自减,并延时 20 个 Tick。* - 写入的数据通过 xQueueSend() 传入队列2,同样阻塞等待写入成功。*/
void Task2Function(void * param)
{int i = -1;while (1){// 向队列2写入当前数值 i,阻塞等待直到发送成功xQueueSend(xQueueHandle2, &i, portMAX_DELAY);i--;                         // 自减,为下一次写入准备数据vTaskDelay(20);              // 延时 20 个 Tick}
}/*-----------------------------------------------------------* Task3Function:* - 任务通过调用 xQueueSelectFromSet() 阻塞等待队列集中的任一队列有数据。* - 当返回某个队列的句柄后,再从该队列读取一条数据(只读取一次)。* - 读取到的数据打印输出。*/
void Task3Function(void * param)
{QueueSetMemberHandle_t handle;   // 用于接收返回的队列句柄int i;while (1){/* 1. 从队列集等待事件,直到有任一添加到队列集中的队列有数据 */handle = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);/* 2. 从返回的队列句柄中读取一项数据,读取操作不阻塞(xTicksToWait = 0) */xQueueReceive(handle, &i, 0);/* 3. 打印读取到的数据 */printf("get data : %d\r\n", i);}
}/*-----------------------------------------------------------* main 函数:初始化硬件、创建队列、队列集和任务,并启动调度器*/
int main( void )
{TaskHandle_t xHandleTask1;#ifdef DEBUGdebug();
#endifprvSetupHardware();printf("Hello, world!\r\n");/* 1. 创建两个队列* 创建队列时指定队列长度为 2,每个队列项大小为 sizeof(int)。*/xQueueHandle1 = xQueueCreate(2, sizeof(int));if (xQueueHandle1 == NULL){printf("can not create queue\r\n");}xQueueHandle2 = xQueueCreate(2, sizeof(int));if (xQueueHandle2 == NULL){printf("can not create queue\r\n");}/* 2. 创建队列集* 队列集的长度参数必须足够大,能够容纳所有被添加队列的写入事件。* 假设队列集管理两个队列(队列1和队列2),每个队列长度为 2,* 则理论上队列集的长度应至少为 2 + 2 = 4。* 这里示例中设置为 3,实际使用中需要注意这一点。*/xQueueSet = xQueueCreateSet(3);/* 3. 将两个队列添加到队列集中 */xQueueAddToSet(xQueueHandle1, xQueueSet);xQueueAddToSet(xQueueHandle2, xQueueSet);/* 4. 创建三个任务* Task1Function:不断向队列1写数据。* Task2Function:不断向队列2写数据。* Task3Function:等待队列集中任一队列有数据,并读取数据打印。*/xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);/* 启动调度器 */vTaskStartScheduler();/* 如果调度器启动失败,将执行到这里 */return 0;
}

7.队列的好处

7.1 实现互斥访问

假设有这么一个情况:

img

A和B都要通过队列来向C发送东西,如果我任务A正在执行发送的过程中被高优先级的任务B打断了,B也向队列中写东西,这可能会导致数据的不完整性,就好比如你原本说了了Hello后面还想说"帅哥",结果被另一个人强行插画说了句"沙比",这不就导致语句不完整了,那要是后面你还一直插不上话,不更完。

那么回到上图,这种情况就可以在任务A运行的时候将中断关闭,使得任务B无法抢占,等任务A发送完了再打开中断。别忘了tick中断会引起调度切换,关了中断就没法实现调度切换,也就是任务切换。(其实就是你说了Hello后看到有别人想要插你话,缠住他嘴,你讲完再给他解开让他说)

7.2 休眠唤醒_提高CPU利用率

假设不是队列机制:

int a[MAX];
int flag = 0;void A_func(void *pvParameters )
{int flag = 1;//往数组a写数据,这个数据写入时间需要持续10分钟}
void B_func(void *pvParameters )
{//从a读出数据,并且读取的速度比输入的速度快
}int main(void)
{xTaskCreate(A_func, "A_func", 1000, NULL, 1, NULL);xTaskCreate(B_func, "B_func", 1000, NULL, 1, NULL);/* 启动调度器 */vTaskStartScheduler();/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */return 0;
}

设置的是时间片轮流,由于数组a被读取的速度比被输入的速度快,就会导致任务B总有读到空的情况,也就是白执行了,造成了;还有一种就是当数组a为空,任务A往里面写入一次需要10分的时间,这期间会发生任务切换,A停止写入,任务B执行,读取但啥也没有,也是白执行了。

而如果是队列,就可以实现当队列数据为空时,我任务B可以阻塞等待,不抢占CPU资源,直到你任务A往队列写入数据了,唤醒任务B,我任务B再去读取,这样可以提高效率

这个读队列的函数xQueueReceiveand xQueueReceiveISR在上面也介绍过

8.队列的内部源码分析

8.1 结构体

xQueueCreate创建队列的函数当中,它里面是去创建Queue_t这么一个结构体:

typedef xQUEUE Queue_t;
---->
typedef struct QueueDefinition 
{int8_t * pcHead;           /* 指向队列存储区域的起始位置,即队列的底部。 */int8_t * pcWriteTo;        /* 指向队列存储区域中下一个可写入的位置。 */union{QueuePointers_t xQueue;     /* 当该结构作为队列使用时,存储相关的队列指针数据。 */SemaphoreData_t xSemaphore; /* 当该结构作为信号量使用时,存储相关的信号量数据。 */} u; /* 该联合体用于区分结构体的用途(队列或信号量)。 */List_t xTasksWaitingToSend;    /* 阻塞等待向该队列发送数据的任务列表,以优先级顺序存储。 */List_t xTasksWaitingToReceive; /* 阻塞等待从该队列接收数据的任务列表,以优先级顺序存储。 */volatile UBaseType_t uxMessagesWaiting; /* 当前队列中存储的消息数量。 */UBaseType_t uxLength;                    /* 队列的最大容量(单位:项目数,而非字节数)。 */UBaseType_t uxItemSize;                  /* 队列中每个元素的大小(单位:字节)。 */volatile int8_t cRxLock; /* 记录在队列被锁定时从队列接收(移除)的项目数量。 如果队列未被锁定,则该值设为 queueUNLOCKED。*/volatile int8_t cTxLock; /* 记录在队列被锁定时向队列发送(添加)的项目数量。如果队列未被锁定,则该值设为 queueUNLOCKED。*/#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )uint8_t ucStaticallyAllocated; /* 指示队列的内存是否为静态分配。若为 pdTRUE,则内存为静态分配,防止错误释放。*/#endif#if ( configUSE_QUEUE_SETS == 1 )struct QueueDefinition * pxQueueSetContainer; /* 指向包含该队列的 Queue Set,仅当启用了 Queue Sets 功能时使用。*/#endif#if ( configUSE_TRACE_FACILITY == 1 )UBaseType_t uxQueueNumber; /* 队列编号,用于跟踪和调试。 */uint8_t ucQueueType;       /* 队列类型(用于调试或跟踪目的)。 */#endif
} xQUEUE; /* 队列(Queue)或信号量(Semaphore)的核心数据结构。 */

其中涉及的队列的成员,QueuePointers_t结构体展开如下:

typedef struct QueuePointers
{int8_t * pcTail;     /* 指向队列存储区域的末尾字节。通常多分配一个额外的字节作为标记,以便在环形队列结构中正确检测队列状态。 */int8_t * pcReadFrom; /* 指向上一次从队列读取数据的位置。在结构作为队列使用时,该指针用于跟踪最新读取的元素位置。 */
} QueuePointers_t;

那么问题来了,这个队列结构体Queue_tint8_t * pcHead指向的实际上用于传输数据的队列,这两者的分开存储的吗?还是说结构体后面就紧接队列空间?其实是后者,看队列创建函数的内部实现就可以知道了。

8.2 创建函数

#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )#define xQueueCreate( uxQueueLength, uxItemSize )    xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endifQueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,const uint8_t ucQueueType )
{Queue_t * pxNewQueue = NULL;  /* 指向新创建的队列结构体 */size_t xQueueSizeInBytes;     /* 计算队列存储区所需的字节数 */uint8_t * pucQueueStorage;    /* 指向队列存储区的指针 *//* 进行基本参数检查,避免溢出问题 */if( ( uxQueueLength > ( UBaseType_t ) 0 ) &&/* 防止乘法溢出:检查 uxQueueLength * uxItemSize 是否超出 SIZE_MAX */( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) &&/* 防止加法溢出:检查 (sizeof(Queue_t) + uxQueueLength * uxItemSize) 是否超出 SIZE_MAX */( ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) ){/* 计算队列所需的存储空间,uxItemSize 为 0 时表示队列作为信号量使用 */xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );/* 分配队列结构体和存储空间 *///⚡⚡⚡⚡ 这里就可以看出来结构体和队列存储空间实际上是挨在一块的 pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );if( pxNewQueue != NULL ){/* 定位队列存储区地址,在 Queue_t 结构体之后 */pucQueueStorage = ( uint8_t * ) pxNewQueue;pucQueueStorage += sizeof( Queue_t );#if ( configSUPPORT_STATIC_ALLOCATION == 1 ){/* 记录该队列是动态分配的(用于后续释放判断) */pxNewQueue->ucStaticallyAllocated = pdFALSE;}#endif /* configSUPPORT_STATIC_ALLOCATION *//* 初始化队列结构 */prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );}else{/* 队列创建失败,触发相应的调试跟踪事件 */traceQUEUE_CREATE_FAILED( ucQueueType );mtCOVERAGE_TEST_MARKER();}}else{/* 断言检查,确保 pxNewQueue 不为空 */configASSERT( pxNewQueue );mtCOVERAGE_TEST_MARKER();}return pxNewQueue; /* 返回新创建的队列句柄(成功)或 NULL(失败) */
}

注释很详细了,这个函数主要讲的其实是对于内存空间的开辟,具体的结构体成员初始化主要在于其内部调用的prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );函数:

  • 参1:存储队列的长度
  • 参2:队列中每一部分空间的大小(理解成数组那样就好了)
  • 参3:指向队列的存储空间,而不是指向队列结构体Queue_t的存储空间
  • 参4:宏定义queueQUEUE_TYPE_BASE,队列的类型,这里指的是最基础的类型
  • 参5:指向队列结构体Queue_t

img

继续进入prvInitialiseNewQueue查看:

static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,uint8_t * pucQueueStorage,const uint8_t ucQueueType,Queue_t * pxNewQueue )
{/* 避免编译器对未使用参数的警告 (当 configUSE_TRACE_FACILITY 未启用时) */( void ) ucQueueType;if( uxItemSize == ( UBaseType_t ) 0 ){/* 队列存储区域未分配 RAM,但 pcHead 不能为 NULL,* 因为 NULL 作为互斥量的标志。因此,将 pcHead* 指向队列结构体自身,保证它指向有效内存。 */pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;}else{/* 将队列头部指向实际的存储区域。 */pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;}/* 初始化队列成员变量 */pxNewQueue->uxLength = uxQueueLength;  /* 设置队列长度 */pxNewQueue->uxItemSize = uxItemSize;  /* 设置每个元素的大小 */( void ) xQueueGenericReset( pxNewQueue, pdTRUE );  /* 重置队列 */#if ( configUSE_TRACE_FACILITY == 1 ){/* 记录队列类型(用于调试和跟踪) */pxNewQueue->ucQueueType = ucQueueType;}#endif /* configUSE_TRACE_FACILITY */#if ( configUSE_QUEUE_SETS == 1 ){/* 初始化队列集合容器指针 */pxNewQueue->pxQueueSetContainer = NULL;}#endif /* configUSE_QUEUE_SETS *//* 记录队列创建事件(用于调试和跟踪) */traceQUEUE_CREATE( pxNewQueue );
}

8.3 读操作

之前提到过,如果有多个读或是多个写,都会造成数据顺序错乱,这时候就得靠关中断来实现,在队列的读操作函数xQueueReceive中,就有所体现:

BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait )
{BaseType_t xEntryTimeSet = pdFALSE; /* 是否已设置进入时间 */TimeOut_t xTimeOut; /* 用于超时管理的结构体 */Queue_t * const pxQueue = xQueue; /* 队列句柄 *//* 断言:检查队列指针是否为空 */configASSERT( ( pxQueue ) );/* 断言:如果队列项大小不为 0,则接收缓冲区指针不能为 NULL */configASSERT( !( ( ( pvBuffer ) == NULL ) && ( ( pxQueue )->uxItemSize != ( UBaseType_t ) 0U ) ) );/* 断言:如果调度器被挂起,调用线程不能阻塞 */#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ){configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );}#endiffor( ; ; ){taskENTER_CRITICAL(); /* ⚡⚡⚡⚡进入临界区,防止并发访问 */{const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting; /* 当前队列中的消息数 *//* 如果队列中有数据,则读取 */if( uxMessagesWaiting > ( UBaseType_t ) 0 ){/* 从队列中复制数据到缓冲区 */prvCopyDataFromQueue( pxQueue, pvBuffer );traceQUEUE_RECEIVE( pxQueue ); /* 记录接收操作的跟踪信息 */pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1; /* 递减消息计数 *//* 处理等待发送的任务 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ){if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION(); /* 如果有任务被解除阻塞,则进行任务切换 */}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}taskEXIT_CRITICAL(); /* 退出临界区 */return pdPASS; /* 接收成功 */}else{if( xTicksToWait == ( TickType_t ) 0 ) /* 如果不等待,则直接返回 */{taskEXIT_CRITICAL();traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else if( xEntryTimeSet == pdFALSE ){/* 第一次进入等待状态,初始化超时结构体 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL(); /* ⚡⚡⚡退出临界区 *//* 任务挂起,等待数据可用 */vTaskSuspendAll();prvLockQueue( pxQueue );/* 检查是否超时 */if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) /* 如果队列仍为空,则将任务放入等待队列 */{traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );prvUnlockQueue( pxQueue );if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API(); /* 任务切换 */}else{mtCOVERAGE_TEST_MARKER();}}else{/* 队列中已有数据,重新尝试读取 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{/* 超时处理 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else{mtCOVERAGE_TEST_MARKER();}}}
}

可以看到,使用的就是taskENTER_CRITICAL();, 通过操作 ARM Cortex-M 的 BASEPRI 寄存器,关闭所有优先级 低于或等于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断,而更高优先级的中断(如系统关键中断)仍可响应。

注释中给的是进入临界区,其实意思都是一样的。临界区指的是访问共享资源(如全局变量、硬件寄存器)的代码段,要求执行过程中不可被中断或任务切换打断,否则可能导致数据不一致或逻辑错误。例如,对队列的读写操作需要“写入数据+更新指针”的原子性,若中途被中断打断可能引发覆盖或丢失数据的问题。因此进入临界区,实际上就有进行关闭部分中断的操作


从上面代码块抽取出关键部分如下:

        taskENTER_CRITICAL(); /* ⚡⚡⚡⚡进入临界区,防止并发访问 */{const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting; /* 当前队列中的消息数 *//* 如果队列中有数据,则读取 */if( uxMessagesWaiting > ( UBaseType_t ) 0 ){/* 从队列中复制数据到缓冲区 */prvCopyDataFromQueue( pxQueue, pvBuffer );traceQUEUE_RECEIVE( pxQueue ); /* 记录接收操作的跟踪信息 */pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1; /* 递减消息计数 *//* 处理等待发送的任务 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ){if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION(); /* 如果有任务被解除阻塞,则进行任务切换 */}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}taskEXIT_CRITICAL(); /* 退出临界区 */return pdPASS; /* 接收成功 */}else{if( xTicksToWait == ( TickType_t ) 0 ) /* 如果不等待,则直接返回 */{taskEXIT_CRITICAL();traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else if( xEntryTimeSet == pdFALSE ){/* 第一次进入等待状态,初始化超时结构体 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL(); /* ⚡⚡⚡退出临界区 */

写的时候如果队列满了,可以阻塞指定时间等待空间出现。 写只会因为队列满而阻塞,不会因为队列被读访问而阻塞。

内部调用了prvCopyDataFromQueue将数据从队列取到buff,那我队列不就有空间了,放在Queue_t结构体当中xTasksWaitingToSend的等待发送数据的任务的记录会被删除,同时会从任务等待链表pxDelayedTaskList中将 调用写队列函数因为队列满而阻塞休眠的任务 取出放到就绪任务链表pxReadyTasksLists,然后读操作函数xQueueReceive接下来会调用queueYIELD_IF_USING_PREEMPTION,主动引起任务切换(注意不是tick中断引起的任务切换)使写任务唤醒,如果写任务和读任务优先级相同,设置的是时间片轮流,那么宏观上来看其实就可以当作是边读边写了。

img

img


接下来再看xQueueReceive的下半部分内容:

         if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) /* 如果队列仍为空,则将任务放入等待队列 */{traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );prvUnlockQueue( pxQueue );if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API(); /* 任务切换 */}else{mtCOVERAGE_TEST_MARKER();}}else{/* 队列中已有数据,重新尝试读取 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{if( xTicksToWait == ( TickType_t ) 0 ) /* 如果不等待,则直接返回 */{taskEXIT_CRITICAL();traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else if( xEntryTimeSet == pdFALSE ){/* 第一次进入等待状态,初始化超时结构体 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}}

很简单,其实就如果我读到队列为空了(前提是没超时,超时了就执行else部分,进入等待),就将当前的读队列的任务记录在Queue_t结构体当中的读等待链表vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait ),并指定其等待的时间,同时将调用该函数

当然这个操作在之前讲的任务章节当中,其实就将当前读任务从就绪链表移到等待链表:pxReadyTasksLists --> pxDelayedTaskList,这一点具体就在vTaskPlaceOnEventList函数当中体现,它不仅仅将任务 记录到Queue_t结构体中自身的等待接收链表xTasksWaitingToReceive,还将当前所 调用该读队列函数的任务 移入到 任务等待链表pxDelayedTaskList

img

8.4 写函数

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )BaseType_t xQueueGenericSend( QueueHandle_t xQueue,const void * const pvItemToQueue,TickType_t xTicksToWait,const BaseType_t xCopyPosition )
{BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired; /* xEntryTimeSet 标记是否已设置超时初始时间,xYieldRequired 表示是否需要任务切换 */TimeOut_t xTimeOut;                                  /* 用于管理超时的结构体 */Queue_t * const pxQueue = xQueue;                    /* 将队列句柄转换为内部队列结构指针 *//* 断言检查:确保队列指针有效 */configASSERT( pxQueue );/* 断言检查:如果队列项大小不为零,则发送的数据指针不能为 NULL */configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );/* 断言检查:如果使用覆盖发送(queueOVERWRITE),队列长度必须为 1 */configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ){/* 断言:如果调度器处于挂起状态,则不能指定等待时间 */configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );}#endif/* 循环尝试将数据发送到队列,直到成功或超时退出 */for( ; ; ){taskENTER_CRITICAL(); /* 进入临界区,防止同时访问队列数据结构 */{/* 检查队列是否有空位:* 如果队列中消息数小于队列长度,或者是覆盖模式(允许覆盖现有数据) */if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ){traceQUEUE_SEND( pxQueue ); /* 跟踪发送事件 */#if ( configUSE_QUEUE_SETS == 1 ){const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting;/* 将数据复制到队列中,返回是否需要触发任务切换 */xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 如果队列属于队列集合 */if( pxQueue->pxQueueSetContainer != NULL ){if( ( xCopyPosition == queueOVERWRITE ) && ( uxPreviousMessagesWaiting != ( UBaseType_t ) 0 ) ){/* 对于覆盖模式,如果队列中已经有数据,则不通知队列集合,* 因为队列中的消息数未发生变化 */mtCOVERAGE_TEST_MARKER();}else if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE ){/* 通知队列集合后,可能有优先级更高的任务需要唤醒,* 因此进行任务切换 */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{/* 如果没有队列集合,检查是否有任务在等待接收数据 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){/* 移除等待接收任务列表中的一个任务,并唤醒它 */if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){/* 如果唤醒的任务优先级更高,则触发上下文切换 */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}/* 特殊情况:如果 xYieldRequired 非 pdFALSE,* 可能意味着任务持有多个互斥量,需要立即进行任务切换 */else if( xYieldRequired != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}}#else /* 如果没有启用队列集合 */{/* 将数据复制到队列中 */xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 检查是否有任务在等待接收数据 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){/* 唤醒等待接收任务 */if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else if( xYieldRequired != pdFALSE ){/* 特殊情况:如果需要任务切换,则立即执行 */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_QUEUE_SETS */taskEXIT_CRITICAL(); /* 退出临界区 */return pdPASS;       /* 成功将数据发送到队列,返回 pdPASS */}else{/* 队列已满的情况 */if( xTicksToWait == ( TickType_t ) 0 ){/* 如果不允许阻塞等待,则直接退出并返回错误 */taskEXIT_CRITICAL();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL;}else if( xEntryTimeSet == pdFALSE ){/* 如果是第一次检测到队列已满,并且允许阻塞等待,* 则初始化超时结构体,记录当前时间 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{/* 超时初始时间已设置,无需重复设置 */mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL(); /* 退出临界区 *//* 临界区外允许其他任务和中断操作队列 */vTaskSuspendAll();   /* 暂停任务调度,防止在等待期间调度器切换 */prvLockQueue( pxQueue ); /* 锁定队列,确保后续操作的原子性 *//* 更新超时状态,检查是否已经超时 */if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){/* 如果队列依然满,则将当前任务加入等待发送列表中 */if( prvIsQueueFull( pxQueue ) != pdFALSE ){traceBLOCKING_ON_QUEUE_SEND( pxQueue );vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );/* 解锁队列,使得队列事件可以影响等待列表 */prvUnlockQueue( pxQueue );/* 恢复任务调度,并检查是否需要进行上下文切换 */if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API();}}else{/* 如果队列已不满,则重新解锁并尝试发送数据 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{/* 超时:如果等待超时且队列仍然满,则退出并返回错误 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL;}} /* 循环结束,最终会成功发送或者返回超时/队列满的错误 */
}

写队列就不缀述了,其实和读队列几乎是差不多的。

8.5 休眠唤醒

在读函数的分析中,提到:读到队列为空了(前提是没超时,超时了就执行else部分,进入等待),就将当前的读队列的任务记录到Queue_t结构体当中的读等待链表vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait ),同时将读任务从就绪任务链表移动到等待任务链表,指定其等待的时间。(其pxReadyTasksLists --> pxDelayedTaskList)

那如果超时了呢?超时的话又是谁将该阻塞等待读队列的任务把它从延迟等待队列中取出来,放到就绪任务链表中 ---- tick中断,来看看tick的中断函数,在STM32F10x.c中可以找到tick中断的处理函数xPortSysTickHandler

 DCD     xPortSysTickHandler       ; SysTick Handler/*
DCD xPortSysTickHandler 表示:在中断向量表的 SysTick 条目 处,写入 xPortSysTickHandler 函数的地址。当 SysTick 中断发生时,处理器会自动跳转到 xPortSysTickHandler 执行 FreeRTOS 的系统节拍处理逻辑。注释 ; SysTick Handler 仅用于说明该条目对应 SysTick 中断。*/

来看看其内部实现:

/* FreeRTOS 的 SysTick 中断服务函数 */
void xPortSysTickHandler( void )
{/*-----------------------------------------------------------* 提升中断屏蔽级别(关闭低优先级中断)*  SysTick 被配置为最低中断优先级,因此进入此中断时所有低优先级中断已被屏蔽*  此处无需保存/恢复中断状态,直接使用更快的 vPortRaiseBASEPRI() 而非 portSET_INTERRUPT_MASK_FROM_ISR()*  操作 ARM Cortex-M 的 BASEPRI 寄存器,屏蔽优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断*-----------------------------------------------------------*/vPortRaiseBASEPRI();  // 等效于 basepri = configMAX_SYSCALL_INTERRUPT_PRIORITY/* 进入临界区代码段(大括号仅用于代码结构划分,无实际作用) */{/*-----------------------------------------------------------*  xTaskIncrementTick() 执行以下功能:*   1. 递增系统节拍计数器 xTickCount*   2. 检查延时任务是否超时,将超时任务移入就绪队列*   3. 若启用了时间片轮转,标记同优先级任务切换需求*  返回值 pdTRUE 表示需要触发上下文切换*  ⚡⚡⚡⚡*-----------------------------------------------------------*/if( xTaskIncrementTick() != pdFALSE ){/*-----------------------------------------------------------* 请求 PendSV 中断触发上下文切换*  写入 portNVIC_PENDSVSET_BIT 到 ICSR 寄存器(地址 portNVIC_INT_CTRL_REG)*  PendSV 是 Cortex-M 的延迟上下文切换中断,优先级通常设为最低*  延迟切换到 PendSV 处理可减少 SysTick 中断占用时间,提升实时性*-----------------------------------------------------------*/portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;  // 示例值:0xE000ED04 |= (1 << 28)}}/*-----------------------------------------------------------* 恢复中断屏蔽级别(允许低优先级中断)*  清除 BASEPRI 寄存器,恢复进入中断前的中断屏蔽状态*  等效于 basepri = 0,允许所有优先级中断*-----------------------------------------------------------*/vPortClearBASEPRIFromISR();
}/* 注:* 1. SysTick 中断优先级必须在 FreeRTOS 配置中设为最低优先级,确保不会阻塞高优先级中断* 2. PendSV 中断处理函数中实际执行 vTaskSwitchContext() 完成上下文切换* 3. 此函数运行在中断上下文,需严格遵守 ISR 编程规范(短耗时、无阻塞等)*/

其中就调用了xTaskIncrementTick,注释也已经说了:检查延时任务是否超时,将超时任务移入就绪队列。

继续进入这个函数看:

BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB;              /* 指向延时任务的任务控制块 (TCB),用于后续解除阻塞操作 */TickType_t xItemValue;      /* 用于保存延时任务的解阻时间(任务等待时间) */BaseType_t xSwitchRequired = pdFALSE; /* 标志位,表示是否需要进行上下文切换 *//* 每次 tick 中断发生时,由可移植层调用该函数,* 此处调用跟踪宏记录当前 tick 值(调试与性能追踪) */traceTASK_INCREMENT_TICK( xTickCount );/* 如果调度器没有被挂起,则正常处理 tick */if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){/* 小优化:在本临界区内 tick 值不会改变,所以将其拷贝到一个局部常量中 */const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;/* 更新系统 tick 计数 */xTickCount = xConstTickCount;/* 检查是否发生 tick 溢出(即 tick 计数从最大值回绕到 0) */if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 这里检查溢出情况 */{/* 当 tick 溢出时,切换延时列表和溢出延时列表 */taskSWITCH_DELAYED_LISTS();}else{mtCOVERAGE_TEST_MARKER();}/* 检查是否有任务的延时等待时间已到期。* xNextTaskUnblockTime 存储着延时队列中最早需要解除阻塞的时间 */if( xConstTickCount >= xNextTaskUnblockTime ){/* 遍历延时任务列表,直到遇到未到解除阻塞时间的任务 */for( ; ; ){/* 如果延时任务列表为空 */if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ){/* 将 xNextTaskUnblockTime 设置为一个极大值,防止下次不必要的检查 */xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 */break; /* 没有任务等待解除阻塞,退出循环 */}else{/* 延时任务列表非空,从列表头获取最早的任务 */pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 *//* 取出该任务的解除阻塞时间 */xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );/* 如果当前 tick 值还未达到该任务的解除阻塞时间 */if( xConstTickCount < xItemValue ){/* 更新 xNextTaskUnblockTime 为该任务的解除阻塞时间,并退出循环,* 因为后面的任务的解除时间肯定更晚 */xNextTaskUnblockTime = xItemValue;break; /* 使用 break 退出循环 */}else{mtCOVERAGE_TEST_MARKER();}/* 到达解除阻塞时间,将该任务从延时列表中移除 */listREMOVE_ITEM( &( pxTCB->xStateListItem ) );/* 检查该任务是否同时在等待某个事件,如果是,则也将其从事件列表中移除 */if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ){listREMOVE_ITEM( &( pxTCB->xEventListItem ) );}else{mtCOVERAGE_TEST_MARKER();}/* 将解除阻塞的任务放入对应的就绪任务列表中,使其可以被调度运行 */prvAddTaskToReadyList( pxTCB );/* 如果启用了抢占调度,则检查解除阻塞任务的优先级* 如果该任务的优先级高于或等于当前任务,则标记需要上下文切换 */#if ( configUSE_PREEMPTION == 1 ){if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){xSwitchRequired = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_PREEMPTION */}}}/* 如果启用了时间片调度,当就绪列表中当前优先级任务数量大于 1 时,* 则允许这些同优先级任务共享 CPU 时间,可能需要上下文切换 */#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ){if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ){xSwitchRequired = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* ( configUSE_PREEMPTION == 1 && configUSE_TIME_SLICING == 1 ) *//* 如果定义了 tick hook,调用用户自定义的钩子函数,* 但要确保在处理挂起 tick 计数时不调用 */#if ( configUSE_TICK_HOOK == 1 ){if( xPendedTicks == ( TickType_t ) 0 ){vApplicationTickHook();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_TICK_HOOK *//* 如果启用了抢占调度,并且有挂起的任务切换请求,则设置上下文切换标志 */#if ( configUSE_PREEMPTION == 1 ){if( xYieldPending != pdFALSE ){xSwitchRequired = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_PREEMPTION */}else{/* 如果调度器被挂起,则仅增加挂起的 tick 数,* 后续在调度器恢复时处理这些挂起的 tick */++xPendedTicks;/* 即使调度器挂起,也会周期性调用 tick hook */#if ( configUSE_TICK_HOOK == 1 ){vApplicationTickHook();}#endif}/* 返回是否需要进行上下文切换的标志 */return xSwitchRequired;
}
/*-----------------------------------------------------------*/

注释很详细,这里就不缀诉了。

疑问

疑问1

使用全局变量也可以在任务之间实现同步,为什么要加volatile?

  • 任务A写变量,任务B读变量,不加volatile的话,任务B读到的值可能不是任务A写的值
  • 在while循环中判断一个变量时,不加volatile的话,有可能把变量读进来后就循环判断,不会多次读变量
  • volatle的意思是"易变",就是告诉编译器:别轻易来优化我,老老实实去读写内存上的变量

疑问2

除了通过vTaskDelay让出CPU资源,还有没有更合理的函数?

  • 使用taskYIELD(),主动发起一次任务切换
  • vTaskDelay会让任务阻塞、暂停若干tick,taskYIELD()更合理
  • 可以设置不同的优先级来实现抢占

疑问3

假设队列A、B、C使用队列集,有哪些要注意的地方?

  • 写队列A N次,会导致写队列集N次,也就是队列集里有N个队列A的handle
  • 读一次队列集返回一个队列后,只能读这个队列一次
  • 创建队列集时,它要管理队列ABC,那么队列集的长度=队列A长度+队列B长度+队列C长度

  1. 事件数量累计:
    • 如果对队列 A 进行了 N 次写入,那么队列集将记录 N 个事件(队列 A 的句柄会在队列集中出现 N 次)。
    • 这可能导致队列集的事件数超过单个队列的长度,因此在创建队列集时,其容量必须足够大,通常等于所有被管理队列的长度之和(例如:队列 A 长度 + 队列 B 长度 + 队列 C 长度)。
  1. 读取事件后需从对应队列读取数据:
    • xQueueSelectFromSet() 返回某个队列的句柄后,只能从该队列读取一次数据来消费掉一个事件。
    • 如果不读取,那么该事件仍然保留在队列集中,下次 xQueueSelectFromSet() 可能仍会返回同一队列的句柄。
  1. 队列集容量配置:
    • 创建队列集时传入的容量参数,必须能够涵盖所有添加到队列集的队列的所有可能事件。
    • 例如,若队列 A 长度为 2,队列 B 长度为 2,队列 C 长度为 2,则队列集的容量至少要设置为 6,以确保所有写入操作都能在队列集中得到反映。

相关文章:

  • c语言——字符函数
  • 查询宝塔的数据库信息
  • 多轮对话实现
  • 【大模型】RankRAG:基于大模型的上下文排序与检索增强生成的统一框架
  • 格恩朗超声波水表 绿色助农 精准领航​
  • 02.运算符
  • [ linux-系统 ] 进程控制
  • helm使用说明和实例
  • 赞助打赏单页HTML源码(源码下载)
  • 基于算法竞赛的c++编程(24)位运算及其应用
  • python3基础语法梳理(一)
  • 安全领域新突破:可视化让隐患无处遁形
  • Easy Excel
  • c语言(持续更新)
  • 使用DataX同步MySQL数据
  • OSPF域内路由
  • matlab时序预测并绘制预测值和真实值对比曲线
  • 6.9本日总结
  • DPC密度峰值聚类
  • PostgreSQL 与 SQL 基础:为 Fast API 打下数据基础
  • 手机端网站建设的注意事项/百度风云榜游戏
  • 做网站需要每年都交钱吗/怎么做网页设计的页面
  • 怎么做游戏自动充值的网站/核心关键词如何优化
  • 网站开发增值税/网络推广方案怎么写
  • 网站的主要内容/湖北疫情最新消息
  • 网站排名突然掉没了/注册网址