You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 09:39:17