STM32串口DMA連續發送兩幀,導致數據部分覆蓋的問題

問題描述

使用STM32的串口進行DMA發送(Noraml模式),在某個任務中連續調用兩次發送函數log_printf(),但是發回的數據在串口調試助手上顯示與預期不符。第一次發送的數據有一部分被第二次發送的數據覆蓋,如圖所示:
失敗
任務代碼如下:

/* Log_Task function */
void Log_Task(void const * argument)
{
  /* USER CODE BEGIN Log_Task */
  /* Infinite loop */
  for(;;)
  {
      if(router_rx_flag == 1)
      {
          router_rx_flag = 0;
          log_printf("Get ok\r\n");
          log_printf("%s",router_rx_buffer);
      }
    osDelay(100);
  }
  /* USER CODE END Log_Task */
}

從代碼中可以看出,期望的結果應該是下圖這樣:

成功


log_printf函數代碼如下:

/* 
 * 名稱: log_printf
 * 功能: 在串口1上打印出日誌內容
 * 輸入: 格式化輸出的字符串
 * 輸出: 無
 */
void log_printf(const char *format ,... )
{
    va_list arg;
    static char tx_buffer[256]={""};

    //把數據處理後放進緩衝區
    va_start(arg, format);
    vsprintf((char *)tx_buffer, format, arg);
    va_end(arg);

    //開始發送數據
    send_to_router((u8 *)tx_buffer,strlen(tx_buffer));
}

send_to_router函數代碼如下:

void send_to_router(unsigned char *buffer,unsigned int length)
{
    //等待上一次的數據發送完畢
    while(HAL_DMA_GetState(&hdma_usart1_tx) == HAL_DMA_STATE_BUSY)  osDelay(1);

    /* 關閉DMA */
    __HAL_DMA_DISABLE(&hdma_usart1_tx);

    //開始發送數據
    HAL_UART_Transmit_DMA(&huart1,buffer,length);
    //  while(HAL_DMA_GetState(&hdma_usart1_tx) == HAL_DMA_STATE_BUSY)  osDelay(1); /* 放在此處可以保證每次發送完全,但會佔用時間 */
}

串口中斷接收處理函數如下:

/* 
 * 名稱: router_parse
 * 功能: 接收路由器數據的解析,在回調函數中調用
 * 輸入: 空閒中斷時串口1接收的數據長度
 * 輸出: 無
 */
void router_parse(uint16_t buffer_len)
{
    char *p_start = NULL,*p_end = NULL;

    /* 只提取一幀NMEA數據,$開頭,\n結尾 */
    p_start = strchr(usart1_rx_buffer,'$');
    if(p_start != NULL)
    {
        p_end = strchr(p_start,'\n');
        if(p_end != NULL)
        {
            memcpy(router_rx_buffer, p_start, (p_end - p_start + 1)); /* 保存數據 */
            router_rx_flag = 1;
        }
    }
}

分析過程

以前一直以爲是send_to_router函數中的

    //等待上一次的數據發送完畢
    while(HAL_DMA_GetState(&hdma_usart1_tx) == HAL_DMA_STATE_BUSY)  osDelay(1);

這一句的問題,即由於某種原因導致DMA緩存中數據未發送完全,但DMA狀態卻被釋放了,結果重新開始了新一輪的發送,導致上次數據的後半部分被覆蓋。但無論如何調試,都無法證實這個猜想,DMA外設沒有出過任何異常。
今天仔細觀察了一下,“Getckey”和“Get ok\r\n”和”$Mickey\r\n“,爲什是第二次發送的內容的後半部覆蓋了第一次發送的內容,一般不應該是前半部分”(美元符號,此處會排版出錯)Mic”嗎?問題的原因可能與狀態位無關。於是我再審視了一下send_to_router函數:void send_to_router(unsigned char *buffer,unsigned int length)突然間想到,入參只是一個指針,發送緩存區在log_printf函數中

static char tx_buffer[256]={""};

整理一下,整個發送過程流程如下:

  1. log_printf(“Get ok\r\n”);時,“Get ok\r\n”被裝進了tx_buffer,附帶一個發送長度8字節。
  2. send_to_router函數中,HAL_UART_Transmit_DMA(&huart1,buffer,length);開啓了這個8個字節的發送。
  3. 8個字節可能只完成了“Get”的發送, log_printf(“%s”,router_rx_buffer);(即log_printf(“$Mickey\r\n“);)已經開始執行。
  4. $Mickey\r\n“被裝進tx_buffer,附帶一個發送長度9字節。
  5. send_to_router函數中,因爲上一次數據還沒有發送完全,進入DMA狀態等待循環。但是DMA發送指針char *buffer原本指向的那個地址的內容” ok\r\n“已經被”ckey\r\n“代替,所以就變成了”Getckey\r“。由於顯示原因,只看到”Getckey“。

解決辦法

while(HAL_DMA_GetState(&hdma_usart1_tx) == HAL_DMA_STATE_BUSY) osDelay(1);這一句放到緩存區tx_buffer裝載步驟之前即可:

/* 
 * 名稱: log_printf
 * 功能: 在串口1上打印出日誌內容
 * 輸入: 格式化輸出的字符串
 * 輸出: 無
 */
void log_printf(const char *format ,... )
{
    va_list arg;
    static char tx_buffer[256]={""};

    //等待上一次的數據發送完畢
    while(HAL_DMA_GetState(&hdma_usart1_tx) == HAL_DMA_STATE_BUSY)  osDelay(1);

    //把數據處理後放進緩衝區
    va_start(arg, format);
    vsprintf((char *)tx_buffer, format, arg);
    va_end(arg);

    //開始發送數據
    send_to_router((u8 *)tx_buffer,strlen(tx_buffer));
}

至於send_to_router函數中的該代碼,保留或刪除都可以。

後言

很久以前就開始使用STM32的DMA串口發送功能,套路基本上就是曾經的博文《iar中使用DMA+printf+uart1》所描述的那樣。後來開始用STM32CubeMX了,把之前的例程稍微做了一些修改,調試成功之後,就一直沿用至今。期間,這個問題困擾了我很久,雖然在寫代碼時稍微注意一下就可避免其發生,但做技術的人都明白:千里之堤,潰於螻蟻,放過任何一個小細節都可能在將來引發重大災難。很慶幸今天能夠找到問題的原因。
再回去看來一遍《iar中使用DMA+printf+uart1》,其實這個問題的答案很早就寫在裏面了。。。
找個時間,我會專門寫一篇使用DMA串口Normal模式發送的博文,還是以Cube來創建工程。屆時,再用一個例程完整復現和解決這個問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章