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

📦 串口循环缓冲区设计(基于keysking教程)及状态机改进

🧩 数据帧格式

串口通信采用如下数据帧结构:

包头 + 包长度 + 数据 + 校验和
  • 包头:人为规定,例如 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[i] = 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[i];
                }
                if ((sum & 0xFF) == byte) {
                    // 校验成功,处理数据
                    Handle_Command(rxFrame, expectedLength);
                }
                currentState = WAIT_FOR_HEADER; // 无论成功失败,重置状态
            }
            break;
    }
}

🧾 串口接收中断或回调中调用

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
    if (huart == &huart2){
        for (uint16_t i = 0; i < Size; i++) {
            Parse_Byte(readBuffer[i]);
        }
        HAL_UARTEx_ReceiveToIdle_IT(&huart2, readBuffer, sizeof(readBuffer)); // 重启接收
    }
}

🔄 循环缓冲区 vs 状态机 —— 串口数据接收解析方法对比

📆 一、循环缓冲区法【Ring Buffer】

✅ 优点

  • 【高效利用空间】数据以数组环状形形成循环使用,避免频繁内存分配
  • 【结构清晰】分离接收与解析,便于异步处理
  • 【处理黄包/断包】能力强,适合大数据流接收

❌ 缺点

  • 【逻辑缺备复杂】:需维护读写指针/剩余空间/有效长度等状态
  • 【效率依赖于轮询频率】一般在主循环中扫描解析
  • 【代码量略大】:需写较多辅助函数,如指针维护、跨界处理

⚙️ 二、状态机法【State Machine】

✅ 优点

  • 【逻辑简洁直观】:通过状态转移逐步解析,流程清晰,易于调试
  • 【适合字节流处理】:边接收边处理,无需等待整完数据局
  • 【内存占用小】:不依赖大缓冲区,适合内存缺乏场景
  • 【响应速度快】:字节到达立即处理,适合实时性要求高的场景

❌ 缺点

  • 【难以复用】:状态机逻辑高度依赖协议格式,移植性较差
  • 【不适合大数据量】:频繁状态切换和字节处理在高速场景下效率低
  • 【可读性略差】:太多状态跳转时,代码可读性大降

📊 三、适用场景对比

特性/方式循环缓冲区状态机
实时性一般(靠轮询)高(到就处理)
代码复用性较高较低
实现复杂度中等(辅助函数多)低(逻辑线性)
内存消耗较大(需缓冲区)较小(字节处理)
适合数据流类型批量/黄包/断包简单协议,字节流类
高速大数据处理适合效率低,容易卡顿

🧐 结论建议

  • 【协议复杂/大数据量】:建议使用 循环缓冲区,配合解析函数
  • 【协议简单/实时性要求高】:建议使用 状态机方式,响应较快
  • 【开发初期或调试阶段】:状态机更易部署和解析
  • 【生产级高速设计】:循环缓冲更稳定可靠
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值