打印
[应用相关]

利用循环缓冲区及状态机解析数据

[复制链接]
91|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
Puchou|  楼主 | 2025-5-11 07:34 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
数据帧格式
串口通信采用如下数据帧结构:

包头 + 包长度 + 数据 + 校验和


包头:人为规定,例如 0xAA
包长度:整个数据帧的长度(含包头、长度、数据、校验)
数据:用户自定义的数据
校验和:前面所有字节(不含自身)的求和结果的最后一个字节(即取 sum & 0xFF)

缓冲区基础设置
#define COMMAND_MIN_LENGTH 4         // 数据帧最小长度
#define BUFFER_SIZE 128             // 循环缓冲区大小

uint8_t buffer[BUFFER_SIZE];        // 缓冲区数组
uint8_t readIndex = 0;              // 读指针
uint8_t writeIndex = 0;             // 写指针



函数说明

读写索引管理

增加读索引
void Command_AddReadIndex(uint8_t length){
    readIndex = (readIndex + length) % BUFFER_SIZE;
}



读取缓冲区中第 i 位的数据
uint8_t Command_Read(uint8_t i){
    uint8_t index = i % BUFFER_SIZE;
    return buffer[index];
}



获取数据长度与空间长度

获取未处理数据长度
uint8_t Command_GetLength(){
    if(readIndex==writeIndex)
        return 0;
    if(writeIndex+1==readIndex ||(writeIndex==BUFFER_SIZE -1 &&readIndex==0)){
        return BUFFER_SIZE;
    }
    if(readIndex<writeIndex){
        return writeIndex - readIndex;
    }else{
        return BUFFER_SIZE -readIndex +writeIndex;
    }
    /*可直接用下面的公式*/
    //return (writeIndex + BUFFER_SIZE - readIndex) % BUFFER_SIZE;
}




获取剩余空间长度
uint8_t Command_GetRemain(){
    return BUFFER_SIZE - Command_GetLength();
}



向缓冲区写入数据
uint8_t Command_Write(uint8_t *data, uint8_t length){
    if (Command_GetRemain() < length) // 检查剩余空间是否足够
        return 0;

    if (writeIndex + length < BUFFER_SIZE){
        // 数据不会越界,直接写入
        memcpy(buffer + writeIndex, data, length);
        writeIndex += length;
    } else {
        // 数据将越界,分两段写入
        uint8_t firstLength = BUFFER_SIZE - writeIndex; // 尾部部分长度
        memcpy(buffer + writeIndex, data, firstLength); // 写入尾部
        memcpy(buffer, data + firstLength, length - firstLength); // 写入头部
        writeIndex = length - firstLength; // 更新写指针
    }

    return length;
}




解析一条完整命令
uint8_t Command_GetCommand(uint8_t *command) {
    // 寻找完整指令
    while (1) {
        // 如果缓冲区长度小于COMMAND_MIN_LENGTH 则不可能有完整的指令
        if (Command_GetLength() < COMMAND_MIN_LENGTH) {
        return 0;
        }
        // 如果不是包头 则跳过 重新开始寻找
        if (Command_Read(readIndex) != 0xAA) {
        Command_AddReadIndex(1);
        continue;
        }
        // 如果缓冲区长度小于指令长度 则不可能有完整的指令
        uint8_t length = Command_Read(readIndex + 1);
        if (Command_GetLength() < length) {
        return 0;
        }
        // 如果校验和不正确 则跳过 重新开始寻找
        uint8_t sum = 0;
        for (uint8_t i = 0; i < length - 1; i++) {
        sum += Command_Read(readIndex + i);
        }
        if (sum != Command_Read(readIndex + length - 1)) {
        Command_AddReadIndex(1);
        continue;
        }
        // 如果找到完整指令 则将指令写入command 返回指令长度
        for (uint8_t i = 0; i < length; i++) {
        command = Command_Read(readIndex + i);
        }
        Command_AddReadIndex(length);
        return length;
    }
}




串口接收初始化设置
为了启用 Rx To Idle 模式的串口接收,需要在初始化阶段显式调用以下函数:

HAL_UARTEx_ReceiveToIdle_IT(&huart2, readBuffer, sizeof(readBuffer));



此调用应放置于主函数 main() 中 HAL 初始化完成后或用户自定义的串口初始化流程末尾。否则将无法正确接收数据。


串口接收回调函数
用于 HAL 库的 Rx To Idle 接口,在接收数据后自动写入循环缓冲区:

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
    if (huart == &huart2){
        Command_Write(readBuffer, Size);
        HAL_UARTEx_ReceiveToIdle_IT(&huart2, readBuffer, sizeof(readBuffer));
    }
}



为什么用状态机?
在原来的方式中,我们通过不断判断数据帧格式(包头、长度、校验)并不断跳过无效字节来尝试提取一帧数据,这种方法逻辑虽然直观,但:

对错包的处理较为混乱;

某些边界条件处理比较啰嗦;

代码结构不太清晰,可读性差;

状态之间的切换不明显,不易调试。

使用状态机之后,逻辑变得像“流程图”一样清晰,每一个接收到的字节只做一件事,根据当前状态决定怎么处理、转到哪个状态。


状态机解析的核心思想
通过状态机逐字节解析串口接收数据,避免处理大缓冲、边界判断复杂的情况。每接收到一个字节就立即判断当前所处状态并处理,有效解决“粘包”、“断包”问题。


状态机实现方式
使用 enum 定义状态
typedef enum {
    WAIT_FOR_HEADER,
    WAIT_FOR_LENGTH,
    WAIT_FOR_DATA,
    WAIT_FOR_CHECKSUM
} ParseState;



变量定义
#define MAX_FRAME_SIZE 64

ParseState currentState = WAIT_FOR_HEADER;
uint8_t rxFrame[MAX_FRAME_SIZE];
uint8_t rxIndex = 0;
uint8_t expectedLength = 0;



状态转移逻辑
void Parse_Byte(uint8_t byte) {
    switch (currentState) {
        case WAIT_FOR_HEADER:
            if (byte == 0xAA) {
                rxFrame[0] = byte;
                rxIndex = 1;
                currentState = WAIT_FOR_LENGTH;
            }
            break;

        case WAIT_FOR_LENGTH:
            rxFrame[rxIndex++] = byte;
            expectedLength = byte;
            if (expectedLength <= MAX_FRAME_SIZE && expectedLength >= 4) {
                currentState = WAIT_FOR_DATA;
            } else {
                currentState = WAIT_FOR_HEADER; // 异常,重置状态机
            }
            break;

        case WAIT_FOR_DATA:
            rxFrame[rxIndex++] = byte;
            if (rxIndex == expectedLength) {
                currentState = WAIT_FOR_CHECKSUM;
            }
            break;

        case WAIT_FOR_CHECKSUM:
            {
                uint8_t sum = 0;
                for (uint8_t i = 0; i < expectedLength - 1; i++) {
                    sum += rxFrame;
                }
                if ((sum & 0xFF) == byte) {
                    // 校验成功,处理数据
                    Handle_Command(rxFrame, expectedLength);
                }
                currentState = WAIT_FOR_HEADER; // 无论成功失败,重置状态
            }
            break;
    }
}



48698681d4d81e753d.png (30.09 KB )

48698681d4d81e753d.png

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

46

主题

120

帖子

0

粉丝