作为嵌入式开发者,任务间通信是 FreeRTOS 最核心的知识点之一。很多新手都会困惑:“任务之间怎么传递数据?什么时候用队列?什么时候用信号量?”

今天我就结合项目中的实际代码,全面解析 FreeRTOS 的各种任务间通信方式,包括适用场景和最佳实践。


一、任务间通信的本质

想象一下:办公室里有几个同事(任务),他们需要协作完成工作。沟通方式有很多种:

场景沟通方式类比 FreeRTOS
传递文件递纸条队列 (Queue)
通知开会发邮件事件组 (Event Group)
借用工具借钥匙信号量 (Semaphore)
使用会议室预约系统互斥锁 (Mutex)
快速传话口头通知任务通知 (Task Notification)

二、七种通信方式详解

1. 队列 (Queue) - 数据快递

核心作用:任务间传递数据,支持多对多通信。

适用场景
  • 串口数据传递
  • 传感器数据采集
  • 命令下发
实战案例:串口数据传递
// 创建队列:256字节容量
xQueueHandle uartRxQueue = xQueueCreate(256, sizeof(uint8_t));

// 发送数据到队列(任务中)
void SendDataToQueue(uint8_t data) {
    xQueueSend(uartRxQueue, &data, portMAX_DELAY);
}

// 从队列接收数据(任务中)
void ReceiveDataFromQueue(void) {
    uint8_t data;
    if (xQueueReceive(uartRxQueue, &data, 100 / portTICK_RATE_MS) == pdPASS) {
        // 处理数据
        ProcessData(data);
    }
}

// 中断中发送数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uart) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(uartRxQueue, &uartRx, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

队列特点

  • ✅ FIFO(先进先出)
  • ✅ 支持多任务发送/接收
  • ✅ 数据副本传递

2. 事件组 (Event Group) - 状态信号灯

核心作用:任务间状态同步,一个任务等待多个事件。

适用场景
  • 等待多个条件满足
  • 通知多个任务某个事件发生
  • 协议解析完成/错误通知
实战案例:协议解析状态同步
// 创建事件组
EventGroupHandle_t uartEventGroup = xEventGroupCreate();

// 定义事件位
#define PROTOCOL_EVENT_ERR (1 << 0)
#define PROTOCOL_EVENT_OK  (1 << 1)

// 设置事件(解析任务中)
void ParseProtocolComplete(uint8_t success) {
    if (success) {
        xEventGroupSetBits(uartEventGroup, PROTOCOL_EVENT_OK);
    } else {
        xEventGroupSetBits(uartEventGroup, PROTOCOL_EVENT_ERR);
    }
}

// 等待事件(通信任务中)
void WaitForProtocolResult(void) {
    EventBits_t event = xEventGroupWaitBits(
        uartEventGroup,
        PROTOCOL_EVENT_ERR | PROTOCOL_EVENT_OK,
        pdTRUE,   // 自动清除标志
        pdFALSE,  // 任一事件即可
        100 / portTICK_RATE_MS
    );
    
    if (event & PROTOCOL_EVENT_OK) {
        // 处理成功
    } else if (event & PROTOCOL_EVENT_ERR) {
        // 处理错误
    }
}

事件组特点

  • ✅ 支持多事件位
  • ✅ 一个事件可以唤醒多个任务
  • ✅ 适合状态同步

3. 信号量 (Semaphore) - 资源计数器

核心作用:控制对共享资源的访问,或实现任务同步。

适用场景
  • 资源池管理(如缓冲区池)
  • 任务间同步(生产-消费模型)
  • 中断与任务同步
实战案例:资源池管理
// 创建信号量:初始值为5(表示有5个资源)
xSemaphoreHandle resourceSemaphore = xSemaphoreCreateCounting(5, 5);

// 获取资源
void AcquireResource(void) {
    if (xSemaphoreTake(resourceSemaphore, portMAX_DELAY) == pdTRUE) {
        // 使用资源
    }
}

// 释放资源
void ReleaseResource(void) {
    xSemaphoreGive(resourceSemaphore);
}

信号量特点

  • ✅ 计数器模式
  • ✅ 可以有多个持有者
  • ✅ 适合资源池管理

4. 互斥锁 (Mutex) - 排他性访问

核心作用:保护临界区,确保同一时间只有一个任务访问共享资源。

适用场景
  • 串口访问
  • 硬件寄存器操作
  • 共享数据结构访问
实战案例:FPGA 通信保护
// 创建互斥锁
xSemaphoreHandle mutexSemaphore = xSemaphoreCreateMutex();

// 访问共享资源
HAL_StatusTypeDef Communicate2FPGA(uint8_t cFunction, uint8_t cChannel, void *uData, uint8_t cLen) {
    // 获取锁
    xSemaphoreTake(mutexSemaphore, portMAX_DELAY);
    
    // 临界区:访问串口
    CreateSelfDefineProtocolPack(&pack, cFunction, cChannel, uData, cLen);
    TxSelfDefineProtocol(&huart4, &pack);
    
    // 释放锁
    xSemaphoreGive(mutexSemaphore);
    
    return ret;
}

互斥锁特点

  • ✅ 优先级继承机制
  • ✅ 防止优先级反转
  • ✅ 适合临界区保护

5. 任务通知 (Task Notification) - 轻量级通知

核心作用:轻量级的任务间通信,比队列更高效。

适用场景
  • 简单的事件通知
  • 计数信号量替代
  • 二值信号量替代
实战案例:任务唤醒通知
// 任务句柄
TaskHandle_t workerTaskHandle;

// 发送通知(唤醒任务)
void WakeUpWorkerTask(void) {
    xTaskNotifyGive(workerTaskHandle);
}

// 等待通知(工作任务中)
void WorkerTask(void *pvParameters) {
    while(1) {
        // 等待通知
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        // 执行工作
        DoWork();
    }
}

任务通知特点

  • ✅ 无需额外创建对象
  • ✅ 更快更省内存
  • ✅ 适合简单通知场景

6. 消息缓冲区 (Message Buffer) - 变长数据

核心作用:传递变长数据块,支持单次发送/接收完整消息。

适用场景
  • 以太网数据包
  • 可变长度传感器数据
  • 字符串传递
实战案例:变长数据传递
// 创建消息缓冲区:最大消息1024字节
MessageBufferHandle_t msgBuffer = xMessageBufferCreate(1024);

// 发送变长消息
void SendMessage(uint8_t *data, size_t len) {
    size_t sent = xMessageBufferSend(msgBuffer, data, len, portMAX_DELAY);
    if (sent != len) {
        // 发送失败
    }
}

// 接收消息
void ReceiveMessage(void) {
    uint8_t buffer[256];
    size_t received = xMessageBufferReceive(msgBuffer, buffer, sizeof(buffer), portMAX_DELAY);
    if (received > 0) {
        // 处理消息
    }
}

消息缓冲区特点

  • ✅ 支持变长数据
  • ✅ 消息边界保持
  • ✅ 适合数据包传递

7. 流缓冲区 (Stream Buffer) - 流式数据

核心作用:传递流式数据,支持累积接收。

适用场景
  • 串口数据流
  • 音频/视频流
  • 连续数据采集
实战案例:流式数据处理
// 创建流缓冲区:1024字节
StreamBufferHandle_t streamBuffer = xStreamBufferCreate(1024, 1);

// 发送数据流
void SendStreamData(uint8_t *data, size_t len) {
    xStreamBufferSend(streamBuffer, data, len, portMAX_DELAY);
}

// 接收数据流
void ProcessStreamData(void) {
    uint8_t buffer[64];
    while(1) {
        size_t received = xStreamBufferReceive(streamBuffer, buffer, sizeof(buffer), portMAX_DELAY);
        if (received > 0) {
            // 处理数据块
            ProcessChunk(buffer, received);
        }
    }
}

流缓冲区特点

  • ✅ 无消息边界
  • ✅ 适合流式数据
  • ✅ 支持累积读取

三、通信方式选择指南

需求推荐方式理由
传递固定大小数据队列FIFO,数据副本
等待多个条件事件组多事件位同步
保护临界区互斥锁优先级继承
资源池管理计数信号量计数器模式
简单通知任务通知高效轻量
变长数据包消息缓冲区消息边界
流式数据流缓冲区无边界累积

四、项目实战:综合通信架构

完整架构示例

// 通信组件声明
xQueueHandle uartRxQueue;           // 串口接收队列
xQueueHandle sensorDataQueue;       // 传感器数据队列
EventGroupHandle_t systemEventGroup;// 系统事件组
xSemaphoreHandle fpgaMutex;         // FPGA互斥锁
TaskHandle_t protocolTaskHandle;    // 协议任务句柄

// 初始化
void CommunicationInit(void) {
    uartRxQueue = xQueueCreate(256, sizeof(uint8_t));
    sensorDataQueue = xQueueCreate(32, sizeof(SensorData));
    systemEventGroup = xEventGroupCreate();
    fpgaMutex = xSemaphoreCreateMutex();
}

// 协议解析任务
void ProtocolTask(void *pvParameters) {
    uint8_t data;
    while(1) {
        // 从队列接收串口数据
        if (xQueueReceive(uartRxQueue, &data, portMAX_DELAY) == pdPASS) {
            // 解析协议
            if (ParseProtocol(&data)) {
                // 设置事件
                xEventGroupSetBits(systemEventGroup, PROTOCOL_OK);
            }
        }
    }
}

// 传感器采集任务
void SensorTask(void *pvParameters) {
    SensorData data;
    while(1) {
        // 采集数据
        ReadSensor(&data);
        
        // 发送到队列
        xQueueSend(sensorDataQueue, &data, portMAX_DELAY);
        
        // 延迟
        vTaskDelay(100 / portTICK_RATE_MS);
    }
}

// FPGA通信任务
void FPGATask(void *pvParameters) {
    while(1) {
        // 获取互斥锁
        xSemaphoreTake(fpgaMutex, portMAX_DELAY);
        
        // 与FPGA通信
        CommunicateWithFPGA();
        
        // 释放互斥锁
        xSemaphoreGive(fpgaMutex);
        
        vTaskDelay(10 / portTICK_RATE_MS);
    }
}

架构流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                         任务间通信架构                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   [串口中断]    [ProtocolTask]    [SensorTask]    [FPGATask]    [MainTask]  │
│       │              │                  │               │              │     │
│       │ Queue        │                  │               │              │     │
│       └─────────────>│                  │               │              │     │
│                      │ Event Group      │               │              │     │
│                      └─────────────────>│               │              │     │
│                      │                  │ Queue         │              │     │
│                      │                  └──────────────>│              │     │
│                      │                  │               │ Mutex        │     │
│                      │                  │               │<─────────────┤     │
│                      │                  │               │              │     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

五、性能对比

通信方式内存开销速度适用场景
队列中等中等通用数据传递
事件组状态同步
互斥锁临界区保护
信号量资源管理
任务通知最小最快简单通知
消息缓冲区较大中等变长数据
流缓冲区较大中等流式数据

六、新手常见坑

1. 混淆队列和消息缓冲区

// ❌ 错误:用队列传递变长数据
xQueueHandle queue = xQueueCreate(10, sizeof(uint8_t *));  // 只存指针!
uint8_t data[256];
xQueueSend(queue, &data, 0);  // 发送的是指针,不是数据!

// ✅ 正确:用消息缓冲区
MessageBufferHandle_t msgBuf = xMessageBufferCreate(1024);
xMessageBufferSend(msgBuf, data, sizeof(data), portMAX_DELAY);

2. 忘记释放信号量/互斥锁

// ❌ 错误:获取后没释放
xSemaphoreTake(mutex, portMAX_DELAY);
if (error) {
    return;  // 锁没释放!
}
xSemaphoreGive(mutex);

// ✅ 正确:确保释放
xSemaphoreTake(mutex, portMAX_DELAY);
if (error) {
    xSemaphoreGive(mutex);  // 先释放
    return;
}
xSemaphoreGive(mutex);

3. 任务通知的溢出问题

// ❌ 错误:快速发送多个通知会丢失
xTaskNotifyGive(taskHandle);
xTaskNotifyGive(taskHandle);  // 这个通知可能丢失!

// ✅ 正确:使用计数模式
xTaskNotify(taskHandle, 1, eIncrement);  // 累加计数

4. 队列大小设置不合理

// ❌ 错误:队列太小
xQueueHandle queue = xQueueCreate(8, sizeof(uint8_t));  // 只能存8字节

// ✅ 正确:根据需求设置
xQueueHandle queue = xQueueCreate(256, sizeof(uint8_t));  // 256字节

七、总结

通信方式核心用途关键字
队列固定大小数据传递xQueueSend/Receive
事件组多事件同步xEventGroupSetBits/WaitBits
信号量资源计数xSemaphoreTake/Give
互斥锁临界区保护xSemaphoreCreateMutex
任务通知轻量级通知xTaskNotifyGive/Take
消息缓冲区变长消息xMessageBufferSend/Receive
流缓冲区流式数据xStreamBufferSend/Receive

选择建议

  1. 简单通知 → 任务通知
  2. 数据传递 → 队列
  3. 多条件等待 → 事件组
  4. 资源保护 → 互斥锁
  5. 资源池 → 计数信号量
  6. 变长数据 → 消息缓冲区
  7. 流式数据 → 流缓冲区

掌握这些通信方式,你就能灵活地设计任务间的协作机制了!


下一篇预告:FreeRTOS 实战项目架构设计


📝 作者注:任务间通信方式的选择需要结合实际需求,没有绝对的优劣之分。建议在项目初期进行架构设计,选择合适的通信方式,避免后期重构。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐