第4章 队列通信:队列创建与发送、队列接收与超时、队列在运动指令传递中的应用、多任务数据同步

队列这东西,说白了就是任务之间的“快递通道”。

我在做运动控制项目时,最头疼的就是多个任务怎么安全地传数据。全局变量?加锁?嗯,用是能用,但坑太多。后来我彻底转向了FreeRTOS的队列机制,这才算真正解放了双手。

4.1 队列的创建与发送

队列的本质是什么?是一个先进先出的缓冲区。任务A往里扔数据,任务B从另一头取数据。就这么简单。

但创建队列时,有几个参数你得搞明白:

  • 队列长度:能存多少个数据项
  • 数据项大小:每个数据占多少字节
  • 队列句柄:创建成功后返回的“身份证”

我个人习惯用 xQueueCreate() 这个API。举个例子:

// 创建一个能存放10个int32_t数据的队列
QueueHandle_t xMotorCmdQueue;
xMotorCmdQueue = xQueueCreate(10, sizeof(int32_t));

if (xMotorCmdQueue == NULL) {
    // 创建失败,内存不够了
    printf("队列创建失败!\n");
}

发送数据时,用 xQueueSend() 或者 xQueueSendFromISR()。前者用在任务中,后者用在中断服务函数里。

重要提醒:队列发送是“拷贝”数据,不是传指针。如果你传的是结构体,确保结构体大小别太大,否则拷贝开销会让你心疼。

我曾经在一个项目中,传了一个512字节的结构体,结果队列操作慢得像蜗牛。后来改成传指针,性能直接翻倍。

4.2 队列接收与超时机制

接收数据用 xQueueReceive()。这个函数有个很实用的参数——超时时间。

你想想看,如果队列是空的,任务要不要等?等多久?

  • portMAX_DELAY:死等,直到有数据来
  • 0:不等待,没数据就立即返回
  • pdMS_TO_TICKS(100):等100毫秒,超时返回

代码示例:

int32_t receivedCmd;
BaseType_t status;

// 等待100ms,看有没有新指令
status = xQueueReceive(xMotorCmdQueue, &receivedCmd, pdMS_TO_TICKS(100));

if (status == pdPASS) {
    // 成功收到数据
    processMotorCommand(receivedCmd);
} else {
    // 超时了,做点别的
    checkMotorStatus();
}

我的经验:运动控制中,超时时间别设太长。我一般设10-50ms,这样既能及时响应,又不会让CPU空转。

为什么会这样?因为运动控制讲究实时性。你等100ms,电机可能已经跑偏了。

4.3 队列在运动指令传递中的应用

这是队列最经典的用法。我画个图你就明白了:

任务A 指令生成 xQueueSend() 运动指令队列 长度: 20 数据: 运动指令结构体 xQueueReceive() 任务B 电机控制 运动指令传递流程 任务A生成指令 → 队列缓存 → 任务B执行指令 ⚠ 队列满时发送会阻塞,注意设置合理的超时时间

实际项目中,我通常定义一个运动指令结构体:

typedef struct {
    uint8_t  cmdType;      // 指令类型:0-停止,1-启动,2-变速
    int32_t  targetPos;    // 目标位置(脉冲数)
    int32_t  targetSpeed;  // 目标速度(脉冲/秒)
    uint32_t accelTime;    // 加速时间(毫秒)
} MotorCommand_t;

// 创建队列
QueueHandle_t xMotorQueue = xQueueCreate(20, sizeof(MotorCommand_t));

这样设计的好处是:指令生成任务只管往里塞指令,电机控制任务只管往外取。两者互不干扰。

避坑指南:我曾经把队列长度设成1,结果指令生成太快,电机还没执行完就被覆盖了。后来改成20,配合超时机制,问题解决。

4.4 多任务数据同步

队列不只是传数据,它还能做同步。你想想看,如果三个任务都要访问同一个电机状态,怎么办?

用队列实现“生产者-消费者”模式,是最优雅的方案。

任务角色 任务名称 队列操作 数据内容
生产者1 按键扫描任务 xQueueSend() 按键指令
生产者2 通信接收任务 xQueueSend() 远程指令
消费者 电机控制任务 xQueueReceive() 执行指令
观察者 状态上报任务 xQueuePeek() 查看但不取走

这里有个小技巧:xQueuePeek() 可以查看队列头部数据,但不移除它。适合“观察者”任务使用。

多任务同步时,我建议遵循几个原则:

  • 单一消费者:一个队列尽量只让一个任务接收,避免竞争
  • 优先级反转:高优先级任务等待低优先级任务的数据?嗯,要小心
  • 队列深度:根据数据产生速率和消费速率计算,别拍脑袋

核心要点:队列是FreeRTOS中最安全的任务间通信方式。它自带互斥保护,你不需要额外加锁。但要注意,队列操作是时间确定的,不会导致死锁。

我记得有一次调试,三个任务同时往一个队列发数据,结果发现数据顺序乱了。后来排查发现,是中断服务函数里用了 xQueueSendFromISR(),而任务里用了 xQueueSend(),两者混用导致优先级问题。嗯,从此我严格区分中断上下文和任务上下文。

队列通信,说白了就是“你只管发,我保证收”。在运动控制这种对实时性要求高的场景,它比全局变量靠谱一万倍。

个人建议:刚开始用队列时,先从小数据量、短队列开始。跑通了再逐步加大。别一上来就搞几百字节的结构体,容易翻车。


专注资料整理