GitHub 地址:https://github.com/MaJerle/lwrb 当前版本:v3.2.0 协议:MIT Star 数:1.4k+一、库的介绍
在嵌入式开发中,数据在不同模块之间传输是家常便饭:串口收数据、DMA 搬运、任务间通信……这些场景都绕不开一个核心数据结构——环形缓冲区(Ring Buffer)。
自己手撸一个环形缓冲区说难不难,但要做到线程安全、中断安全、支持零拷贝 DMA、再加上事件通知机制,就不那么简单了。
lwrb(Lightweight Ring Buffer)就是为解决这个痛点而生的。它是由嵌入式大神 Tilen MAJERLE 开源的一款轻量级通用 FIFO 环形缓冲区管理库,代码用标准 C11 编写,核心实现仅有两个文件(lwrb.h / lwrb.c),却覆盖了嵌入式开发中缓冲区管理的绝大多数需求。
lwrb 的诞生并非为了炫技,而是真实工程需求的提炼。作者在开发 STM32、ESP、蓝牙等多种嵌入式项目过程中,反复遇到同样的问题,于是将这套经过大量实战验证的缓冲区实现抽象成独立库,开放给社区使用。目前已被全球大量嵌入式开发者引入项目,在 GitHub 上获得 1.4k+ Star、323 次 Fork。
二、核心特性
在深入使用之前,先把 lwrb 的核心特性摸清楚,后面写代码才能用对、用好。
2.1 纯 C 实现,无动态内存分配
lwrb 全程使用静态数组作为缓冲区底层存储,不调用 malloc/free,这在资源受限的嵌入式系统上至关重要。
lwrb_t buff;uint8_t buff_data[64]; // 静态数组,你说了算lwrb_init(&buff, buff_data, sizeof(buff_data));2.2 线程安全 & 中断安全(单读单写场景)lwrb 在单生产者 + 单消费者的场景下(即一个地方写、一个地方读),天然是线程安全和中断安全的——前提是目标 CPU 对 size_t 的读写是原子操作(ARM Cortex-M 满足此条件)。
这个特性非常实用:UART 中断写缓冲区,主循环读缓冲区,无需加锁,直接用。
⚠️ 注意:对于 AVR 等 8 位 CPU,size_t 读写不是原子的,需要手动加临界区保护。2.3 零拷贝 DMA 支持
lwrb 提供了 lwrb_get_linear_block_read_address / lwrb_get_linear_block_write_address 接口,可以直接把缓冲区的内部地址暴露给 DMA 控制器,实现零拷贝数据传输,极大减少 CPU 负担。
2.4 Peek / Skip / Advance 高级操作
- Peek:偷看数据但不移动读指针,适合"先看再决定要不要读"的场景
- Skip:跳过(丢弃)若干字节,移动读指针
- Advance:提前移动写指针,告知缓冲区"硬件已经写了这么多字节"(配合 DMA 使用)
支持注册回调函数,在写入、读取等操作完成时触发通知,方便与上层业务解耦。
2.6 数据查找(Find)
支持在缓冲区中搜索特定字节序列,从指定偏移开始查找,适用于帧协议解析场景。
三、架构解析
理解 lwrb 的内部架构,才能写出正确、高效的代码。
3.1 核心数据结构
typedef struct lwrb {uint8_t* buff; // 指向实际数据存储区lwrb_sz_t size; // 数据区大小(实际可用容量为 size - 1)lwrb_sz_t r; // 读指针lwrb_sz_t w; // 写指针} lwrb_t;缓冲区有三种状态:
状态
条件
r == w
w == (r - 1 + size) % size
其他
介于两者之间
为什么实际容量是 size - 1?这是为了区分"满"和"空"两种状态。如果 r == w 既表示空又表示满,逻辑就乱了。所以 lwrb 牺牲一个字节来明确区分这两种状态。
3.2 读写指针模型
初始状态(空):R=W=0写入 5 字节后:| D | D | D | D | D | | | |R=0 W=5读取 3 字节后:| | | | D | D | | | |R=3 W=5写指针追上读指针(差一格)时,缓冲区满;读指针追上写指针时,缓冲区空。
3.3 文件结构
lwrb/├── lwrb/│ ├── include/│ │ └── lwrb/│ │ └── lwrb.h # 唯一头文件,包含所有 API 声明│ └── src/│ └── lwrb/│ └── lwrb.c # 全部实现,约 600 行├── docs/ # 文档源码├── dev/ # 开发示例└── CMakeLists.txt只需要把 lwrb.h 和 lwrb.c 两个文件加入你的项目,就完成了集成。
四、快速上手4.1 获取源码
# 克隆仓库(推荐使用 develop 分支获取最新特性)git clone --recurse-submodules --branch develop https://github.com/MaJerle/lwrb.git# 或者只下载两个核心文件# lwrb/include/lwrb/lwrb.h# lwrb/src/lwrb/lwrb.c4.2 CMake 集成# 在你的 CMakeLists.txt 中add_subdirectory(lwrb)target_link_libraries(your_target PRIVATE lwrb)4.3 裸机工程集成如果你的项目不用 CMake,直接把两个文件复制进去,在编译器添加头文件路径即可:
your_project/├── src/│ ├── main.c│ ├── lwrb.c ← 复制进来│ └── ...└── include/└── lwrb/└── lwrb.h ← 复制进来4.4 Hello World:基础读写#include#include "lwrb/lwrb.h"int main(void) {lwrb_t buff;uint8_t buff_data[16]; /* 静态缓冲区,实际可用 15 字节 */uint8_t app_buf[8];lwrb_sz_t len;/* 第一步:初始化 */lwrb_init(&buff, buff_data, sizeof(buff_data));/* 第二步:写入数据 */const char* msg = "Hello";len = lwrb_write(&buff, msg, 5);printf("Written: %u bytes\n", (unsigned)len);/* 第三步:查询可读字节数 */printf("Available to read: %u bytes\n",(unsigned)lwrb_get_full(&buff));/* 第四步:读取数据 */len = lwrb_read(&buff, app_buf, sizeof(app_buf));app_buf[len] = '\0';printf("Read: %s (%u bytes)\n", app_buf, (unsigned)len);/* 第五步:验证缓冲区已空 */printf("Buffer empty: %s\n",lwrb_get_full(&buff) == 0 ? "YES" : "NO");return 0;输出:
Written: 5 bytesAvailable to read: 5 bytesRead: Hello (5 bytes)Buffer empty: YES五、各功能模块详解与代码示例5.1 初始化与销毁lwrb_t buff;uint8_t storage[128];/* 初始化 */if (lwrb_init(&buff, storage, sizeof(storage)) == 0) {/* 初始化失败(参数无效) *//* 检查是否初始化成功 */if (lwrb_is_ready(&buff)) {printf("Buffer is ready!\n");/* 重置缓冲区(清空数据,保留 size) */lwrb_reset(&buff);/* 释放(仅清除内部状态,不释放内存,因为没有动态分配) */lwrb_free(&buff);5.2 写操作lwrb_t buff;uint8_t storage[64];lwrb_sz_t written;lwrb_init(&buff, storage, sizeof(storage));/* 普通写入 */uint8_t data[] = {0x01, 0x02, 0x03, 0x04};written = lwrb_write(&buff, data, sizeof(data));if (written < sizeof(data)) {printf("Buffer full! Only wrote %u of %u bytes\n",(unsigned)written, (unsigned)sizeof(data));/* 使用 Flag:要求全部写入,否则返回 0 */lwrb_sz_t bread = 0;uint8_t result = lwrb_write_ex(&buff, data, sizeof(data),&bread, LWRB_FLAG_WRITE_ALL);if (result == 0) {printf("Failed to write all %u bytes (only %u available)\n",(unsigned)sizeof(data), (unsigned)bread);/* 查询写入空闲空间 */lwrb_sz_t free_space = lwrb_get_free(&buff);printf("Free space: %u bytes\n", (unsigned)free_space);5.3 读操作uint8_t rx_buf[32];lwrb_sz_t read_bytes;/* 普通读取 */read_bytes = lwrb_read(&buff, rx_buf, sizeof(rx_buf));printf("Read %u bytes\n", (unsigned)read_bytes);/* 带 Flag 读取:要求全部读取,否则不读 */lwrb_sz_t bread = 0;uint8_t result = lwrb_read_ex(&buff, rx_buf, 10, &bread,LWRB_FLAG_READ_ALL);if (result == 0) {printf("Not enough data (only %u bytes available)\n",(unsigned)bread);/* 查询可读字节数 */lwrb_sz_t avail = lwrb_get_full(&buff);printf("Bytes available: %u\n", (unsigned)avail);5.4 Peek(偷看不消费)Peek 是个很实用的功能——先看数据头部判断是否是完整帧,再决定要不要读取。
/* 场景:协议帧以 0xAA 0x55 开头,先偷看前两字节 */uint8_t header[2];lwrb_sz_t peeked;peeked = lwrb_peek(&buff,0, /* 从当前读位置偏移 0 开始 */header,sizeof(header));if (peeked == 2 && header[0] == 0xAA && header[1] == 0x55) {/* 是有效帧头,继续读取完整帧 */uint8_t frame[64];lwrb_read(&buff, frame, frame_length);} else {/* 不是有效帧,丢弃一个字节,继续同步 */lwrb_skip(&buff, 1);5.5 Skip(跳过/丢弃数据)/* 跳过(丢弃)已处理的数据 */lwrb_sz_t skipped = lwrb_skip(&buff, 10);printf("Skipped %u bytes\n", (unsigned)skipped);5.6 Advance(推进写指针,配合 DMA 使用)这是 lwrb 与 DMA 联动的核心接口。当 DMA 控制器把数据直接写入缓冲区内存后,通过 lwrb_advance 通知 lwrb 更新写指针。
/* DMA 接收完成中断处理(伪代码) */void DMA_RX_Complete_IRQHandler(void) {/* DMA 已经把 received_bytes 字节写入了缓冲区的写区域 */lwrb_advance(&ring_buff, received_bytes);/* 触发主循环处理 */data_received_flag = 1;5.7 线性块地址(零拷贝核心)get_linear_block 系列函数是实现零拷贝 DMA 的关键:
/* ===== 写方向:DMA → 缓冲区 ===== *//* 获取当前可以连续写入的内存地址和长度 */uint8_t* write_addr;lwrb_sz_t write_len;write_addr = lwrb_get_linear_block_write_address(&buff);write_len = lwrb_get_linear_block_write_length(&buff);if (write_len > 0) {/* 把这块地址交给 DMA,让它直接往里写 */DMA_StartReceive(USART1_DMA_RX,write_addr,write_len);/* DMA 完成后(中断里):推进写指针 */void USART1_DMA_Complete(uint32_t bytes_received) {lwrb_advance(&buff, bytes_received);/* ===== 读方向:缓冲区 → 外部处理 ===== */uint8_t* read_addr;lwrb_sz_t read_len;read_addr = lwrb_get_linear_block_read_address(&buff);read_len = lwrb_get_linear_block_read_length(&buff);if (read_len > 0) {/* 直接把缓冲区地址传给处理函数,无需额外拷贝 */process_data(read_addr, read_len);/* 处理完成后,跳过已消费的数据 */lwrb_skip(&buff, read_len);关键理解:Linear Block 是指从当前读/写指针到缓冲区末尾这段连续内存。因为环形缓冲区在绕回时数据可能不连续,所以每次线性块操作之后可能需要再调用一次,处理绕回后的剩余数据。5.8 数据查找(Find)
适用于在流数据中搜索协议帧头、换行符等特殊标记:
/* 在缓冲区中查找换行符 '\n' */uint8_t needle = '\n';lwrb_sz_t found_idx;if (lwrb_find(&buff, &needle, 1, 0, &found_idx)) {/* 找到了,found_idx 是相对于当前读指针的偏移 */printf("Found newline at offset %u\n", (unsigned)found_idx);/* 读取到换行符为止(包含换行符) */uint8_t line[128];lwrb_read(&buff, line, found_idx + 1);line[found_idx] = '\0'; /* 替换换行符为字符串终止符 */printf("Line: %s\n", line);} else {printf("No complete line received yet\n");5.9 事件回调通过注册事件回调,可以感知缓冲区的读写动作,实现与上层业务的松耦合:
/* 事件回调函数 */void my_buff_event(lwrb_t* buff, lwrb_evt_type_t evt, lwrb_sz_t bp) {switch (evt) {case LWRB_EVT_WRITE:/* 有 bp 字节被写入了缓冲区 */printf("[EVT] Written %u bytes to buffer\n", (unsigned)bp);/* 可以在这里发送信号量,通知消费者任务 */// osSemaphoreRelease(rx_semaphore);break;case LWRB_EVT_READ:/* 有 bp 字节被从缓冲区读出 */printf("[EVT] Read %u bytes from buffer\n", (unsigned)bp);break;case LWRB_EVT_RESET:printf("[EVT] Buffer was reset\n");break;default:break;/* 初始化时注册回调 */lwrb_t buff;uint8_t storage[256];lwrb_init(&buff, storage, sizeof(storage));lwrb_set_evt_fn(&buff, my_buff_event);/* 可选:设置自定义参数(在回调中可通过 lwrb_get_arg 获取) */lwrb_set_arg(&buff, (void*)0x1234);/* 之后的读写操作会自动触发回调 */lwrb_write(&buff, "test", 4); /* 触发 LWRB_EVT_WRITE */六、应用场景详解6.1 UART 串口接收缓冲区这是 lwrb 最经典的使用场景。串口中断频繁触发,如果在中断里做协议解析会影响实时性,正确做法是:中断只往环形缓冲区写,主循环从缓冲区读并解析。
/* 全局缓冲区(多处访问) */static lwrb_t uart_rx_buff;static uint8_t uart_rx_data[512];/* 系统初始化 */void system_init(void) {lwrb_init(&uart_rx_buff, uart_rx_data, sizeof(uart_rx_data));UART_EnableReceiveInterrupt(USART1);/* UART 接收中断(HAL 回调形式) */void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart) {if (huart->Instance == USART1) {uint8_t byte = huart->Instance->DR;lwrb_write(&uart_rx_buff, &byte, 1);/* 继续接收下一个字节 */HAL_UART_Receive_IT(huart, &byte, 1);/* 主循环处理 */void main_loop(void) {uint8_t cmd_buf[64];lwrb_sz_t avail;while (1) {avail = lwrb_get_full(&uart_rx_buff);if (avail > 0) {lwrb_sz_t to_read = avail < sizeof(cmd_buf) ?avail : sizeof(cmd_buf);lwrb_read(&uart_rx_buff, cmd_buf, to_read);parse_command(cmd_buf, to_read);/* 其他任务... */6.2 UART + DMA 零拷贝接收使用 DMA 可以让 CPU 从"搬运工"角色解放出来,结合 lwrb 线性块接口实现真正的零拷贝:
static lwrb_t dma_buff;static uint8_t dma_storage[1024];void uart_dma_init(void) {lwrb_init(&dma_buff, dma_storage, sizeof(dma_storage));/* 获取写线性块地址,直接作为 DMA 目标地址 */uint8_t* addr = lwrb_get_linear_block_write_address(&dma_buff);lwrb_sz_t len = lwrb_get_linear_block_write_length(&dma_buff);/* 配置 DMA,将 UART 数据直接搬到 ring buffer 内存 */HAL_UART_Receive_DMA(&huart1, addr, len);/* DMA 传输完成中断 */void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart) {uint32_t received = huart->RxXferSize - huart->RxXferCount;/* 通知 lwrb 写指针推进 */lwrb_advance(&dma_buff, received);/* 重新启动 DMA 接收 */uint8_t* next_addr = lwrb_get_linear_block_write_address(&dma_buff);lwrb_sz_t next_len = lwrb_get_linear_block_write_length(&dma_buff);HAL_UART_Receive_DMA(huart, next_addr, next_len);6.3 RTOS 任务间通信管道在 FreeRTOS 等 RTOS 环境中,lwrb 配合信号量可以构建高效的任务间数据管道:
#include "FreeRTOS.h"#include "semphr.h"#include "lwrb/lwrb.h"static lwrb_t pipe;static uint8_t pipe_data[256];static SemaphoreHandle_t data_sem;/* 事件回调:有数据写入时释放信号量 */void pipe_evt(lwrb_t* b, lwrb_evt_type_t evt, lwrb_sz_t bp) {(void)b; (void)bp;if (evt == LWRB_EVT_WRITE) {BaseType_t higher_woken = pdFALSE;xSemaphoreGiveFromISR(data_sem, &higher_woken);portYIELD_FROM_ISR(higher_woken);/* 生产者任务 */void producer_task(void* arg) {uint8_t sensor_data[16];while (1) {read_sensor(sensor_data, sizeof(sensor_data));lwrb_write(&pipe, sensor_data, sizeof(sensor_data));vTaskDelay(pdMS_TO_TICKS(10));/* 消费者任务 */void consumer_task(void* arg) {uint8_t buf[64];while (1) {/* 等待数据到来 */xSemaphoreTake(data_sem, portMAX_DELAY);lwrb_sz_t n = lwrb_read(&pipe, buf, sizeof(buf));if (n > 0) {process_data(buf, n);void app_init(void) {data_sem = xSemaphoreCreateBinary();lwrb_init(&pipe, pipe_data, sizeof(pipe_data));lwrb_set_evt_fn(&pipe, pipe_evt);xTaskCreate(producer_task, "Producer", 256, NULL, 2, NULL);xTaskCreate(consumer_task, "Consumer", 256, NULL, 1, NULL);6.4 自定义二进制协议帧解析对于帧格式固定的协议(如 [SOF][LEN][DATA][CRC]),结合 peek 和 find 可以写出优雅的解析器:
/* 帧格式:0xAA 0x55 [1字节长度] [N字节数据] [1字节CRC] */#define FRAME_SOF1 0xAA#define FRAME_SOF2 0x55#define FRAME_OVERHEAD 4 /* SOF1 + SOF2 + LEN + CRC */typedef struct {uint8_t len;uint8_t data[64];uint8_t crc;} frame_t;int try_parse_frame(lwrb_t* buff, frame_t* out) {uint8_t header[3];lwrb_sz_t avail = lwrb_get_full(buff);/* 数据不够,等待更多 */if (avail < FRAME_OVERHEAD) return 0;/* 偷看帧头 */if (lwrb_peek(buff, 0, header, 3) < 3) return 0;/* 验证帧头 */if (header[0] != FRAME_SOF1 || header[1] != FRAME_SOF2) {/* 帧头不对,丢弃一个字节,继续同步 */lwrb_skip(buff, 1);return -1;uint8_t data_len = header[2];uint8_t total = data_len + FRAME_OVERHEAD;/* 等待完整帧 */if (avail < total) return 0;/* 读取完整帧 */uint8_t raw[72];lwrb_read(buff, raw, total);/* 验证 CRC */uint8_t crc = calc_crc(raw + 2, data_len + 1);if (crc != raw[total - 1]) return -1;/* 提取数据 */out->len = data_len;memcpy(out->data, raw + 3, data_len);out->crc = raw[total - 1];return 1;七、社区与生态lwrb 属于 MaJerle 的Lw(Lightweight)系列库,这个系列专为嵌入式开发设计,风格统一、质量可靠:
库名
功能
LwRB
环形缓冲区管理
LwJSON
JSON 解析器
LwGPS
GPS NMEA 协议解析
LwMEM
内存管理器
LwESP
ESP-AT 指令库
LwPKT
数据包协议封装
LwSHELL
命令行 Shell
LwPRINTF
轻量 printf 实现
LwBTN
按键管理器
整个系列的设计哲学一致:纯 C 实现、无动态内存、平台无关、MIT 协议。如果你在项目中用了 lwrb,很可能会逐渐引入这个系列的其他库。
参与贡献:
- 发现 Bug?在 GitHub Issues 提交
- 想加新特性?Fork 之后按照 C 编码规范 提交 PR 到 develop 分支
- 项目文档托管在 Read the Docs,地址:docs.majerle.eu/projects/lwrb/
- 作者支持 PayPal 赞助:paypal.me/tilz0R
lwrb 是一个非常"务实"的库:它没有花哨的功能,没有复杂的配置,只专注于把环形缓冲区这件事做到极致。
回顾其核心价值:
- 零动态内存:适合资源受限的嵌入式系统,栈上或全局数组直接用
- 线程/中断安全:单读单写场景下,ARM Cortex-M 直接用,免加锁
- 零拷贝 DMA:通过线性块地址接口,把 DMA 和缓冲区无缝连接,极大减少 CPU 开销
- Peek/Skip/Advance:满足复杂协议解析的精细化控制需求
- 事件回调:写完数据就触发通知,轻松与 RTOS 信号量/消息队列联动
- MIT 协议:商业项目随便用,不用担心开源污染
如果你还在手写环形缓冲区,或者用的是那种只有基础读写的简陋实现,不妨把 lwrb 引入进来。两个文件,五分钟集成,换来的是经过大量工程验证的稳定性和完整的功能集。
代码之所以优雅,是因为作者把复杂性藏进了正确的抽象里。lwrb 做到了这一点。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.