SMARTWATCH 学习手册有 C 基础 · 嵌入式从零开始
第二章 / FreeRTOS / 第 4 节

任务之间传数据:先把一个整数交清楚

信号量告诉任务“有事要处理”。如果还要把一份数值交给另一个任务,并保留先后顺序,就需要明确数据存在哪里、谁负责保存。

接续:3 · 中断怎样唤醒任务先看问题,再增加对应代码

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 端口核对接口。图解描述执行关系,任务耗时和硬件行为需在实际工程观察。