ESP-IDF+vscode开发ESP32第十五讲——队列、流缓冲区、环形缓冲区

目录

前言

一、三种机制简介

[1. 队列 (Queue)](#1. 队列 (Queue))

[2. 流缓冲区 (Stream Buffer)](#2. 流缓冲区 (Stream Buffer))

[3. 环形缓冲区 (Ring Buffer)](#3. 环形缓冲区 (Ring Buffer))

[4. 核心区别对比表](#4. 核心区别对比表)

[5. 选型指南](#5. 选型指南)

[二、流缓冲区 (Stream Buffer) 使用](#二、流缓冲区 (Stream Buffer) 使用)

[2.1 uart_m.c](#2.1 uart_m.c)

[2.2 uart_m.h](#2.2 uart_m.h)

[2.3 main.c](#2.3 main.c)

[2.4 代码解释](#2.4 代码解释)

[2.5 结果展示](#2.5 结果展示)

[三、环形缓冲区 (Ring Buffer)使用](#三、环形缓冲区 (Ring Buffer)使用)


前言

在 ESP32开发中,队列(Queue)流缓冲区(Stream Buffer)环形缓冲区(Ring Buffer) 是三种最常用的任务间通信和数据缓冲机制。

虽然它们都能实现"一个任务/中断发数据,另一个任务收数据",但它们的设计初衷、数据组织方式、并发限制和性能开销有着本质的区别。本章就这三种机制进行一个讲解和使用。


一、三种机制简介

1. 队列 (Queue)

  • 来源:FreeRTOS 原生组件。
  • 数据组织:固定大小的离散数据项(Item)。创建时必须指定每个 Item 的大小(如 4 字节的 int,或 32 字节的 struct)。它像是一列火车,每节车厢大小固定,按先进先出(FIFO)顺序排队。
  • 并发支持:支持多读多写(内部有互斥锁保护,线程安全)。
  • 内存机制:发生内存拷贝。发送时把数据 memcpy 进队列,接收时把数据 memcpy 出来。
  • 典型应用场景:
    • 传递传感器离散采样值(如温度、湿度)。
    • 传递控制命令、按键事件、状态标志。
    • 传递结构体(如果结构体很大,通常传递结构体的指针,但需自行管理指针指向的内存生命周期)。

2. 流缓冲区 (Stream Buffer)

  • 来源:FreeRTOS 原生组件(从 v10.0.0 引入)。
  • 数据组织:连续的字节流(Byte Stream)。没有"Item"边界的概念,你可以一次写 10 字节,分两次每次读 5 字节。
  • 并发支持:严格的单读单写(Single Reader, Single Writer)。为了追求极致的性能,它去掉了内部的互斥锁。这意味着只能有一个任务往里写,一个任务从里读,绝不能多任务并发读写。
  • 内存机制:发生内存拷贝。
  • 典型应用场景:
    • 中断 (ISR) 到任务的数据传递(因为无锁,在中断中执行极快)。
    • UART 串口接收不定长的连续数据流。
    • ADC 连续采样产生的数据流。

3. 环形缓冲区 (Ring Buffer)

  • 来源:ESP-IDF 特有组件(非 FreeRTOS 原生,专为 ESP32 优化)。
  • 数据组织:支持变长数据项或纯字节流。它提供了三种类型:
    • RINGBUF_TYPE_NOSPLIT:Item 必须存放在连续的物理内存中(最常用)。、
    • RINGBUF_TYPE_ALLOWSPLIT:允许 Item 在缓冲区尾部截断并绕回头部(节省空间但读取复杂)。
    • RINGBUF_TYPE_BYTEBUF:纯字节流(类似 Stream Buffer)。
  • 并发支持:支持多读多写(内部有锁保护)。
  • 内存机制:支持零拷贝(Zero-Copy)。这是它最大的杀手锏!通过 SendAcquireReceive,你可以直接获取 Ring Buffer 内部内存的指针,直接在里面读写数据,完全省去了 memcpy 的开销。
  • 典型应用场景:
    • 音视频流处理(如 I2S 麦克风采集、MP3 解码数据传递)。
    • DMA 数据缓冲(让 DMA 直接把数据写到 Ring Buffer 内部)。
    • 网络数据包(Wi-Fi/蓝牙)的封包与解包。
    • 任何数据量大、频率高,且对 CPU 拷贝开销敏感的场景。

4. 核心区别对比表

对比维度 队列 (Queue) 流缓冲区 (Stream Buffer) 环形缓冲区 (Ring Buffer)
来源 FreeRTOS 原生 FreeRTOS 原生 ESP-IDF 特有
数据单元 固定大小的 Item 连续字节流 (无边界) 变长 Item 或 字节流
并发限制 多读多写 单读单写 (极其重要) 多读多写
内部锁 有 (互斥锁) 无 (为性能妥协) 有 (自旋锁/互斥锁)
内存拷贝 必须拷贝 (2次) 必须拷贝 (2次) 支持零拷贝 (0次)
CPU 开销 中等 (有锁+拷贝) 最低 (无锁,但有拷贝) 较低 (有锁,但无拷贝)
ISR 支持 支持 (带 FromISR 后缀 API) 极佳 (无锁,中断耗时极短) 支持 (带 FromISR 后缀 API)
空间满时处理 阻塞等待或丢弃新数据 阻塞等待或丢弃新数据 支持覆盖旧数据 (可配置)

5. 选型指南

场景 1:传递控制信号、事件、小结构体

选择 Queue

  • 理由:数据量小(几个字节到几十个字节),拷贝开销可以忽略不计。Queue 支持多读多写,API 最丰富,支持阻塞等待,是传递"离散状态"最稳妥的选择。

场景 2:传递连续的字节流(如串口数据),且只有"一发一收"

选择 Stream Buffer

  • 理由:没有 Item 大小的限制,非常适合处理不定长的字节流。由于它没有锁,在 UART 接收中断(ISR)中调用 xStreamBufferSendFromISR 的速度极快,不会导致中断阻塞。

场景 3:传递大块数据(如音频、图像、大数组),且对性能要求极高

选择 Ring Buffer

  • 理由:当你要传递 1KB 甚至更大的数据块时,Queue 和 Stream Buffer 的 memcpy 会消耗大量 CPU 时间。Ring Buffer 的零拷贝特性允许你直接操作底层内存,或者直接把 Ring Buffer 的内部指针交给 DMA 控制器,从而实现极高的数据吞吐量。

总结一句话

  • Queue 是用来传 "消息/事件" 的(稳妥、通用)。
  • Stream Buffer 是用来传 "字节流" 的(轻量、无锁、单发单收)。
  • Ring Buffer 是用来传 "大数据块" 的(零拷贝、高性能、ESP32 专属)。

说了以上这么多,大家对这三种机制应该有了一个大致的印象,但是如果没有使用开发过,很难去体会它的特点。下面就分别对流缓冲区和环形缓冲区进行使用示例,因为队列属于基础,相信大家都会使用,这里就不再重复了

二、流缓冲区 (Stream Buffer) 使用

我们用《ESP-IDF+vscode开发ESP32第三讲------UART_vscode 开发esp32-CSDN博客》该文的工程来实现流缓冲区的作用。代码如下:

2.1 uart_m.c

cpp 复制代码
#include <stdio.h>
#include "uart_m.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "freertos//stream_buffer.h"

static const char *TAG = "UART";
QueueHandle_t uart_intr_handle= NULL;
StreamBufferHandle_t uart_stream_buffer = NULL;

void uart_intr_callback(void *arg);
void stream_buffer_info(void *arg);

void uart_init(void)
{
    uart_stream_buffer = xStreamBufferCreate(1024, 100);

    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_DEFAULT,
    };
    ESP_ERROR_CHECK(uart_driver_install(uart_port, uart_rx_buffer, uart_tx_buffer, 5, &uart_intr_handle, 0));
    ESP_ERROR_CHECK(uart_param_config(uart_port, &uart_config));
    ESP_ERROR_CHECK(uart_set_pin(uart_port, uart_tx_pin, uart_rx_pin, -1, -1, -1, -1));
    ESP_ERROR_CHECK(uart_set_mode(uart_port, UART_MODE_UART));
    ESP_LOGI(TAG, uart_is_driver_installed(uart_port)? "UART driver is installed" : "UART driver is not installed");
 
    //中断配置
    uart_intr_config_t intr_config = {
        .intr_enable_mask = uart_intr,
        .rx_timeout_thresh = 20,
        .rxfifo_full_thresh = 50,
    };
    ESP_ERROR_CHECK(uart_intr_config(uart_port, &intr_config));
    uart_enable_intr_mask(uart_port, uart_intr);
    uart_set_sw_flow_ctrl(uart_port, true, 10, 30);
    uart_enable_pattern_det_baud_intr(uart_port, '\n', 1, 10, 0, 0);

    xTaskCreate(uart_intr_callback, "uart_intr", 4096, NULL, 5, NULL);
    xTaskCreate(stream_buffer_info, "stream_buffer_info", 4096, NULL, 10, NULL);
    ESP_LOGI(TAG, "UART初始化成功,波特率为:%d", uart_config.baud_rate);

}
void uart_intr_callback(void *arg)
{
    uart_event_t uart_event;
    size_t wait_read_size;
    int len;
    char read_data[1024];
    while(1)
    {
        xQueueReceive(uart_intr_handle, &uart_event, portMAX_DELAY);
        switch (uart_event.type)
        {
            case UART_DATA:
                if (uart_event.timeout_flag == false){
                    ESP_LOGI(TAG, "触发接收FIFO阈值中断, uart rx data size: %d", uart_event.size);
                }
                else{
                    ESP_LOGI(TAG, "触发接收超时中断, uart rx data size: %d", uart_event.size);
                }
                break;
            case UART_PATTERN_DET:
                ESP_LOGI(TAG, "触发PATTERN_DET中断, uart rx data size: %d", uart_event.size);
                break;
            default:
                ESP_LOGI(TAG, "其他未知事件触发");
                break;
        }
        uart_get_buffered_data_len(uart_port, &wait_read_size);
        len = uart_read_bytes(uart_port, read_data, wait_read_size, 1000);
        if(len > 0){
            read_data[len] = '\0';
            ESP_LOGI(TAG, "uart rx data: %s", read_data);
        }
        xStreamBufferSend(uart_stream_buffer, read_data, len, 0);
    }
}
void stream_buffer_info(void *arg)
{
    char data[1024];
    while(1)
    {
        ESP_LOGI(TAG, "Stream Buffer中包含的数据大小: %d", xStreamBufferBytesAvailable(uart_stream_buffer));
        size_t rx_data_size = xStreamBufferReceive(uart_stream_buffer, data, 30, portMAX_DELAY);
        ESP_LOGI(TAG, "Stream Buffer中收到的数据大小: %d", rx_data_size);
        rx_data_size = uart_write_bytes(uart_port, data, rx_data_size);
        ESP_LOGI(TAG, "uart tx length: %d", rx_data_size);
    }
}

2.2 uart_m.h

cpp 复制代码
#ifndef UART_M_H
#define UART_M_H

#include <string.h>              // 字符串处理函数
#include "esp_log.h"             // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h"   // FreeRTOS函数
#include "FreeRTOS/task.h"       // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h"     // FreeRTOS信号量管理函数
#include "hal/uart_ll.h"

#define uart_rx_buffer 512
#define uart_tx_buffer 512
#define uart_port UART_NUM_0
#define uart_tx_pin GPIO_NUM_37
#define uart_rx_pin GPIO_NUM_38
#define uart_intr UART_INTR_RXFIFO_TOUT | UART_INTR_RXFIFO_FULL

void uart_init(void);

#endif                        // UART_M_H

2.3 main.c

cpp 复制代码
#include <stdio.h>
#include "user.h"
#include "uart_m.h"

void app_main(void)
{
    CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
    uart_init(); // 初始化UART
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

2.4 代码解释

这里面关于uart的API就不解释了,需要了解的话去看第三讲的工程。这里说一下改动的地方。

在初始化函数uart_init里使用函数xStreamBufferCreate创建一个流缓冲区,这里没使用带回调函数,一帮情况下也不需要用回调。创建的缓冲区大小是1024字节,触发阈值是100字节。接着在尾部在创建一个流缓冲区任务stream_buffer_info。

在原先串口任务中调用xStreamBufferSend函数,将接收到的数据发送到流缓冲区。

在任务stream_buffer_info里,先使用xStreamBufferSpacesAvailable查询流缓冲区中空闲的大小来判断是否会溢出,接着使用xStreamBufferReceive来接收数据。如果流缓冲区中的数据大小没有到阈值,就会阻塞等待。

xStreamBufferReceive函数读取机制是,第一次读取时缓冲区内数据没有达到阈值,则会阻塞等待,直到数据达到阈值,接触阻塞并读取设定的字节数,如果接下来再次调用则不会阻塞,直到将流缓冲区所有数据读完,下次读取时继续阻塞,如此周期运行。

所以流缓冲区一旦满足了100个字节的阈值,立马解除阻塞并从缓冲区读取30个字节的数据并回传到上位机。接着不断读取30个(不够30个就读所有剩余),直到流缓冲区数据为0,从新下一次阻塞。

2.5 结果展示

我每次从上位机发送21个字节到设备,第五次发送完流缓冲区数据大小为105字节,大于阈值100字节,解除任务stream_buffer_info阻塞,接着不断循环,每次30个字节从流缓冲区中读出,最后一次剩余15个字节,不满30,则全部读出。此时流缓冲区内为空,则继续阻塞任务stream_buffer_info,等待下一次阈值到来。

三、环形缓冲区 (Ring Buffer)使用

敬请期待

相关推荐
taiguisheng4 小时前
Docker中编译esp32
windows·docker·esp32
cheungxiongwei.com5 小时前
VSCode Copilot 如何配置第三方API/自定义端点?
ide·vscode·copilot
游戏开发爱好者85 小时前
iOS开发工具推荐:Xcode、AppCode、SwiftLint使用心得与效率提升
ide·vscode·macos·ios·个人开发·xcode·敏捷流程
AI行业学习6 小时前
CC-Switch 下载、安装与使用配置指南【2026.5.29】
java·开发语言·vscode·python·eclipse·laravel
H Journey6 小时前
Windows下通过vscode连接 Linux服务器
windows·vscode·venv
青山如墨雨如画6 小时前
【Claude】Win11系统VSCode环境中Claude+Deepseek报错的全自动解决方式
vscode·aigc·claude·authropic
权、狐妖1 天前
【Vscode安装ESlint插件、下载ESLint包以及他们之间的关系和使用】
ide·vscode·编辑器
罗超驿1 天前
1.HTML基础入门:标签、属性与路径详解(VSCode开发环境)
前端·vscode·html
摇滚侠1 天前
VScode 需要安装的插件和修改的设置
ide·vscode·编辑器