【C语言】环形缓冲区ringbuffer

发布于:2025-08-08 ⋅ 阅读:(9) ⋅ 点赞:(0)

ringbufferts.h

/**
 * @file ringbufferts.h
 * @brief 线程安全环形缓冲区实现头文件
 * @author [作者名]
 * @date [日期]
 * @version 1.0
 *
 * 该文件定义了线程安全的环形缓冲区数据结构及相关操作函数。
 * 环形缓冲区是一种固定大小的缓冲区,当数据写入到达缓冲区末尾时会从头开始继续写入,
 * 实现了先进先出(FIFO)的数据存储方式。本实现通过互斥锁保证线程安全。
 */

#ifndef RING_BUFFER_TS_H
#define RING_BUFFER_TS_H

#include <stdint.h>
#include <stdbool.h>

/**
 * @brief 线程安全环形缓冲区结构体
 */
typedef struct RingBufferTS {
    uint8_t *buffer;              /**< 缓冲区内存指针 */
    uint16_t size;                /**< 缓冲区总大小 */
    volatile uint16_t writeIndex; /**< 写索引,指向下一个可写入位置 */
    volatile uint16_t readIndex;  /**< 读索引,指向下一个可读取位置 */

    void *mutex;                  /**< 互斥锁句柄,用于线程同步 */
} RingBufferTS;

/**
 * @brief 初始化环形缓冲区
 * @param rb 环形缓冲区结构体指针
 * @param buffer 用于存储数据的缓冲区内存
 * @param size 缓冲区大小
 */
void RingBufferTS_Init(RingBufferTS *rb, uint8_t *buffer, uint16_t size);

/**
 * @brief 销毁环形缓冲区
 * @param rb 环形缓冲区结构体指针
 */
void RingBufferTS_Deinit(RingBufferTS *rb);

/**
 * @brief 获取缓冲区中可用数据的字节数
 * @param rb 环形缓冲区结构体指针
 * @return 可读取的字节数
 */
uint16_t RingBufferTS_AvailableData(RingBufferTS *rb);

/**
 * @brief 获取缓冲区中可用空间的字节数
 * @param rb 环形缓冲区结构体指针
 * @return 可写入的字节数
 */
uint16_t RingBufferTS_AvailableSpace(RingBufferTS *rb);

/**
 * @brief 向缓冲区写入数据
 * @param rb 环形缓冲区结构体指针
 * @param data 待写入的数据指针
 * @param len 待写入的数据长度
 * @return 实际写入的字节数
 */
uint16_t RingBufferTS_Write(RingBufferTS *rb, const uint8_t *data, uint16_t len);

/**
 * @brief 从缓冲区窥视数据(不移动读指针)
 * @param rb 环形缓冲区结构体指针
 * @param dest 目标存储区域指针
 * @param len 希望读取的数据长度
 * @return 实际读取的字节数
 */
uint16_t RingBufferTS_Peek(RingBufferTS *rb, uint8_t *dest, uint16_t len);

/**
 * @brief 消费缓冲区中的数据(移动读指针)
 * @param rb 环形缓冲区结构体指针
 * @param len 希望消费的数据长度
 */
void RingBufferTS_Consume(RingBufferTS *rb, uint16_t len);

#endif

ringbufferts.c

/**
 * @file ringbufferts.c
 * @brief 线程安全环形缓冲区实现源文件
 * @author [作者名]
 * @date [日期]
 * @version 1.0
 *
 * 该文件实现了线程安全的环形缓冲区相关操作函数,包括初始化、销毁、读写等操作。
 * 通过互斥锁保证在多线程环境下的数据一致性。
 */

#include "ringbufferts.h"
#include "cm_os.h"
#include <string.h>

/**
 * @brief 初始化环形缓冲区
 * @param rb 环形缓冲区结构体指针
 * @param buffer 用于存储数据的缓冲区内存
 * @param size 缓冲区大小
 *
 * 此函数初始化环形缓冲区的各个字段,并创建互斥锁用于线程同步。
 * 缓冲区的读写索引都被初始化为0,表示缓冲区为空。
 */
void RingBufferTS_Init(RingBufferTS *rb, uint8_t *buffer, uint16_t size) {
    rb->buffer = buffer;
    rb->size = size;
    rb->writeIndex = 0;
    rb->readIndex = 0;
    rb->mutex = osMutexNew(NULL);
}

/**
 * @brief 销毁环形缓冲区
 * @param rb 环形缓冲区结构体指针
 *
 * 此函数释放环形缓冲区使用的互斥锁资源,并将互斥锁句柄置为NULL。
 */
void RingBufferTS_Deinit(RingBufferTS *rb) {
    if (rb->mutex) {
        osMutexDelete((osMutexId_t )rb->mutex);
        // vSemaphoreDelete((SemaphoreHandle_t)rb->mutex);
        rb->mutex = NULL;
    }
}

/**
 * @brief 加锁操作
 * @param rb 环形缓冲区结构体指针
 *
 * 内部辅助函数,用于获取互斥锁,确保对缓冲区的访问是线程安全的。
 */
static void lock(RingBufferTS *rb) {
    if (rb->mutex) {
        // xSemaphoreTake((SemaphoreHandle_t)rb->mutex, portMAX_DELAY);
        osMutexAcquire((osMutexId_t )rb->mutex,osWaitForever);
    }
}

/**
 * @brief 解锁操作
 * @param rb 环形缓冲区结构体指针
 *
 * 内部辅助函数,用于释放互斥锁,允许其他线程访问缓冲区。
 */
static void unlock(RingBufferTS *rb) {
    if (rb->mutex) {
        // xSemaphoreGive((SemaphoreHandle_t)rb->mutex);
        osMutexRelease((osMutexId_t )rb->mutex);
    }
}

/**
 * @brief 获取缓冲区中可用数据的字节数
 * @param rb 环形缓冲区结构体指针
 * @return 可读取的字节数
 *
 * 此函数计算缓冲区中当前可读取的数据量:
 * - 当写索引大于等于读索引时,数据量为 writeIndex - readIndex
 * - 当写索引小于读索引时,说明数据环绕到了缓冲区开头,数据量为 size - (readIndex - writeIndex)
 */
uint16_t RingBufferTS_AvailableData(RingBufferTS *rb) {
    uint16_t ret;
    // lock(rb);
    if (rb->writeIndex >= rb->readIndex)
        ret = rb->writeIndex - rb->readIndex;
    else
        ret = rb->size - (rb->readIndex - rb->writeIndex);
    // unlock(rb);
    return ret;
}

/**
 * @brief 获取缓冲区中可用空间的字节数
 * @param rb 环形缓冲区结构体指针
 * @return 可写入的字节数
 *
 * 为什么要减1?
 * 假设缓冲区大小是 N,索引范围是 0∼N−1。
 * 当 writeIndex == readIndex 时,缓冲区为空。
 * 如果允许写满整个缓冲区,即写入 N 个元素后,writeIndex 会追上 readIndex,此时会出现空和满状态无法区分的问题。
 * 为了避免这种歧义,通常设计成:
 * 缓冲区最多只能存储 N−1 个元素。
 * 保留一个空位作为"满"和"空"的分界。
 */
uint16_t RingBufferTS_AvailableSpace(RingBufferTS *rb) {
    uint16_t space;
    // lock(rb);
    space = rb->size - RingBufferTS_AvailableData(rb) - 1;
    // unlock(rb);
    return space;
}

/**
 * @brief 向缓冲区写入数据
 * @param rb 环形缓冲区结构体指针
 * @param data 待写入的数据指针
 * @param len 待写入的数据长度
 * @return 实际写入的字节数
 *
 * 此函数将数据写入环形缓冲区,在写入前会检查参数有效性及缓冲区空间是否足够:
 * 1. 如果请求写入的数据量大于可用空间,则只写入可用空间大小的数据
 * 2. 如果写入位置到达缓冲区末尾,则将剩余数据写入缓冲区开头(处理环绕情况)
 */
uint16_t RingBufferTS_Write(RingBufferTS *rb, const uint8_t *data, uint16_t len) {
    uint16_t written = 0;
    if (!rb || !data || len == 0) return 0;

    lock(rb);

    uint16_t space = RingBufferTS_AvailableSpace(rb);
    if (len > space) {
        len = space;
    }

    uint16_t firstPart = rb->size - rb->writeIndex;
    if (firstPart > len)
        firstPart = len;

    memcpy(&rb->buffer[rb->writeIndex], data, firstPart);
    rb->writeIndex = (rb->writeIndex + firstPart) % rb->size;

    uint16_t secondPart = len - firstPart;
    if (secondPart > 0) {
        memcpy(&rb->buffer[rb->writeIndex], data + firstPart, secondPart);
        rb->writeIndex = (rb->writeIndex + secondPart) % rb->size;
    }

    written = len;

    unlock(rb);
    return written;
}

/**
 * @brief 从缓冲区窥视数据(不移动读指针)
 * @param rb 环形缓冲区结构体指针
 * @param dest 目标存储区域指针
 * @param len 希望读取的数据长度
 * @return 实际读取的字节数
 *
 * RingBufferTS_Peek 函数的作用是 从环形缓冲区中"窥视"一定长度的数据,但不移动读指针,
 * 也就是说,它允许你查看缓冲区中当前可读的数据内容,而不会改变缓冲区的状态(不会标记数据为已消费)。
 *
 * 实现细节:
 * 1. 如果请求读取的数据量大于可用数据量,则只读取可用数据量大小的数据
 * 2. 如果读取位置到达缓冲区末尾,则从缓冲区开头继续读取(处理环绕情况)
 */
uint16_t RingBufferTS_Peek(RingBufferTS *rb, uint8_t *dest, uint16_t len) {
    uint16_t ret = 0;
    if (!rb || !dest || len == 0) return 0;

    lock(rb);

    uint16_t available = RingBufferTS_AvailableData(rb);
    if (len > available)
        len = available;

    uint16_t firstPart = rb->size - rb->readIndex;
    if (firstPart > len)
        firstPart = len;

    memcpy(dest, &rb->buffer[rb->readIndex], firstPart);

    uint16_t secondPart = len - firstPart;
    if (secondPart > 0) {
        memcpy(dest + firstPart, &rb->buffer[0], secondPart);
    }

    ret = len;

    unlock(rb);
    return ret;
}

/**
 * @brief 消费缓冲区中的数据(移动读指针)
 * @param rb 环形缓冲区结构体指针
 * @param len 希望消费的数据长度
 *
 * RingBufferTS_Consume 函数的作用是 从环形缓冲区中"消费"一定长度的数据,也就是说,它会移动读指针(readIndex),
 * 表示已经读取或处理了这些数据,缓冲区中这部分数据可以被覆盖或重新写入。
 *
 * 实现细节:
 * 1. 如果请求消费的数据量大于可用数据量,则只消费可用数据量大小的数据
 * 2. 读索引会根据消费的数据量进行相应移动,支持环绕操作
 */
void RingBufferTS_Consume(RingBufferTS *rb, uint16_t len) {
    if (!rb || len == 0) return;

    lock(rb);

    uint16_t available = RingBufferTS_AvailableData(rb);
    if (len > available)
        len = available;

    rb->readIndex = (rb->readIndex + len) % rb->size;

    unlock(rb);
}

使用示例

监听modbus报文,或者类crc校验报文,并解析。
主函数周期性将数据从串口中读取,并放入环形缓冲区,同时周期调用parseModbusFrames解析缓冲区中的报文。

    uint8_t rxBuffer[RX_BUFFER_SIZE];
    RingBufferTS rxRingBuffer;
    RingBufferTS_Init(&rxRingBuffer,rxBuffer,sizeof(rxBuffer));
    uint8_t buf[20];
    while (1)
    {
        sleep_ms(10);
        int len = readDataByLenFromUartWait(UART_CAP, buf, sizeof(buf), 0);
        if (len > 0) {
            rxBufferWrite(&rxRingBuffer,buf, len);
        }
        // 解析缓冲区中的Modbus报文
        parseModbusFrames(&rxRingBuffer);
    }
static void parseModbusFrames(RingBufferTS *rb) {
    uint8_t tempFrame[RX_BUFFER_SIZE];
    while (RingBufferTS_AvailableData(rb) >= 5) {
        uint16_t available = RingBufferTS_AvailableData(rb);
        uint16_t peekLen = RingBufferTS_Peek(rb, tempFrame, available);
        if (peekLen < 5) break;

        uint16_t frameLen = getModbusFrameLength(tempFrame, peekLen);
        if (frameLen == 0 || frameLen > peekLen) {
            if (frameLen == 0) {
                // 跳过一个字节,避免死循环
                RingBufferTS_Consume(rb, 1);
            }
            break;
        }

        uint16_t crcCalc = modbusCRC16(tempFrame, frameLen - 2);
        uint16_t crcRecv = (uint16_t)tempFrame[frameLen - 2] | ((uint16_t)tempFrame[frameLen - 1] << 8);

        if (crcCalc == crcRecv) {
            processModbusFrame(tempFrame, frameLen);
            RingBufferTS_Consume(rb, frameLen);
        } else {
            RingBufferTS_Consume(rb, 1);
        }
    }
}

#define MIN_MODBUS_FRAME_SIZE 5  // 地址(1) + 功能码(1) + CRC(2) + 最少1字
uint16_t getModbusFrameLength(const uint8_t *frame, uint16_t len) {
    if (len < MIN_MODBUS_FRAME_SIZE) {
        return 0; // 数据长度不足,无法判断
    }

    uint8_t funcCode = frame[1];

    switch (funcCode) {
        case 0x01: // 读线圈状态
        case 0x02: // 读离散输入
        case 0x03: // 读保持寄存器
        case 0x04: // 读输入寄存器
            // 响应帧格式:地址(1) + 功能码(1) + 字节计数(1) + 数据(N) + CRC(2)
            if (len < 5) return 0;
            {
                uint8_t byteCount = frame[2];
                uint16_t frameLen = 3 + byteCount + 2; // 头3字节 + 数据 + CRC
                if (frameLen <= len) {
                    return frameLen;
                } else {
                    return 0; // 数据不完整
                }
            }

        case 0x05: // 写单个线圈
        case 0x06: // 写单个寄存器
        case 0x0F: // 写多个线圈(请求)
        case 0x10: // 写多个寄存器(请求)
            // 请求帧格式固定长度
            // 0x05和0x06请求帧长度固定为8字节
            if (funcCode == 0x05 || funcCode == 0x06) {
                if (len >= 8) return 8; else return 0;
            }
            // 0x0F和0x10请求帧长度可变
            if (funcCode == 0x0F || funcCode == 0x10) {
                if (len < 7) return 0; // 至少7字节才能读取字节计数
                uint8_t byteCount = frame[6];
                uint16_t frameLen = 7 + byteCount + 2; // 7字节头 + 数据 + CRC
                if (frameLen <= len) {
                    return frameLen;
                } else {
                    return 0;
                }
            }
            break;

        case 0x81: // 异常响应(功能码 + 0x80)
        case 0x82:
        case 0x83:
        case 0x84:
        case 0x85:
        case 0x86:
        case 0x8F:
        case 0x90:
            // 异常响应帧长度固定为5字节
            if (len >= 5) return 5; else return 0;

        default:
            // 其他功能码暂时无法判断长度,返回0
            return 0;
    }

    return 0;
}
void processModbusFrame(const uint8_t *frame, uint16_t length) {
    // 这里写你的报文处理逻辑
    // 例如打印、响应等
}

网站公告

今日签到

点亮在社区的每一天
去签到