在使用STM32单片机串口接收不固定长度数据包时,采用了每个字节中断,然后接收存储的方式,前面一直会有错误,尤其是波特率比较高(可能相对于该款单片机的主频,其他中断任务过多),甚至到后面没法接收到数据的情况。所以,修改整理了串口处理流程如下,以备后面需要时套用框架:
1 初始化
...
MX_USART4_UART_Init();
if (huart4.gState == HAL_UART_STATE_READY)HAL_UART_Receive_IT(&huart4, &rec_ch_uart4,1);
_cdcCmd.rawVal=0;
...
static void MX_USART4_UART_Init(void)
{
huart4.Instance = USART4;
huart4.Init.BaudRate = 57600;
huart4.Init.WordLength = UART_WORDLENGTH_8B;
huart4.Init.StopBits = UART_STOPBITS_1;
huart4.Init.Parity = UART_PARITY_NONE;
huart4.Init.Mode = UART_MODE_TX_RX;
huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart4.Init.OverSampling = UART_OVERSAMPLING_16;
huart4.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart4.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart4.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart4) != HAL_OK)
{
Error_Handler();
}
}
MX_USART4_UART_Init(void)是设置后参数后,CubeMX自动生成的
2 错误处理中断回调函数重载
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART4) {
if (huart->ErrorCode & HAL_UART_ERROR_ORE) {
// 处理溢出错误
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_OREF);
}
if (huart->ErrorCode & HAL_UART_ERROR_FE) {
// 处理帧错误
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_FEF);
}
HAL_UART_AbortReceive(&huart4); // 重启接收
HAL_UART_Receive_IT(&huart4, &rec_ch_uart4,1);
}
}
3 接收中断回调重载
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if(huart->Instance == USART4){
if(rec_counter_uart4<MAX_LEN_USART4){
Uart4_RxBuffer[rec_counter_uart4]=rec_ch_uart4;
rec_counter_uart4++;
usartIdleCounter=UsartIdleMax;
}
if (huart4.gState == HAL_UART_STATE_READY) HAL_UART_Receive_IT(&huart4, &rec_ch_uart4,1);
}
}
保存收到的字节到缓存区,重启接收(1字节中断)
usartIdleCounter计数变成10(或20)ms
4 ms中断处理(systick)
void _embed_ms_loop()
{
msCounter++;
if(msCounter>=1000)
{
if(TTL)
{
_cdcCmd.bits.ReportStateCmd=1;
TTL--;
}else{
TTL=20; //Only for debug
}
msCounter=0;
}
if(usartIdleCounter)
{
usartIdleCounter--;
}else{
_cdcCmd.bits.PackageReady=1;
}
}
如果usartIdleCounter减到0,说明有段时间没有收到字节了,可以处理缓存区里的包
5 主循环
void baidu_interface_embed_main_loop()
{
if(_cdcCmd.bits.PackageReady)
{
parseCDCPackage();
_cdcCmd.bits.PackageReady=0;
}
if(_cdcCmd.bits.SetExtrasCmd)
{
SetSurcharge();
_cdcCmd.bits.SetExtrasCmd=0;
}
if(_cdcCmd.bits.ReportStateCmd)
{
toCDC_reportState();
_cdcCmd.bits.ReportStateCmd=0;
}
if(_cdcCmd.bits.SimpleRspCmd)
{
Respond2Cmd(RspCmd,RspErrCode);
_cdcCmd.bits.SimpleRspCmd=0;
}
if(_cdcCmd.bits.SetRtcCmd)
{
rtc_set();
_cdcCmd.bits.SetRtcCmd=0;
}
}
所有任务主体都在主循环里处理,中断里面只是做些命令置位、清零、缓存、计数等动作以避免长时间在中断内执行。
解析包子函数
void parseCDCPackage()
{
uint8_t packageLen;
if( rec_counter_uart4>6) //7 is the least length of a package
{
if((Uart4_RxBuffer[0]==0xFF) && (Uart4_RxBuffer[1]==0xCD)) //Correct Header and device
{
packageLen=Uart4_RxBuffer[3]+7;
if(rec_counter_uart4>=packageLen)// enough length.
{
//copy package
memcpy(packageBuff,(unsigned char*)(&Uart4_RxBuffer[2]),Uart4_RxBuffer[3]+2);
//check CRC and tail byte
uint8_t ucReadCRC_L=Uart4_RxBuffer[Uart4_RxBuffer[3]+4];
uint8_t ucReadCRC_H=Uart4_RxBuffer[Uart4_RxBuffer[3]+5];
uint16_t ReadCRC = ((uint16_t)ucReadCRC_H<< 8)+ ucReadCRC_L;
uint16_t calc_CRC=Get_CRC(packageBuff,Uart4_RxBuffer[3]+2);
if(calc_CRC==ReadCRC && Uart4_RxBuffer[Uart4_RxBuffer[3]+6]==0xEF)
{
UseCOMData(packageBuff,Uart4_RxBuffer[3]+2);//apply the command;
}
}
}
rec_counter_uart4=0; //clear buffer
}
else
{
HAL_UART_AbortReceive(&huart4); // 重启接收
HAL_UART_DeInit(&huart4);
HAL_UART_Init(&huart4);
rec_counter_uart4=0; //clear buffer
HAL_UART_Receive_IT(&huart4, &rec_ch_uart4,1);
}
usartIdleCounter=250;
}
解析完一个包后,把usartIdleCounter设置为大些(根据需要),那么长时间接收不到包,也会重启串口模块。
DMA改进
📍方法 1:DMA + 串口空闲中断 (IDLE Line Interrupt) - 最常用
原理:
- 使能 USART/UART 的空闲中断 (IDLE Line Detection)。当 RX 线持续空闲(高电平)超过 1 个完整字符传输时间(包括停止位)后,硬件会触发空闲中断。
- 配置 DMA 在循环模式 (Circular Mode) 下接收数据到一个足够大的缓冲区。
- DMA 会在后台持续接收数据到缓冲区,覆盖旧数据(循环缓冲)。
- 当一帧数据接收完毕,RX 线进入空闲状态,触发空闲中断。
- 在空闲中断服务程序 (ISR) 中:
- 计算本次接收到的数据长度:Length = BufferSize -DMA_GetRemainingDataCount(DMA_Stream);
- 处理接收到的完整一帧数据(复制到其他缓冲区、设置标志位通知主循环等)。
- 清除空闲中断标志位。
优点:
- 高效: CPU 参与度极低,仅在帧结束时处理一次中断。
- 实时性好: 帧结束检测及时。
- 硬件支持: 利用 STM32 内置硬件特性,实现简单可靠。
- 非常适合不定长帧: 不依赖特定帧头帧尾或超时。
缺点:
- 需要芯片的 USART/UART 支持空闲中断检测(绝大多数 STM32 都支持)。
- 需要足够大的 DMA 缓冲区以防止在高波特率或密集数据时被覆盖。
- 连续快速发送帧时,如果处理不及时,缓冲区可能会被覆盖(需合理设计缓冲区大小和处理速度)。
实现步骤 (以 HAL 库为例):
// 1. 定义足够大的循环缓冲区
#define RX_BUFFER_SIZE 256
uint8_t RxBuffer[RX_BUFFER_SIZE];
// 2. 初始化 UART 和 DMA (循环模式)
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
// ... (配置 UART 参数: 波特率, 数据位, 停止位, 校验位等)
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); }
// ... (配置 DMA 通道和流/通道,关联到 UART RX)
__HAL_RCC_DMA1_CLK_ENABLE(); // 根据实际 DMA 和 UART 使能时钟
hdma_usart1_rx.Instance = DMA1_Stream5; // 根据芯片手册选择正确的 Stream/Channel
hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR; // 关键!循环模式
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK) { Error_Handler(); }
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx); // 关联 DMA 到 UART
// 3. 使能 UART 的空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使用 HAL 库的宏
// 4. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, RxBuffer, RX_BUFFER_SIZE);
// 5. 编写空闲中断服务函数 (通常在 stm32..._it.c 中)
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1); // 调用 HAL 的通用处理函数
// 或者直接判断中断标志:
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲中断标志!非常重要!
// 计算接收到的数据长度
uint16_t receivedLength = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
// 处理接收到的数据 (receivedLength 字节, 起始位置需要计算)
// 注意:在循环缓冲区中,数据可能不是从 RxBuffer[0] 开始的!
uint16_t dma_current_index = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
uint16_t start_index = (dma_current_index - receivedLength) % RX_BUFFER_SIZE; // 计算起始索引
// 处理从 RxBuffer[start_index] 开始的 receivedLength 字节数据...
// 例如:复制到应用层缓冲区 (memcpy),设置标志通知主循环等。
// 注意: DMA 会继续在循环缓冲区中接收,无需重启 DMA
}
}
关键点:
- 清除空闲中断标志 (__HAL_UART_CLEAR_IDLEFLAG()) 是必须的。
- 计算 receivedLength 和确定数据在循环缓冲区中的起始位置 (start_index) 是核心。
- 使用 % RX_BUFFER_SIZE 处理循环缓冲区的环绕。
📍 方法 2:DMA + 接收超时中断 (Receiver Timeout Interrupt - RTO) - (较新系列如 G0, G4, L4+, H5, H7)
原理:
- 使能 USART 的接收超时中断 (RTO)。RTO 在连续两个字符的起始位之间的时间间隔超过可编程阈值时触发。
- 配置 DMA 在普通模式 (Normal Mode) 或循环模式下接收。
- 当帧内字符间隔超过设定时间,触发 RTO 中断。
- 在 RTO ISR 中计算接收到的数据长度并处理帧(类似 IDLE 中断)。
- 如果是普通模式 DMA,在 RTO ISR 中需要重新启动 DMA 接收。
优点:
- 比 IDLE 更灵活: 超时时间可编程,适应不同协议。
- 避免噪声误触发: IDLE 要求总线严格高电平,RTO 只关心字符间隔。
- 同样高效。
缺点:
- 仅支持较新的 STM32 系列 (G0, G4, L4+, H5, H7 等)。
- 需要配置超时时间 (RTO 寄存器)。
- 普通模式 DMA 需要重启。
实现要点:
// 启用 RTO 中断
SET_BIT(huart->Instance->CR2, USART_CR2_RTOEN); // 使能 RTO
SET_BIT(huart->Instance->CR1, USART_CR1_RTOIE); // 使能 RTO 中断
// 设置 RTO 值 (例如 3.5 个字符时间: 3.5 * (1 + 8 + 1) / BaudRate)
uint32_t rto_value = ...; // 根据波特率和所需超时计算
MODIFY_REG(huart->Instance->RTOR, USART_RTOR_RTO, rto_value);
// RTO 中断服务程序 (USARTx_IRQHandler) 中:
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_RTOF) != RESET) {
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_RTOF);
// 计算接收长度 (如果是普通模式,长度就是设定的DMA大小减去剩余计数;循环模式计算同IDLE)
uint16_t receivedLength = ...;
// 处理数据...
if (huart->hdmarx->Init.Mode == DMA_NORMAL) {
HAL_UART_Receive_DMA(huart, RxBuffer, RX_BUFFER_SIZE); // 重启 DMA
}
}
STM32单片机串口数据接收处理框架整理

1万+

被折叠的 条评论
为什么被折叠?



