STM32接收MQTT大数据写入QSPI Flash时UART丢包如何解决
问题根因
丢包核心原因是你在UART DMA接收的中断回调里执行了阻塞式QSPI写入。
常规外挂QSPI NOR Flash单页(多为256/512字节)编程有1~5ms的固定硬件耗时,BSP默认提供的BSP_QSPI_Write是轮询阻塞实现,会一直等到Flash编程完成才返回。这段时间UART外设还在持续接收SIM7600发来的数据,DMA缓冲区被填满后新到的字节会触发UART溢出错误,后续数据无法存入缓冲区,next_size自然停止增长,最终表现为只能收到前512字节数据。
当前代码还有两个会加剧丢包的隐患:
- 中断回调里调用了
printf,串口打印本身是阻塞操作,输出字符串的耗时同样会阻塞中断响应 - 重启DMA前没有检查、清除UART溢出、帧错误标志,一旦出现过溢出错,DMA会直接停止接收,再也不会触发回调
修复方案
核心原则:中断上下文只允许跑微秒级的快速操作,所有毫秒级耗时逻辑全部移到主循环处理。
1. 改造UART接收为无阻塞逻辑
- 预分配一块环形接收缓冲区(大小设为2~4KB即可,远小于128KB RAM上限,足够缓存UART突发数据)。
- 接收回调里只做三个操作:把本次DMA收到的
Size长度数据快速拷贝到环形缓冲区、更新写指针、清除UART错误标志后重启DMA接收,整个过程耗时不超过10us,完全不会丢数据。 - 删掉中断回调里所有QSPI写入、printf类的阻塞操作。
2. 主循环轮询写入QSPI
- 主循环里持续检查环形缓冲区的待读取数据长度,凑够QSPI Flash单页大小(256或512字节,和所用Flash芯片参数对齐)时,读取一页数据写入外部Flash,更新Flash写入地址和环形缓冲区读指针。
- 等全部MQTT数据接收完成后,再把环形缓冲区里不足一页的尾数据写入Flash即可。
- 若要进一步提升效率,可以把QSPI也配置为DMA写入模式,发起写操作后主循环无需轮询等待Flash编程耗时,等QSPI DMA完成中断触发后再标记可写下一页即可。
参考实现代码
// 环形缓冲区配置,可根据RAM余量调整大小 #define RING_BUF_SIZE 4096 #define QSPI_PAGE_SIZE 512 uint8_t ring_buf[RING_BUF_SIZE]; volatile uint32_t ring_write_ptr = 0; volatile uint32_t ring_read_ptr = 0; uint8_t rx_buf[QSPI_PAGE_SIZE * 2]; // UART DMA接收缓冲区,大小设为2倍QSPI页即可 const uint32_t rx_buf_size = sizeof(rx_buf); // 环形缓冲区写入工具函数 uint32_t ring_buf_push(uint8_t *src, uint32_t len) { uint32_t free_len = RING_BUF_SIZE - ((ring_write_ptr - ring_read_ptr) & (RING_BUF_SIZE - 1)); len = len > free_len ? free_len : len; // 防止缓冲区溢出 for(uint32_t i = 0; i < len; i++) { ring_buf[ring_write_ptr & (RING_BUF_SIZE - 1)] = src[i]; ring_write_ptr++; } return len; } // UART接收事件回调(中断上下文,仅做快速操作) void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == UART4) { // 快速拷贝数据到环形缓冲区 uint32_t written = ring_buf_push(rx_buf, Size); prev_size += written; // 清除UART错误标志,避免DMA死锁 __HAL_UART_CLEAR_OREFLAG(huart); __HAL_UART_CLEAR_FEFLAG(huart); __HAL_UART_CLEAR_NEFLAG(huart); // 重启DMA接收 HAL_UARTEx_ReceiveToIdle_DMA(&huart4, rx_buf, rx_buf_size); __HAL_DMA_DISABLE_IT(&hdma_uart4_rx, DMA_IT_HT); } } // 主循环逻辑 int main(void) { // 省略硬件初始化、MQTT连接、QSPI初始化、Flash擦除流程 uint8_t flash_write_buf[QSPI_PAGE_SIZE]; uint32_t qspi_write_addr = WRITE_READ_ADDR; // 启动第一次UART DMA接收 HAL_UARTEx_ReceiveToIdle_DMA(&huart4, rx_buf, rx_buf_size); __HAL_DMA_DISABLE_IT(&hdma_uart4_rx, DMA_IT_HT); while(1) { uint32_t avail_len = (ring_write_ptr - ring_read_ptr) & (RING_BUF_SIZE - 1); // 凑够整页数据就写入Flash if(avail_len >= QSPI_PAGE_SIZE) { // 从环形缓冲区读取一页数据 for(uint32_t i = 0; i < QSPI_PAGE_SIZE; i++) { flash_write_buf[i] = ring_buf[ring_read_ptr & (RING_BUF_SIZE - 1)]; ring_read_ptr++; } // 写入QSPI Flash if(BSP_QSPI_Write(flash_write_buf, qspi_write_addr, QSPI_PAGE_SIZE) == QSPI_OK) { qspi_write_addr += QSPI_PAGE_SIZE; } } // 判定MQTT大包接收完成后,写入剩余不足一页的数据 if(mqtt_recv_done_flag) { uint32_t remain_len = (ring_write_ptr - ring_read_ptr) & (RING_BUF_SIZE - 1); if(remain_len > 0) { for(uint32_t i = 0; i < remain_len; i++) { flash_write_buf[i] = ring_buf[ring_read_ptr & (RING_BUF_SIZE - 1)]; ring_read_ptr++; } BSP_QSPI_Write(flash_write_buf, qspi_write_addr, remain_len); } mqtt_recv_done_flag = 0; } // 其他业务逻辑 } }
注意事项
- QSPI写入前必须按扇区/块擦除对应地址范围的Flash,否则会出现写入数据错误。
- 如果UART波特率设置较高(如921600及以上),可以把环形缓冲区扩大到8KB,或者开启UART DMA双缓冲模式,进一步降低数据溢出风险。
- 日志打印全部移到主循环执行,不要在任何中断回调里调用阻塞式打印函数。
内容的提问来源于stack exchange,提问作者dnyaneshvar salve
相关产品推荐
相关产品推荐

