[STM32WL] 串口 DMA 接收长时间运行后出现数据丢失

[复制链接]
182|3
是你的乱码 发表于 2025-10-31 01:19 | 显示全部楼层 |阅读模式
通过增大环形缓冲区仅能缓解问题,确实很可能与 DMA 中断响应不及时、缓冲区管理机制或硬件时序冲突有关。以下是更深入的原因分析和针对性解决方案:
一、深层原因剖析(结合硬件特性)
DMA 中断被 “饿死”当系统中存在高优先级中断(如定时器中断、SPI 中断)且其处理时间过长时,会持续阻塞 DMA 接收中断。例如:若一个高优先级中断每次占用 100μs,而串口以 115200bps(约 10μs / 字节)连续发送数据,仅需 10 个字节就会导致 DMA 缓冲区溢出。
环形缓冲区 “读写不同步”即使使用环形缓冲区,若主循环读取速度慢于 DMA 写入速度,会导致 “写指针追上读指针”,覆盖未处理数据。增大缓冲区只能延迟这一时刻,无法根治。
串口硬件 FIFO 溢出STM32 的 UART 外设通常有 1 字节或 4 字节硬件 FIFO(因型号而异)。当 DMA 传输滞后时,新数据会覆盖硬件 FIFO 中的旧数据,触发ORE(溢出)标志,导致数据永久丢失。

 楼主| 是你的乱码 发表于 2025-10-31 01:19 | 显示全部楼层
解决方案(分层次优化)
1. 中断优先级与响应速度优化
严格的中断优先级分级:确保 DMA 接收中断优先级高于所有非实时性中断(如用户按键、普通 GPIO 中断),但低于系统级故障中断(如 HardFault):
c
运行
// 在NVIC配置中明确优先级
void MX_NVIC_Init(void)
{
  // DMA接收中断设为较高优先级(数值越小优先级越高)
  HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn);
  
  // 其他中断(如定时器)设为更低优先级
  HAL_NVIC_SetPriority(TIM2_IRQn, 3, 0);
  HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
中断服务程序(ISR)“零耗时” 改造:中断中仅记录数据位置,不做任何处理(包括打印、校验等):
c
运行
volatile uint16_t dma_current_pos = 0;  // DMA当前写入位置
volatile uint8_t dma_half_complete = 0; // 半缓冲标志
volatile uint8_t dma_full_complete = 0; // 全缓冲标志

// DMA半传输完成回调(仅设置标志)
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1) {
    dma_current_pos = DMA_BUF_SIZE / 2;
    dma_half_complete = 1;
  }
}

// DMA全传输完成回调(仅设置标志)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1) {
    dma_current_pos = DMA_BUF_SIZE;
    dma_full_complete = 1;
  }
}
2. 双缓冲区 + DMA 循环模式(彻底解决覆盖问题)
采用 “Ping-Pong” 双缓冲机制,DMA 在两个缓冲区间交替传输,主循环处理完一个缓冲区再处理另一个,避免覆盖:
c
运行
#define BUF_SIZE 1024
uint8_t dma_buf[2][BUF_SIZE];  // 双缓冲区
uint8_t current_buf = 0;       // 当前活跃缓冲区

// 初始化DMA双缓冲模式
void uart_dma_init(void)
{
  // 配置DMA为循环模式,首次启动填充第一个缓冲区
  HAL_UART_Receive_DMA(&huart1, dma_buf[0], BUF_SIZE);
  
  // 使能半传输和全传输中断(关键)
  __HAL_DMA_ENABLE_IT(&hdma_usart1_rx, DMA_IT_HT);  // 半传输中断
  __HAL_DMA_ENABLE_IT(&hdma_usart1_rx, DMA_IT_TC);  // 全传输中断
}

// 主循环中处理缓冲区数据
void process_uart_data(void)
{
  // 半缓冲完成:处理第一个半区
  if (dma_half_complete) {
    process_buffer(dma_buf[current_buf], BUF_SIZE / 2);
    dma_half_complete = 0;
  }
  
  // 全缓冲完成:切换缓冲区并处理第二个半区
  if (dma_full_complete) {
    process_buffer(&dma_buf[current_buf][BUF_SIZE / 2], BUF_SIZE / 2);
    current_buf ^= 1;  // 切换缓冲区(0 <-> 1)
    dma_full_complete = 0;
  }
}

// 实际数据处理函数(在主循环中执行,可耗时)
void process_buffer(uint8_t *buf, uint16_t len)
{
  // 处理数据(如解析、存储等)
  for (uint16_t i = 0; i < len; i++) {
    // 业务逻辑...
  }
}
3. 硬件溢出检测与自动恢复
STM32 的 UART 溢出(ORE)标志会导致后续数据无法接收,需主动检测并复位:
c
运行
void uart_check_overflow(void)
{
  if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE)) {
    // 清除溢出标志(必须先读SR再写DR,否则无法清除)
    __HAL_UART_CLEAR_OREFLAG(&huart1);
   
    // 重启DMA接收(关键步骤)
    HAL_UART_AbortReceiveDMA(&huart1);
    HAL_UART_Receive_DMA(&huart1, dma_buf[current_buf], BUF_SIZE);
   
    // 记录溢出次数(用于调试)
    static uint32_t overflow_cnt = 0;
    overflow_cnt++;
  }
}

// 在main函数中定期调用(建议每10ms至少一次)
while (1) {
  uart_check_overflow();
  process_uart_data();
  // 其他业务...
}
4. 系统级优化(针对高负载场景)
降低 CPU 负载:若主循环中有耗时操作(如复杂计算、大量 printf),将其放入低优先级线程(如使用 FreeRTOS),确保数据处理线程优先执行:
c
运行
// FreeRTOS示例:创建高优先级数据处理任务
xTaskCreate(
  uart_data_task,    // 任务函数
  "UART_Process",    // 任务名
  512,               // 栈大小
  NULL,              // 参数
  3,                 // 高优先级(高于普通任务)
  NULL               // 任务句柄
);
调整 DMA 传输参数:若串口波特率极高(如 2Mbps 以上),可减小 DMA 缓冲区粒度(如将 1024 字节改为 256 字节),让中断更频繁但每次处理数据量更小,降低单次处理延迟。
 楼主| 是你的乱码 发表于 2025-10-31 01:19 | 显示全部楼层
调试验证方法
用逻辑分析仪抓波形:将串口 RX 引脚与 DMA 中断引脚(如 PA0)连接到逻辑分析仪,对比发送数据量、中断触发次数和实际接收量,定位丢失发生的精确时刻。
统计 DMA 传输计数器:在中断中记录 DMA 的NDTR(剩余传输数)寄存器值,计算实际传输字节数,判断是否与发送方一致:
c
运行
// 在DMA回调中获取传输字节数
uint16_t transferred = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
压力测试:让发送方连续发送已知图案(如 0x00~0xFF 循环),运行数小时后检查接收数据是否有断点或重复,快速定位丢失规律。
通过以上措施,可从 “中断响应 - 缓冲区管理 - 硬件容错” 三个层面解决数据丢失问题。核心是确保 DMA 中断不被长期阻塞,同时通过双缓冲机制实现 “边接收边处理”,彻底避免数据覆盖。
szt1993 发表于 2025-10-31 23:04 | 显示全部楼层
通过双缓冲机制实现 “边接收边处理”
您需要登录后才可以回帖 登录 | 注册

本版积分规则

39

主题

525

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部