摘要(From AI):
本文介绍了 FreeRTOS 中的流缓冲区(Stream Buffer)和消息缓冲区(Message Buffer)的使用,重点讲解了它们在任务间数据传输中的应用。流缓冲区适用于实时数据流的传输,支持单一写入者和读取者,而消息缓冲区则用于传递定长消息,确保数据传输的精确性。通过详细的函数使用示例,本文帮助开发者理解如何创建、发送、接收以及重置这些缓冲区,从而优化任务间的通信和同步,提升实时系统的数据处理效率
前言:本文档是本人在依照B站UP:Michael_ee的视频教程进行学习时所做的学习笔记,可能存在疏漏和错误,如有发现,敬请指正
文章目录
- Stream Buffer
- Message Buffer
- xMessageBufferCreate()
- xMessageBufferSend()
- xMessageBufferReceive()
- xMessageBufferReset()
- vMessageBufferDelete()
- Example Code:Message Buffer Communication with Multiple Tasks
参考资料
Michael_ee 视频教程
freeRTOS官网
espressif 在线文档
Stream Buffer
流数据(Stream Data)是指持续不断产生和传输的数据
这些数据通常是按时间顺序排列,并实时传输和处理, 与传统的批量数据处理不同,流数据需要在数据生成的同时进行即时处理,如实时传感器数据、社交媒体的消息流、金融交易数据、网络流量等
FreeRTOS 的流缓冲区和消息缓冲区设计假设只有一个写入者和一个读取者,写入者和读取者可以是不同的任务或中断,但不支持多个写入者或多个读取者
如果有多个写入者,必须将写入操作(如
xStreamBufferSend()
)放入临界区,并使用 0 的发送阻塞时间如果有多个读取者,必须将读取操作放入临界区,并使用 0 的接收阻塞时间
临界区(Critical Section)是指在多任务或多线程环境中,由于共享资源的访问需要互斥,所以必须确保在某段代码执行期间,不会被其他任务或中断打断的代码区域
xStreamBufferCreate()
使用动态分配的内存创建新的流缓冲区
要使用 xStreamBufferCreate()
函数并启用流缓冲区功能,需要确保:
- 在
FreeRTOSConfig.h
配置文件中将configSUPPORT_DYNAMIC_ALLOCATION
设置为 1
或未定义 - 将
FreeRTOS/source/stream_buffer.c
源文件包含在构建中
#include "FreeRTOS.h"
#include "stream_buffer.h"StreamBufferHandle_t xStreamBufferCreate( size_t xBufferSizeBytes, size_t xTriggerLevelBytes );
参数
xBufferSizeBytes
流缓冲区在任何时候能够容纳的总字节数
xTriggerLevelBytes
触发级别,决定了流缓冲区中必须有多少字节数据,才能使被阻塞的任务继续执行
- 如果触发级别设置为 1,任务会在缓冲区有 1 个字节时解除阻塞
- 如果触发级别设置较高(例如 10),任务会等待直到缓冲区中有足够的数据,或者直到阻塞时间到期
- 设置触发级别为 0 等同于设置为 1
- 触发级别不能大于缓冲区的总大小
- xStreamBufferCreate() 设置的触发级别只对进入阻塞状态的任务有效
返回值
StreamBufferHandle_t
已创建的流缓冲区的句柄
NULL
不能创建流缓冲区,因为 FreeRTOS 没有足够的堆内存来分配流缓冲区的数据结构和存储区域
xStreamBufferSend()
将字节发送到流缓冲区,字节被复制到流缓冲区中
#include "FreeRTOS.h"
#include "stream_buffer.h"size_t xStreamBufferSend( StreamBufferHandle_t xStreamBuffer, const void *pvTxData, size_t xDataLengthBytes, TickType_t xTicksToWait );
参数
xStreamBuffer
需要从写入字节的流缓冲区的句柄
pvTxData
指向缓冲区的指针,该缓冲区保存要复制到流缓冲区的字节,需要转换为 void*
xDataLengthBytes
从 pvTxData 复制到流缓冲区的最大字节数
xTicksToWait
如果流缓冲区包含的空间太少,无法容纳另一个 xDataLengthBytes 字节,则任务应保持在挂起状态以等待流缓冲区中有足够的可用空间的最大时间
返回值
写入流缓冲区的字节数
如果一个任务在将所有 xDataLengthBytes 写入缓冲区之前超时,它仍然会写入尽可能多的字节
xStreamBufferReceive()
从流缓冲区接收字节
#include "FreeRTOS.h"
#include "stream_buffer.h"size_t xStreamBufferReceive( StreamBufferHandle_t xStreamBuffer, void *pvRxData, size_t xBufferLengthBytes, TickType_t xTicksToWait );
参数
xStreamBuffer
需要从中接收字节的流缓冲区的句柄
pvRxData
指向接收到的字节要被复制到的数组的指针
xBufferLengthBytes
pvRxData 参数所指向的缓冲区的长度即在一次呼叫中接收的最大字节数
xStreamBufferReceive 将返回尽可能多的字节,直到 xBufferLengthBytes 设置的最大值
xTicksToWait
任务应保持在阻塞状态以等待数据可用的最大时间
返回值
实际从流缓冲区读取的字节数
如果对xStreamBufferReceive()的调用在 xBufferLengthBytes 可用之前超时,则将小于 xBufferLengthBytes
xStreamBufferReset()
将流缓冲区重置为其初始空状态,流缓冲区中的任何数据都将被丢弃
只有当没有任务阻塞等待发送到流缓冲区或从流缓冲区接收时,流缓冲区才能被重置
#include "FreeRTOS.h"
#include "stream_buffer.h"BaseType_t xStreamBufferReset( StreamBufferHandle_t xStreamBuffer );
参数
xStreamBuffer
需要重置的流缓冲区的句柄
返回值
pdPASS
流缓冲区被重置
pdFAIL
有一个任务阻塞等待发送到流缓冲区或从流缓冲区读取,流缓冲区不会被重置
vStreamBufferDelete()
删除先前通过调用xStreamBufferCreate()或xStreamBufferCreateStatic()创建的流缓冲区
#include "FreeRTOS.h"
#include "stream_buffer.h"void vStreamBufferDelete( StreamBufferHandle_t xStreamBuffer );
参数
xStreamBuffer
需要删除的流缓冲区的句柄
RTOS_174">Example Code:Stream Buffer with Task Communication in FreeRTOS
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"#include "freeRTOS/stream_buffer.h"
#include "esp_log.h"
#include <string.h>StreamBufferHandle_t StreamBufferHandle = NULL;TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;void Task0(void *pvParam)
{printf("Task0 is running\n");char txBuffer[50] = {0};int bufferSize = 0;int sendBytes = 0;int i = 0;vTaskDelay(pdMS_TO_TICKS(8000));while (true){i++;bufferSize = sprintf(txBuffer, "Task0 send i:%d to stream buffer\n", i) * sizeof(char); // 将数据写入 txBuffersendBytes = xStreamBufferSend(StreamBufferHandle, (void *)txBuffer, bufferSize, portMAX_DELAY); // 发送数据到 Stream Bufferprintf("Task0 send string:%s\n", txBuffer);printf("sendBytes:%d, bufferSize:%d\n", sendBytes, bufferSize);vTaskDelay(pdMS_TO_TICKS(3000));}
}void Task1(void *pvParam)
{printf("Task1 is running\n");vTaskDelay(pdMS_TO_TICKS(5000));int receiveBytes = 0;char rxBuffer[50] = {0};while (true){memset(rxBuffer, 0, sizeof(rxBuffer));receiveBytes = xStreamBufferReceive(StreamBufferHandle, (void *)rxBuffer, sizeof(rxBuffer), portMAX_DELAY); // 从 Stream Buffer 接收数据// 由于需要接收的数据大于 rxBuffer 大小,会分成多次接收ESP_LOGI("Task1", "receiveBytes:%d, rxBuffer:%s", receiveBytes, rxBuffer);// 由输出可知:// 对于前几次发送的数据,即使没有超过设定的触发级别,Task1 也能接收到数据// 这是因为此时 Task1 还没有进入阻塞状态,会直接读取 Stream Buffer 中的数据// 在此之后,由于 Stream Buffer 中的数据量小于触发级别,Task1 进入阻塞状态// 此后发送的数据只有在超过触发级别时,Task1 才能接收到数据// 如果在 Task0 发送数据之前先阻塞比 Task1 执行接收数据操作前的时间长的时间// 即在 Task0 发送数据前,Task1 已经进入阻塞状态,则所有数据都要超过触发级别才能被接收// 即:xStreamBufferCreate() 设置的触发级别只对进入阻塞状态的任务有效vTaskDelay(pdMS_TO_TICKS(1000));}
}void app_main(void)
{StreamBufferHandle = xStreamBufferCreate(1000, 100); // 创建 Stream Buffer,设置大小为 1000 字节,触发级别为 100 字节if (StreamBufferHandle != NULL){ESP_LOGI("app_main", "StreamBuffer create success");vTaskSuspendAll();xTaskCreatePinnedToCore(Task0, "Task0", 1024 * 5, NULL, 1, &task0Handle, 0);xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, &task1Handle, 0);xTaskResumeAll();}else{ESP_LOGE("app_main", "StreamBuffer create failed");}
}
xStreamBufferSpacesAvailable()
查询流缓冲区以查看它包含多少空闲空间,这等于在流缓冲区满之前可以发送到流缓冲区的数据量
#include "FreeRTOS.h"
#include "stream_buffer.h"size_t xStreamBufferSpacesAvailable( StreamBufferHandle_t xStreamBuffer );
参数
xStreamBuffer
需要拆线呢的流缓冲区句柄
返回值
在流缓冲区满之前可以写入流缓冲区的字节数
RTOS_287">Example Code:Buffer Space Monitoring with Stream Buffer in FreeRTOS
过大的 Stream Buffer Size 会浪费本就不多的单片机资源,通过除sendTask
和receiveTask
以外的第三个 Task 监视 Buffer 中的剩余可用空间,可以帮我们将 Buffer Size 调整为合适的值
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"#include "freeRTOS/stream_buffer.h"
#include "esp_log.h"
#include <string.h>StreamBufferHandle_t StreamBufferHandle = NULL;TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;void Task0(void *pvParam)
{printf("Task0 is running\n");char txBuffer[50] = {0};int bufferSize = 0;int sendBytes = 0;int i = 0;vTaskDelay(pdMS_TO_TICKS(8000));while (true){i++;bufferSize = sprintf(txBuffer, "Task0 send i:%d to stream buffer\n", i) * sizeof(char); // 将数据写入 txBuffersendBytes = xStreamBufferSend(StreamBufferHandle, (void *)txBuffer, bufferSize, portMAX_DELAY); // 发送数据到 Stream Bufferprintf("Task0 send string:%s\n", txBuffer);printf("sendBytes:%d, bufferSize:%d\n", sendBytes, bufferSize);vTaskDelay(pdMS_TO_TICKS(3000));}
}void Task1(void *pvParam)
{printf("Task1 is running\n");vTaskDelay(pdMS_TO_TICKS(5000));int receiveBytes = 0;char rxBuffer[50] = {0};while (true){memset(rxBuffer, 0, sizeof(rxBuffer));receiveBytes = xStreamBufferReceive(StreamBufferHandle, (void *)rxBuffer, sizeof(rxBuffer), portMAX_DELAY); // 从 Stream Buffer 接收数据ESP_LOGI("Task1", "receiveBytes:%d, rxBuffer:%s", receiveBytes, rxBuffer);vTaskDelay(pdMS_TO_TICKS(1000));}
}void Task2(void *pvParam)
{printf("Task2 is running\n");vTaskDelay(pdMS_TO_TICKS(5000));int bufferSpace = 0;int minBufferSpace = 1000;while (true){bufferSpace = xStreamBufferSpacesAvailable(StreamBufferHandle); // 获取 Stream Buffer 中剩余空间if (bufferSpace < minBufferSpace){minBufferSpace = bufferSpace; // 记录 Stream Buffer 中最小的剩余空间,用于后续分析}ESP_LOGI("Task2", "bufferSpace:%d, minBufferSpace:%d", bufferSpace, minBufferSpace);vTaskDelay(pdMS_TO_TICKS(3000));}
}void app_main(void)
{StreamBufferHandle = xStreamBufferCreate(1000, 100); // 创建 Stream Buffer,设置大小为 1000 字节,触发级别为 100 字节if (StreamBufferHandle != NULL){ESP_LOGI("app_main", "StreamBuffer create success");vTaskSuspendAll();xTaskCreatePinnedToCore(Task0, "Task0", 1024 * 5, NULL, 1, &task0Handle, 0);xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, &task1Handle, 0);xTaskCreatePinnedToCore(Task2, "Task2", 1024 * 5, NULL, 1, NULL, 0);xTaskResumeAll();}else{ESP_LOGE("app_main", "StreamBuffer create failed");}
}
Message Buffer
Message Buffer 是 FreeRTOS 提供的一种任务间通信机制,用于传递定长消息数据
与流缓冲区不同,消息缓冲区处理的是具有明确边界的消息(一次一条数据),通常用于需要精确数据传输的场景
它支持阻塞和非阻塞的操作,适用于实时系统中对消息传递有严格要求的应用,如传感器数据、命令传递等
Message Buffer 是专门设计用来传递定长消息的缓冲区,适用于任务间的消息通信,它关注完整的数据块传递
Stream Buffer 是用于字节流传输的缓冲区,适用于需要连续传输数据的场景,允许更灵活的字节流操作
xMessageBufferCreate()
使用动态分配的内存创建一个新的消息缓冲区
为了使 xMessageBufferCreate() 可用,必须在 FreeRTOSConfig.h
中将 configSUPPORT_DYNAMIC_ALLOCATION
设置为 1 或留空未定义
启用消息缓冲区功能需要在构建中包含 FreeRTOS/source/stream_buffer.c
源文件,因为消息缓冲区是基于流缓冲区实现的
#include "FreeRTOS.h"
#include "message_buffer.h"MessageBufferHandle_t xMessageBufferCreate( size_t xBufferSizeBytes );
参数
xBufferSizeBytes
返回值
MessageBufferHandle_t
消息缓冲区已成功创建,返回值应存储为已创建消息缓冲区的句柄
NULL
不能创建消息缓冲区,因为FreeRTOS没有足够的堆内存来分配消息缓冲区的数据结构和存储区域
xMessageBufferSend()
将离散消息发送到消息缓冲区,消息可以是任何长度,只要符合缓冲区的可用空间
#include "FreeRTOS.h"
#include "message_buffer.h"
size_t xMessageBufferSend( MessageBufferHandle_t xMessageBuffer, const void *pvTxData, size_t xDataLengthBytes, TickType_t xTicksToWait );
参数
xMessageBuffer
发送消息的消息缓冲区的句柄
pvTxData
指向要复制到消息缓冲区中的消息的指针
xDataLengthBytes
从pvTxData复制到消息缓冲区的字节数
-
当将一条消息写入消息缓冲区时,除了写入实际的消息数据外,还会写入一个额外的
sizeof(size_t)
字节来存储消息的长度-
sizeof(size_t)
通常是 4 字节(在 32 位架构上),用于存储消息的长度信息 - 如果 xDataLengthBytes 设置为 20,意味着实际的消息数据长度为 20 字节,然而,由于消息长度本身也需要存储在缓冲区中,消息缓冲区的实际内存使用将增加 24 字节:20 字节用于存储消息数据,另外 4 字节用于存储消息长度信息
-
xTicksToWait
如果消息缓冲区没有足够的空间,则调用任务应保持在阻塞状态以等待消息缓冲区中有足够的空间可用的最大时间
返回值
写入消息缓冲区的字节数,如果在有足够的空间将消息写入消息缓冲区之前,对xMessageBufferSend()
的调用超时,则返回0
,此时不会写入任何数据
如果调用没有超时,则返回xDataLengthBytes
xMessageBufferReceive()
从RTOS消息缓冲区接收离散消息,消息可以是可变长度的,并且可以从缓冲区中复制出来
#include "FreeRTOS.h"
#include "message_buffer.h"size_t xMessageBufferReceive( MessageBufferHandle_t xMessageBuffer, void *pvRxData, size_t xBufferLengthBytes, TickType_t xTicksToWait );
参数
xMessageBuffer
从中接收消息的消息缓冲区的句柄
pvRxData
指向缓冲区的指针,接收到的消息将被复制到其中
xBufferLengthBytes
pvRxData参数所指向的缓冲区的长度,设置了可以接收的消息的最大长度
如果 xBufferLengthBytes 太小而不能容纳下一条消息,则消息将留在消息缓冲区中并返回0
xTicksToWait
任务应保留的最大时间,如果消息缓冲区为空,则阻塞状态等待消息
返回值
从消息缓冲区中读取的消息的长度(以字节为单位)
如果 xMessageBufferReceive()在消息可用之前超时,则返回0
如果消息的长度大于 xBufferLengthBytes,则消息将留在消息缓冲区中,并返回0
xMessageBufferReset()
将消息缓冲区重置为其初始空状态,消息缓冲区中的任何数据都将被丢弃
只有当没有任务阻塞等待发送到消息缓冲区或从消息缓冲区接收消息缓冲区时,才能重置消息缓冲区
#include "FreeRTOS.h"
#include "message_buffer.h"BaseType_t xMessageBufferReset( MessageBufferHandle_t xMessageBuffer );
参数
xMessageBuffer
需要重置的消息缓冲区的句柄
返回值
pdPASS
消息缓冲区被重置
pdFAIL
有一个任务阻塞等待发送到消息缓冲区或从消息缓冲区读取,消息缓冲区不会被重置
vMessageBufferDelete()
删除消息缓冲区
#include "FreeRTOS.h"
#include "message_buffer.h"void vMessageBufferDelete( MessageBufferHandle_t xMessageBuffer );
参数
xMessageBuffer
需要删除的消息缓冲区的句柄
Example Code:Message Buffer Communication with Multiple Tasks
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"#include "freeRTOS/message_buffer.h"
#include "esp_log.h"
#include <string.h>MessageBufferHandle_t MessageBufferHandle = NULL;TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;void Task0(void *pvParam)
{printf("Task0 is running\n");char txBuffer[50] = {0};int bufferSize = 0;int sendBytes = 0;for (int i = 0; i < 3; i++){bufferSize = sprintf(txBuffer, "Task0 send i:%d to message buffer\n", i) * sizeof(char);sendBytes = xMessageBufferSend(MessageBufferHandle, (void *)txBuffer, bufferSize, portMAX_DELAY);printf("Task0 send string:%s\n", txBuffer);printf("sendBytes:%d, bufferSize:%d\n", sendBytes, bufferSize);vTaskDelay(pdMS_TO_TICKS(200));}vTaskDelete(NULL);
}void Task1(void *pvParam)
{printf("Task1 is running\n");vTaskDelay(pdMS_TO_TICKS(5000));int receiveBytes = 0;char rxBuffer[200] = {0}; // 通过修改这个buffer的大小,可以看到需要接收的数据大小大于 buffer 的大小时,会返回0,且不接收数据// 如果是 Stream Buffer,则可以分成多次接收while (true){memset(rxBuffer, 0, sizeof(rxBuffer));receiveBytes = xMessageBufferReceive(MessageBufferHandle, (void *)rxBuffer, sizeof(rxBuffer), portMAX_DELAY);// 即使接收的buffer大小足够大,每次接收的数据也只有一条完整消息// 如果是 StreamBuffer,则可以接收多条数据ESP_LOGI("Task1", "receiveBytes:%d, rxBuffer:%s", receiveBytes, rxBuffer);vTaskDelay(pdMS_TO_TICKS(1000));}
}void app_main(void)
{MessageBufferHandle = xMessageBufferCreate(200); // 通过修改这个buffer的大小,可以看到发送的数据大小大于 buffer 的大小时,会返回0,且不发送数据// 不能过小,否则会触发 assert 断言,导致程序重启if (MessageBufferHandle != NULL){ESP_LOGI("app_main", "MessageBuffer create success");vTaskSuspendAll();xTaskCreatePinnedToCore(Task0, "Task0", 1024 * 5, NULL, 1, &task0Handle, 0);xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, &task1Handle, 0);xTaskResumeAll();}else{ESP_LOGE("app_main", "MessageBuffer create failed");}
}