【嵌入式开发】FreeRTOS 任务间通信方式全解析
·
作为嵌入式开发者,任务间通信是 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 |
选择建议:
- 简单通知 → 任务通知
- 数据传递 → 队列
- 多条件等待 → 事件组
- 资源保护 → 互斥锁
- 资源池 → 计数信号量
- 变长数据 → 消息缓冲区
- 流式数据 → 流缓冲区
掌握这些通信方式,你就能灵活地设计任务间的协作机制了!
下一篇预告:FreeRTOS 实战项目架构设计
📝 作者注:任务间通信方式的选择需要结合实际需求,没有绝对的优劣之分。建议在项目初期进行架构设计,选择合适的通信方式,避免后期重构。
更多推荐
所有评论(0)