任务之间传数据:先把一个整数交清楚
信号量告诉任务“有事要处理”。如果还要把一份数值交给另一个任务,并保留先后顺序,就需要明确数据存在哪里、谁负责保存。
1 · 一个共享变量,为什么不够?
设想采集任务依次得到 10、20、30,每次都写入同一个 latest_value。处理任务忙了一会儿,再来读取,只能看到 30。即使每次都另发通知,10 和 20 也没有被这个变量保存下来。
如果应用只需要最新状态,这种覆盖有时是有意的。但若希望依次处理每个结果,就要给尚未处理的数据安排位置。前面 FIFO 篇已经学过先进先出队列和循环存储;FreeRTOS 队列额外提供了任务间安全收发,以及为空、为满时的等待管理。
2 · 先创建一个只装整数的队列
文件级声明
#include "queue.h"
static QueueHandle_t numbers;
static volatile uint32_t dropped;
static volatile uint32_t last_received;main() 中 · 创建两个使用者任务之前
numbers = xQueueCreate(4, sizeof(uint32_t));
if (numbers == NULL) Error_Handler();第一个参数表示最多保存 4 个元素,第二个参数表示每个元素占多少字节。这里每个元素是一份 uint32_t 整数。创建队列时,内核分配管理信息及这 4 份数据所需的存储。
sizeof 在此给出整数的存储字节数。不要把队列长度理解成字节数,也不要把元素大小误填成数组指针的大小。
3 · 发送时,到底复制了什么?
生产数据的任务 · dropped 只有本任务写入
static void ProducerTask(void *argument)
{
(void)argument;
uint32_t value = 0;
for (;;) {
value++;
if (xQueueSend(numbers, &value, 0) != pdPASS) {
dropped++;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}xQueueSend(numbers, &value, 0) 把 value 所在位置的一个元素复制进队列。参数虽然传地址,但由于队列创建时元素大小是整数大小,复制进去的是那个整数的字节。函数成功返回后,本轮 value 再改变,也不影响队列中的旧副本。
最后一个参数 0 表示队列满时不等待。本例丢弃本次新数据,并累加 dropped,便于观察拥堵。它是一项明确的实验策略,并不适合所有业务。需要保留数据时可以等待一段时间,但等待期间上游采样仍可能继续,要检查上游缓冲容量。
4 · 接收不到数据时,为什么不用继续循环检查?
消费数据的任务 · last_received 用于调试观察
static void ConsumerTask(void *argument)
{
(void)argument;
uint32_t received;
for (;;) {
if (xQueueReceive(numbers, &received, portMAX_DELAY) == pdPASS) {
last_received = received;
}
}
}xQueueReceive() 把最早排队的整数复制到 received,同时从队列取走这一项。没有数据时阻塞等待。当前项目启用了相应的任务挂起支持,因此此处的 portMAX_DELAY 可表示不设有限等待超时。它不是另开一个轮询循环。
依照任务创建篇的方法,分别以优先级 1 创建这两个入口,检查每次返回值,再启动调度器。实验初始时,接收者来得及取走,dropped 应不增加。随后只在接收成功分支末尾加 500 毫秒的任务延时,接收者变慢,观察有限队列怎样填满以及丢弃计数怎样增加。
5 · 需要一并交付时间,再加入结构体
现在希望数值和产生时间一起被接收。如果分成两个独立共享变量,处理任务可能读到新数值与旧时间。它们本来就描述同一份结果,可以合成一个队列元素:
元素扩展示意 · 收发两端和创建大小必须同时改为 Measurement
typedef struct {
uint32_t time_ms;
uint32_t value;
} Measurement;
/* 初始化改为 xQueueCreate(4, sizeof(Measurement))。 */
Measurement item = { .time_ms = HAL_GetTick(), .value = 25 };
xQueueSend(numbers, &item, 0);结构体在这里解决的是“把相关字段作为一份数据交付”,不需要为每个传感器再建立设备类、状态枚举和通用框架。换成真实测量时,可在原发送位置调用已经学过的驱动,先确认读数有效,再构造这一份结果。
6 · 队列装指针,又有什么不同?
如果元素大小改成 sizeof(uint16_t *),队列只复制地址。接收者拿到地址后读取的是原数组,而原数组可能已被 DMA 或发送者覆盖。把一个大数组指针排队,并不能替代缓冲区管理。
尤其不能把将要结束作用域的局部数组交给后续任务。要传指针,必须先约定谁拥有这块内存、何时可以修改、谁通知归还。声音篇会从实际缓冲解释这种关系,本节先采用小数据按值复制。
需要知道有工作待处理,用事件通知机制;需要保存并依次交付几份数据,用队列。当前手表的传感器和界面由同一任务顺序推进,不必为了演示队列就把它们拆开。
资料与源码
本章按仓库当前 FreeRTOS V11.2.0 与 STM32U5 端口核对接口。图解描述执行关系,任务耗时和硬件行为需在实际工程观察。