目录
在嵌入式系统中,高效处理串口数据是开发者面临的常见挑战,本博客为学习记录贴,记录stm32f1x芯片的串口环形缓冲区管理设计模式。
一、背景
串口通信面临两个常见问题:
-
数据不定长:接收的数据包长度未知
-
实时性要求:需要快速响应避免数据丢失
目前常用的设计通常结合DMA传输和IDLE中断,配合缓冲区管理机制来解决这两个问题。
二、程序示例
1、串口配置
首先,先配置串口、DMA和空闲中断。
void Uart1_Config(uint32_t baudrate) //先配置但不开启USART_Cmd
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = UART1_TX_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(UART1_TX_POART, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //有外部上拉就选浮空,没有就选上拉
GPIO_InitStructure.GPIO_Pin = UART1_RX_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(UART1_RX_POART, &GPIO_InitStructure);
USART_DeInit(USART1);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = baudrate;
USART_InitStructure.USART_HardwareFlowControl = DISABLE;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
/*开启串口中断*/
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
//配置串口总中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
void Uart1_DMA_Config(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //***********需要开启dma时钟
DMA_DeInit(DMA1_Channel5);
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)Usart1_RxBuffer;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; //配合idle一般使用循环模式
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)(USART1->DR); //数据寄存器
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh; //设置为高优先级
DMA_InitStruct.DMA_BufferSize = UARTx_RX_SIZE + 1; //*************注意此处只使用IDLE控制不使用DMA的TC控制,因此+1
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; //外设到内存
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; //选用硬件触发
DMA_Init(DMA1_Channel5, &DMA_InitStruct);
#if defined(DMA_INTERRUPT)
/*开启DMA中断*/
DMA_ITConfig(DMA1_Channel5, DMA_IT_TC | DMA_IT_HT | DMA_IT_TE, ENABLE);
//配置DMA总中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn; //需要去参考手册查
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
#endif
DMA_Cmd(DMA1_Channel5, ENABLE);
}
void Uart1_start(void)
{
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
USART_Cmd(USART1, ENABLE);
}
uint16_t get_datalen(void)
{
return (UARTx_RX_SIZE + 1) - DMA_GetCurrDataCounter(DMA1_Channel5); //注意为配合DMA的 counter 设置此处为 UARTx_RX_SIZE + 1
}
void dma_setcounter_setmxar(void)
{
DMA_SetCurrDataCounter(DMA1_Channel5, UARTx_RX_SIZE + 1); //***********重新设置dma转运大小
DMA1_Channel5->CMAR = (uint32_t)(U1CB.pURxDataIn->pStart); //***********重新设置起始地址
DMA_Cmd(DMA1_Channel5, ENABLE);
}
关键设计:此处DMA设计为 DMA_InitStruct.DMA_BufferSize = UARTx_RX_SIZE + 1,也就是只用idle中断控制,不会使用dma_tc中断。
核心问题分析
问题1:缓冲区满/空状态歧义
场景:
使用标准缓冲区大小2048:
-
初始状态:计数器=2048
-
接收2048字节后:计数器=0 → 自动重置为2048
歧义:
计数器=2048可能表示:
-
初始状态(缓冲区空)
-
缓冲区满后重置的状态
无法区分这两种状态!
问题2:长度计算失效
当接收2048字节时:
received = initial_counter - current_counter; // 2048 - 2048 = 0 ❌ (应为2048)
问题3:边界条件处理
传统方案在以下场景失效:
-
连续接收多个满缓冲区
-
缓冲区回绕处理
-
错误恢复机制
2、数据结构
#define NUM 10 // 支持10个缓存块
#define UARTx_RX_SIZE 2048
uint8_t Usart1_RxBuffer[UARTx_RX_SIZE]; //物理缓冲区
typedef structure
{
uint8_t *pStart;
uint8_t *pEnd;
}UCB;
typedef structure
{
uint16_t URxtotalReceived; //累计接收数据量
UCB URxDataPtr[NUM]; //se指针对 结构体数组
UCB *pURxDataIn; //IN指针用于标记接收数据
UCB *pURxDataOut; //OUT指针用于提取接收的数据
UCB *pURxDataEnd; //IN和OUT指针的结尾标志
}Usart_Rx_Control_Block;
Usart_Rx_Control_Block U1CB;
将物理缓冲区分为Usart1_RxBuffer[UARTx_RX_SIZE]分为NUM个逻辑分块。
- 物理缓冲区:2048字节大缓冲区
- 逻辑分块:10个数据块(NUM=10)
优势:允许应用程序在处理前一个数据块时,DMA继续接收新数据
3、初始化
//初始化U1CB
void U1Rx_PtrInit(void)
{
U1CB.pURxDataIn = URxDataPtr[0];
U1CB.pURxDataOut = URxDataPtr[0];
U1CB.pURxDataEnd = URxDataPtr[NUM-1];
U1CB.URxtotalReceived = 0;
U1CB.URxDataPtr->pStart = Usart_RxBuffer;
}
4、串口中断
#define UARTx_RX_MAX 1029 //单次接收最大量
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
//清除标志位
USART1->SR; //清中断
USART1->DR; //清中断
U1CB.URxtotalReceived += get_datalen(); //获取本次接收数据长度
U1CB.pURxDataIn->pEnd = &Usart_RxBuffer[U1CB.URxtotalReceived - 1]; //修改pEnd指针
if(++ U1CB.pURxDataIn == U1CB.pURxDataEnd)
{
U1CB.pURxDataIn = URxDataPtr[0];
}
if(UARTx_RX_SIZE - U1CB.URxtotalReceived >= UARTx_RX_MAX)
{
U1CB.pURxDataIn->pStart = &Usart_RxBuffer[U1CB.URxtotalReceived];
}
else
{
U1CB.pURxDataIn->pStart = Usart_RxBuffer;
U1CB.URxtotalReceived = 0;
}
dma_setcounter_setmxar(); //设置dma counter 和 dma数组start地址
}
}
5、获取数据
void process_data(void)
{
if(U1CB.pURxDataIn != U1CB.pURxDataOut) //表明有数据
{
uint16_t Lenth = U1CB.pURxDataOut->pEnd - U1CB.pURxDataOut->pStart; //获取数据长度
/*
处理数据
...
*/
U1CB.pURxDataOut ++;
if(U1CB.pURxDataOut == U1CB.pURxDataEnd)
{
U1CB.pURxDataOut = &URxDataPtr[0];
}
}
}