FreeRTOS 手动移植教程(四):队列 —— 任务间通信的最佳起点

前几篇文章中,我们学会了任务创建与延时管理,但任务之间仍无法传递数据。本篇将引入 FreeRTOS 最基础、最常用的 IPC(进程间通信)机制------队列。通过队列,任务与任务、中断与任务之间可以安全、高效地传递数据,彻底告别裸机全局变量带来的隐患。我们将以按键中断通知 LED 闪烁模式切换为例,完整演示队列的创建、发送、接收及阻塞等待的全过程。


一、为什么需要队列?

在裸机程序中,任务间的数据共享通常靠全局变量实现,但在 RTOS 下这种做法极易引发问题:

  • 竞争条件:多个任务同时读写同一个变量,可能造成数据错乱;
  • 非阻塞需求:接收方任务需要等待数据,又不希望浪费 CPU 做轮询;
  • 中断中传递数据:中断需要快速退出,不能等待任务处理完数据。

队列提供了一种线程安全的机制:它自带互斥访问和阻塞/唤醒逻辑,可以方便地在不同任务(或中断)之间传递消息,而无需开发者自行实现复杂的锁机制。


二、队列的核心概念与 API

2.1 队列的基本模型

队列可以看作一个先进先出(FIFO)的环形缓冲区,每个队列项的大小在创建时固定。任务可以向队列尾部发送 数据,也可以从队列头部接收 数据。当队列为空时,接收任务可以选择阻塞等待,直到有数据进入队列;当队列满时,发送任务也可以阻塞等待,直到队列有空位。

2.2 关键 API

功能 API 名称 说明
创建队列 xQueueCreate 返回队列句柄,后续操作均基于此句柄
发送数据(任务) xQueueSend 类似 xQueueSendToBack,将数据放到队尾
发送数据(中断) xQueueSendFromISR 中断服务函数中使用的版本
接收数据(任务) xQueueReceive 从队首取出数据,可设置阻塞超时
接收数据(中断) xQueueReceiveFromISR 中断中使用的版本(但不常用)
查询队列中项数 uxQueueMessagesWaiting 返回当前队列中的有效数据项数量
删除队列 vQueueDelete 释放队列占用的内存

三、硬件准备与优先级分组设置

3.1 硬件连接

本章实验使用一个按键(PA0)和一个 LED(PC13),要求按一次按键切换一次 LED 的闪烁模式。

3.2 中断优先级分组(必须)

为了让 FreeRTOS 的中断管理策略正确生效,必须在系统初始化早期设置 NVIC 优先级分组为4位抢占优先级 。在 main() 函数开头调用:

c 复制代码
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

这样,每个中断的 4 位优先级都用于抢占,子优先级不再使用,与我们 FreeRTOSConfig.h 中的配置完全匹配。


四、扩展板级驱动

4.1 按键 BSP

BSP 目录下新建 bsp_key.hbsp_key.c

c 复制代码
// bsp_key.h
#ifndef BSP_KEY_H
#define BSP_KEY_H

#include "stm32f10x.h"

void KEY_Init(void);
uint8_t KEY_Read(void);   // 返回 1 表示按下

#endif
c 复制代码
// bsp_key.c
#include "bsp_key.h"

void KEY_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  // 上拉输入,按下为低电平
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

uint8_t KEY_Read(void)
{
    // 返回 1 表示按键按下(PA0 为低电平)
    return (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) ? 1 : 0;
}

4.2 LED BSP

沿用之前文章的 bsp_led.hbsp_led.c,确保已包含 LED_InitAll()LED3_Toggle() 函数。LED3_Toggle 翻转的是 GPIOC->ODR 的 Pin_13。


五、按键中断配置

5.1 EXTI 初始化(标准库)

BSP 目录下新建 bsp_exti.hbsp_exti.c

c 复制代码
// bsp_exti.h
#ifndef BSP_EXTI_H
#define BSP_EXTI_H

#include "stm32f10x.h"

void EXTI0_Init(void);

#endif
c 复制代码
// bsp_exti.c
#include "bsp_exti.h"
#include "bsp_key.h"

void EXTI0_Init(void)
{
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    KEY_Init();   // 初始化 PA0 引脚

    // 将 PA0 连接到 EXTI 线 0
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  // 下降沿触发
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5; // 优先级 5,可安全调用 RTOS API
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

说明 :中断优先级设为 5,与我们配置的 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 一致,表示该中断可以安全调用 FreeRTOS 的 FromISR 系列函数。优先级 0~4 的中断完全不被打扰,但绝对不能在其内部调用任何 RTOS API。

5.2 中断服务函数

stm32f10x_it.c 中编写 EXTI0_IRQHandler(如果该文件已有弱定义的空函数,直接覆盖即可)。

c 复制代码
// stm32f10x_it.c
#include "stm32f10x_it.h"
#include "FreeRTOS.h"
#include "queue.h"

extern QueueHandle_t xKeyQueue;   // 队列句柄,在 main.c 中定义

void EXTI0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 发送按键消息到队列,仅发送一个字节(内容无关紧要,只表示事件)
        uint8_t key_event = 1;
        xQueueSendFromISR(xKeyQueue, &key_event, &xHigherPriorityTaskWoken);

        EXTI_ClearITPendingBit(EXTI_Line0);
    }

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);  // 如有更高优先级任务被唤醒,则触发切换
}

六、实现队列通信与 LED 模式切换

6.1 任务设计

  • KeyProcessTask:阻塞等待队列中的按键事件,收到后切换 LED 模式;
  • LedTask:根据当前模式以不同频率闪烁 LED。

6.2 main.c 完整代码

c 复制代码
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "bsp_led.h"
#include "bsp_exti.h"

/* 队列句柄,需在中断服务函数中引用 */
QueueHandle_t xKeyQueue = NULL;

/* 闪烁模式(0 = 慢闪,1 = 快闪) */
volatile uint8_t led_mode = 0;

/* 按键处理任务 */
void vKeyProcessTask(void *pvParameters)
{
    uint8_t key_val;
    while (1)
    {
        // 阻塞等待队列数据(portMAX_DELAY 表示无限等待)
        if (xQueueReceive(xKeyQueue, &key_val, portMAX_DELAY) == pdTRUE)
        {
            // 收到按键事件,切换模式
            led_mode = !led_mode;
        }
    }
}

/* LED 闪烁任务 */
void vLedTask(void *pvParameters)
{
    while (1)
    {
        if (led_mode == 0)
        {
            LED3_Toggle();
            vTaskDelay(pdMS_TO_TICKS(500));   // 慢闪:周期 1s
        }
        else
        {
            LED3_Toggle();
            vTaskDelay(pdMS_TO_TICKS(100));   // 快闪:周期 200ms
        }
    }
}

int main(void)
{
    /* 设置中断优先级分组为 4 位抢占优先级(必须) */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

    LED_InitAll();    // 初始化 LED
    EXTI0_Init();     // 初始化按键中断

    /* 创建队列,每个消息为 1 个 uint8_t,队列长度 5(足够缓冲按键事件) */
    xKeyQueue = xQueueCreate(5, sizeof(uint8_t));
    if (xKeyQueue == NULL)
    {
        // 创建失败,阻塞程序(实际项目中可重启或报错)
        while (1);
    }

    xTaskCreate(vKeyProcessTask, "KeyProc", 128, NULL, 2, NULL);
    xTaskCreate(vLedTask,         "Led",     128, NULL, 1, NULL);

    vTaskStartScheduler();

    while (1);
}

七、实验现象与验证

  1. 上电后,LED 以 1Hz 频率闪烁(慢闪模式);
  2. 每按一次 PA0 按键,LED 在慢闪(周期 1s)和快闪(周期 200ms)之间切换;
  3. 由于在中断中使用了 xQueueSendFromISR,按键响应实时且不丢事件;
  4. KeyProcessTask 使用 portMAX_DELAY 阻塞等待,完全不占用 CPU,仅在按键按下时才被唤醒执行。

如果需要传递更复杂的数据(如按键次数、按键类型),可以扩大队列项大小,或传递结构体。但请注意:队列存储的是数据副本,而非指针,因此不必担心悬挂指针问题。


八、队列使用的注意事项

  • 阻塞超时portMAX_DELAY 表示无限等待;0 表示不等待;其他值为等待的 tick 数。
  • 中断中必须使用 FromISR 版本xQueueSendFromISRxQueueReceiveFromISR,它们的最后一个参数 pxHigherPriorityTaskWoken 必须传递给 portYIELD_FROM_ISR
  • 队列拷贝语义:所有通过队列传递的数据都会进行字节级拷贝,接收方修改拷贝的数据不会影响队列内部状态。
  • 大小估算:队列长度应能容纳最坏情况下的突发数据,接收任务要及时处理以免队列溢出。

九、总结

通过本篇的实战,你应该掌握了:

  • 队列的创建、发送、接收与阻塞等待;
  • 如何在标准库外部中断中使用 xQueueSendFromISR 安全地传递事件;
  • 队列天然解决了中断与任务间的同步问题,实现了硬件事件与业务逻辑的解耦。

队列是 FreeRTOS 中使用最频繁的 IPC 工具,所有稍复杂的 RTOS 应用都会用到它。下一篇文章我们将学习二值信号量与计数信号量,解决任务同步、中断通知以及资源管理的常见场景。


下一篇:FreeRTOS 信号量 ------ 任务同步与中断通知的优雅解决方案。

相关推荐
0x3F(小茶)1 小时前
嵌入式C设计模式完全指南(基于《C嵌入式编程设计模式》)
c语言·开发语言·单片机·嵌入式硬件·设计模式
都在酒里1 小时前
FreeRTOS 手动移植教程(二):任务管理——多任务创建、优先级抢占与删除
stm32·单片机·嵌入式硬件·rtos
都在酒里2 小时前
FreeRTOS 手动移植教程(五):信号量 —— 任务同步与中断通知的优雅解决方案
stm32·单片机·rtos·嵌入式软件
紫阡星影3 小时前
【STM32CubeMX项目】智能家居门禁系统
c语言·单片机·嵌入式硬件
txh05074 小时前
从零开始学习FOC
单片机·嵌入式硬件·学习
2601_961194024 小时前
考研政治历年真题及解析pdf
stm32·单片机·嵌入式硬件·物联网·考研·pdf
今日待办4 小时前
STM32H747I-DISCO 开发指南【数字麦克风使用】
stm32·单片机·嵌入式硬件
世微 如初4 小时前
【方案】AP5127摩托车灯驱动设计:12-100V输入,2.5A恒流
单片机·嵌入式硬件
嵌入式ZYXC4 小时前
第7章:原理图设计与阅读——从“能看懂”到“会画”的关键一跃
stm32·单片机·嵌入式硬件·物联网