☰
STM32 串口接收防错乱实践:DMA 环形缓冲与帧头解析
2026/9/26 17:34:51 网站建设 项目流程

目录

前言

环形缓冲区

CubeMX配置

代码编写

帧格式

环形缓冲区结构体和初始化

串口环形缓冲写入

环形缓冲区读取

结语


前言

按上篇文章基于 STM32 的空调蓝牙遥控器设计与实现:红外发射篇的步骤,笔者测试中发现,如果不小心按错,先发了一个0x01或者别的数据,即使再发送正常的三字节控制指令也不能响应了。因为错误的字节会占据串口接收数组的内容,导致预期接收的三字节指令出现偏移。

为解决这个问题,笔者将串口环形缓冲与接收DMA相结合,配合自定义的协议,设置指令帧的帧头格式,实现稳定可靠响应由串口接收到的控制指令。

环形缓冲区

环形缓冲区,也叫循环缓冲区、循环队列或 Ring Buffer,是计算机科学中一种非常常见的数据结构。它的核心特点是使用一块固定大小的连续存储空间,并将其逻辑上看作首尾相连,因此特别适合处理连续不断的数据流。环形缓冲区在初始时通常为空,数据写入后会按照先进先出的顺序被读取,也就是最早进入的数据最先被取出。需要注意的是,读取数据时往往只是移动读指针,被“读走”的内容并不会立刻从内存中消失,而是保留在原位置,直到后续被新数据覆盖。当缓冲区被写满后,如果继续写入新数据,就可能覆盖最旧的数据;当然,也有一些实现会选择禁止覆盖,并在缓冲区满时返回错误或抛出异常。是否允许覆盖,取决于具体的设计目标和应用场景。正因为它结构简单、效率高、非常适合流式数据处理,所以环形缓冲区被广泛应用于串口通信、音视频缓存、嵌入式系统和操作系统底层等场景。

CubeMX配置

将串口DMA接收配置改成为环形,当收到数据超过数组大小时,MCU会自动覆盖数组最开始的内容。

代码编写

帧格式

帧头0xAA,命令长度信息0x03(3字节),XX XX XX(3字节的命令)(可选)帧尾0x55

环形缓冲区结构体和初始化

环形缓冲区结构体主要包含指向缓冲数组的指针,头索引,尾索引等。

typedef struct { uint8_t *buffer; uint16_t head; uint16_t tail; uint16_t count; uint8_t overflow; } RingBuffer_t; void RingBuffer_Init(RingBuffer_t *ringBuffer) { ringBuffer->head = 0; ringBuffer->tail = 0; ringBuffer->count = 0; ringBuffer->overflow = 0; ringBuffer->buffer = buf; ringBuffer->count = 0; HAL_UARTEx_ReceiveToIdle_DMA(&huart1, ringBuffer->buffer, RINGBUFFER_SIZE); }

初始化函数负责将环形缓冲区变量赋值,设置串口空闲中断。

串口环形缓冲写入

缓冲区的写入由串口空闲中断完成,仅对空闲中断触发做处理,简化操作。当串口空闲时,头索引等于接收到的字节数。

uint8_t size; void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart == &huart1) { if (HAL_UARTEx_GetRxEventType(huart) == HAL_UART_RXEVENT_IDLE) { size = Size; ringBuffer.head = size % RINGBUFFER_SIZE; } } }

环形缓冲区读取

如果环形缓冲区是空,就直接返回,否则读取一个字节的内容,并使用指针传递出去。

uint8_t RingBuffer_Get(RingBuffer_t *ringBuffer, uint8_t *data) { if (RingBuffer_IsEmpty(ringBuffer)) { return 0; } *data = ringBuffer->buffer[ringBuffer->tail]; ringBuffer->tail = (ringBuffer->tail + 1) % RINGBUFFER_SIZE; ringBuffer->count--; return 1; }

在周期函数中读取缓冲区内容,寻找帧头,尝试匹配格式,判断是否出现控制指令。

char str[40]; void AC_cycle(void) { uint8_t data; RingBuffer_Get(&ringBuffer, &data); if (data == 0xAA) { RingBuffer_Get(&ringBuffer, &data); if (data == 0x03) { RingBuffer_Get(&ringBuffer, &AC_CMD_Receive[0]); RingBuffer_Get(&ringBuffer, &AC_CMD_Receive[1]); RingBuffer_Get(&ringBuffer, &AC_CMD_Receive[2]); AC_CMD_Request = 1; } } if (AC_CMD_Request == 1) { Normal_Code(AC_CMD_Receive[0], AC_CMD_Receive[1], AC_CMD_Receive[2]); sprintf((char*)str, "BLUE CMD: %02X %02X %02X\r\n", AC_CMD_Receive[0], AC_CMD_Receive[1], AC_CMD_Receive[2]); HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY); AC_CMD_Request = 0; display_new_cmd_flag = 1; display_showCMD(str); } }

蓝牙发送0x01 0x02 0x03 0x04 ,buf存放接收到的内容,头索引的值变成4

继续运行,由于没有检测到帧头,就不会执行红外发射。同时尾索引的值也等于头索引,这是在周期内不停读取环形缓冲的缘故。此时环形缓冲区为空。

发送正常指令,例如0xAA 0x03 0xFF 0xFF 0xFF

数组第一个元素被覆盖,头索引变成1,而尾索引还停留在上次的4。对环形缓冲区而言,多了从4到7这4个元素(0xAA 0x03 0xFF 0xFF)还有0(0xFF)。 执行完RingBuffer_Get函数,尾索引++

data是0xAA,接着读取环形缓冲区下一个元素。

将剩余指令全部取出。

如果蓝牙发送0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09共9字节的数据,数组溢出,第一个元素变成0x09,头索引的值变成1

此时再发送正常指令,例如0xAA 0x03 0xFF 0xFF 0xFF,也可以正常响应,因为周期函数会一直读取环形缓冲,尾索引也变成1,环形缓冲区再次为空……

结语

经过测试,使用环形缓冲和帧格式后,系统的鲁棒性大大提高,无效数据得以滤除,真正有效的指令信息可以被稳定执行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询