STM32之串口DMA接收不定長數據

  • 2019 年 10 月 16 日
  • 筆記

STM32之串口DMA接收不定長數據

引言

在使用stm32或者其他單片機的時候,會經常使用到串口通訊,那麼如何有效地接收數據呢?假如這段數據是不定長的有如何高效接收呢?

同學A:數據來了就會進入串口中斷,在中斷中讀取數據就行了!

中斷就是打斷程式正常運行,怎麼能保證高效呢?經常把主程式打斷,主程式還要不要運行了?

同學B:串口可以配置成用DMA的方式接收數據,等接收完畢就可以去讀取了!

這個同學是對的,我們可以使用DMA去接收數據,不過DMA需要定長才能產生接收中斷,如何接收不定長的數據呢?

DMA簡介

題外話:其實,上面的問題是很有必要思考一下的,不斷思考,才能進步。

什麼是DMA

DMA:全稱Direct Memory Access,即直接存儲器訪問

DMA 傳輸將數據從一個地址空間複製到另外一個地址空間。CPU只需初始化DMA即可,傳輸動作本身是由 DMA 控制器來實現和完成。典型的例子就是移動一個外部記憶體的區塊到晶片內部更快的記憶體區。這樣的操作並沒有讓處理器參與處理,CPU可以干其他事情,當DMA傳輸完成的時候產生一個中斷,告訴CPU我已經完成了,然後CPU知道了就可以去處理數據了,這樣子提高了CPU的利用率,因為CPU是大腦,主要做數據運算的工作,而不是去搬運數據。DMA 傳輸對於高效能嵌入式系統演算法和網路是很重要的。

在STM32的DMA資源

STM32F1系列的MCU有兩個DMA控制器(DMA2隻存在於大容量產品中),DMA1有7個通道,DMA2有5個通道,每個通道專門用來管理來自於一個或者多個外設對存儲器的訪問請求。還有一個仲裁器來協調各個DMA請求的優先權。

f1-dam

f1-dam2

而STM32F4/F7/H7系列的MCU有兩個DMA控制器總共有16個數據流(每個DMA控制器8個),每一個DMA控制器都用於管理一個或多個外設的存儲器訪問請求。每個數據流總共可以有多達8個通道(或稱請求)。每個通道都有一個仲裁器,用於處理 DMA 請求間的優先順序。
f4-dam

f4-dam2

DMA接收數據

DMA在接收數據的時候,串口接收DMA在初始化的時候就處於開啟狀態,一直等待數據的到來,在軟體上無需做任何事情,只要在初始化配置的時候設置好配置就可以了。等到接收到數據的時候,告訴CPU去處理即可。

判斷數據接收完成

那麼問題來了,怎麼知道數據是否接收完成呢?

其實,有很多方法:

  • 對於定長的數據,只需要判斷一下數據的接收個數,就知道是否接收完成,這個很簡單,暫不討論。
  • 對於不定長的數據,其實也有好幾種方法,麻煩的我肯定不會介紹,有興趣做複雜工作的同學可以在網上看看別人怎麼做,下面這種方法是最簡單的,充分利用了stm32的串口資源,效率也是非常之高。

DMA+串口空閑中斷

這兩個資源配合,簡直就是天衣無縫啊,無論接收什麼不定長的數據,管你數據有多少,來一個我就收一個,就像廣東人吃「山竹」,來一個吃一個~(最近風好大,我好怕)。

可能很多人在學習stm32的時候,都不知道idle是啥東西,先看看stm32串口的狀態暫存器:

idle

idle1

當我們檢測到觸發了串口匯流排空閑中斷的時候,我們就知道這一波數據傳輸完成了,然後我們就能得到這些數據,去進行處理即可。這種方法是最簡單的,根本不需要我們做多的處理,只需要配置好,串口就等著數據的到來,dma也是處於工作狀態的,來一個數據就自動搬運一個數據。

接收完數據時處理

串口接收完數據是要處理的,那麼處理的步驟是怎麼樣呢?

  • 暫時關閉串口接收DMA通道,有兩個原因:1.防止後面又有數據接收到,產生干擾,因為此時的數據還未處理。2.DMA需要重新配置。
  • 清DMA標誌位。
  • 從DMA暫存器中獲取接收到的數據位元組數(可有可無)。
  • 重新設置DMA下次要接收的數據位元組數,注意,數據傳輸數量範圍為0至65535。這個暫存器只能在通道不工作(DMA_CCRx的EN=0)時寫入。通道開啟後該暫存器變為只讀,指示剩餘的待傳輸位元組數目。暫存器內容在每次DMA傳輸後遞減。數據傳輸結束後,暫存器的內容或者變為0;或者當該通道配置為自動重載入模式時,暫存器的內容將被自動重新載入為之前配置時的數值。當暫存器的內容為0時,無論通道是否開啟,都不會發生任何數據傳輸。
  • 給出訊號量,發送接收到新數據標誌,供前台程式查詢。
  • 開啟DMA通道,等待下一次的數據接收,注意,對DMA的相關暫存器配置寫入,如重置DMA接收數據長度,必須要在關閉DMA的條件進行,否則操作無效。

注意事項

STM32的IDLE的中斷在串口無數據接收的情況下,是不會一直產生的,產生的條件是這樣的,當清除IDLE標誌位後,必須有接收到第一個數據後,才開始觸發,一斷接收的數據斷流,沒有接收到數據,即產生IDLE中斷。如果中斷髮送數據幀的速率很快,MCU來不及處理此次接收到的數據,中斷又發來數據的話,這裡不能開啟,否則數據會被覆蓋。有兩種方式解決:

  1. 在重新開啟接收DMA通道之前,將Rx_Buf緩衝區裡面的數據複製到另外一個數組中,然後再開啟DMA,然後馬上處理複製出來的數據。

  2. 建立雙緩衝,重新配置DMA_MemoryBaseAddr的緩衝區地址,那麼下次接收到的數據就會保存到新的緩衝區中,不至於被覆蓋。

程式實現

實驗效果:
當外部給單片機發送數 據的時候,假設這幀數據長度是1000個位元組,那麼在單片機接收到一個位元組的時候並不會產生串口中斷,只是DMA在背後默默地把數據搬運到你指定的緩衝區裡面。當整幀數據發送完畢之後串口才會產生一次中斷,此時可以利用DMA_GetCurrDataCounter()函數計算出本次的數據接受長度,從而進行數據處理。

串口的配置
很簡單,基本與使用串口的時候一致,只不過一般我們是打開接收緩衝區非空中斷,而現在是打開空閑中斷——USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);

/**    * @brief  USART GPIO 配置,工作參數配置    * @param  無    * @retval 無    */  void USART_Config(void)  {      GPIO_InitTypeDef GPIO_InitStructure;      USART_InitTypeDef USART_InitStructure;        // 打開串口GPIO的時鐘      DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);        // 打開串口外設的時鐘      DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);        // 將USART Tx的GPIO配置為推挽復用模式      GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;      GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);      // 將USART Rx的GPIO配置為浮空輸入模式      GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;      GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);        // 配置串口的工作參數      // 配置波特率      USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;      // 配置 針數據字長      USART_InitStructure.USART_WordLength = USART_WordLength_8b;      // 配置停止位      USART_InitStructure.USART_StopBits = USART_StopBits_1;      // 配置校驗位      USART_InitStructure.USART_Parity = USART_Parity_No ;      // 配置硬體流控制      USART_InitStructure.USART_HardwareFlowControl =      USART_HardwareFlowControl_None;      // 配置工作模式,收發一起      USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;      // 完成串口的初始化配置      USART_Init(DEBUG_USARTx, &USART_InitStructure);      // 串口中斷優先順序配置      NVIC_Configuration();    #if USE_USART_DMA_RX      // 開啟 串口空閑IDEL 中斷      USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);    // 開啟串口DMA接收      USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE);      /* 使能串口DMA */      USARTx_DMA_Rx_Config();  #else      // 使能串口接收中斷      USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);  #endif    #if USE_USART_DMA_TX      // 開啟串口DMA發送  //  USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Tx, ENABLE);      USARTx_DMA_Tx_Config();  #endif        // 使能串口      USART_Cmd(DEBUG_USARTx, ENABLE);  }

串口DMA配置

把DMA配置完成,就可以直接打開DMA了,讓它處於工作狀態,當有數據的時候就能直接搬運了。

#if USE_USART_DMA_RX    static void USARTx_DMA_Rx_Config(void)  {      DMA_InitTypeDef DMA_InitStructure;        // 開啟DMA時鐘      RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);      // 設置DMA源地址:串口數據暫存器地址*/      DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS;      // 記憶體地址(要傳輸的變數的指針)      DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Rx_Buf;      // 方向:從記憶體到外設      DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;      // 傳輸大小      DMA_InitStructure.DMA_BufferSize = USART_RX_BUFF_SIZE;      // 外設地址不增      DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;      // 記憶體地址自增      DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;      // 外設數據單位      DMA_InitStructure.DMA_PeripheralDataSize =      DMA_PeripheralDataSize_Byte;      // 記憶體數據單位      DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;      // DMA模式,一次或者循環模式      //DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;      DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;      // 優先順序:中      DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;      // 禁止記憶體到記憶體的傳輸      DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;      // 配置DMA通道      DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure);      // 清除DMA所有標誌      DMA_ClearFlag(DMA1_FLAG_TC5);      DMA_ITConfig(USART_RX_DMA_CHANNEL, DMA_IT_TE, ENABLE);      // 使能DMA      DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE);  }  #endif

接收完數據處理

因為接收完數據之後,會產生一個idle中斷,也就是空閑中斷,那麼我們就可以在中斷服務函數中知道已經接收完了,就可以處理數據了,但是中斷服務函數的上下文環境是中斷,所以,盡量是快進快出,一般在中斷中將一些標誌置位,供前台查詢。在中斷中先判斷我們的產生在中斷的類型是不是idle中斷,如果是則進行下一步,否則就無需理會。

/**    ******************************************************************    * @brief   串口中斷服務函數    * @author  jiejie    * @version V1.0    * @date    2018-xx-xx    ******************************************************************    */  void DEBUG_USART_IRQHandler(void)  {  #if USE_USART_DMA_RX      /* 使用串口DMA */      if(USART_GetITStatus(DEBUG_USARTx,USART_IT_IDLE)!=RESET)      {          /* 接收數據 */          Receive_DataPack();          // 清除空閑中斷標誌位          USART_ReceiveData( DEBUG_USARTx );      }  #else    /* 接收中斷 */      if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)      {      Receive_DataPack();      }  #endif  }

Receive_DataPack()

這個才是真正的接收數據處理函數,為什麼我要將這個函數單獨封裝起來呢?因為這個函數其實是很重要的,因為我的程式碼兼容普通串口接收與空閑中斷,不一樣的接收類型其處理也不一樣,所以直接封裝起來更好,在源碼中通過宏定義實現選擇接收的方式!更考慮了兼容作業系統的,可能我會在系統中使用dma+空閑中斷,所以,供前台查詢的訊號量就有可能不一樣,可能需要修改,我就把它封裝起來了。不過無所謂,都是一樣的。

/************************************************************    * @brief   Uart_DMA_Rx_Data    * @param   NULL    * @return  NULL    * @author  jiejie    * @github  https://github.com/jiejieTop    * @date    2018-xx-xx    * @version v1.0    * @note    使用串口 DMA 接收時調用的函數    ***********************************************************/  #if USE_USART_DMA_RX  void Receive_DataPack(void)  {      /* 接收的數據長度 */      uint32_t buff_length;        /* 關閉DMA ,防止干擾 */      DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE);  /* 暫時關閉dma,數據尚未處理 */        /* 清DMA標誌位 */      DMA_ClearFlag( DMA1_FLAG_TC5 );        /* 獲取接收到的數據長度 單位為位元組*/      buff_length = USART_RX_BUFF_SIZE - DMA_GetCurrDataCounter(USART_RX_DMA_CHANNEL);        /* 獲取數據長度 */      Usart_Rx_Sta = buff_length;        PRINT_DEBUG("buff_length = %dn ",buff_length);        /* 重新賦值計數值,必須大於等於最大可能接收到的數據幀數目 */      USART_RX_DMA_CHANNEL->CNDTR = USART_RX_BUFF_SIZE;        /* 此處應該在處理完數據再打開,如在 DataPack_Process() 打開*/      DMA_Cmd(USART_RX_DMA_CHANNEL, ENABLE);        /* (OS)給出訊號 ,發送接收到新數據標誌,供前台程式查詢 */        /* 標記接收完成,在 DataPack_Handle 處理*/      Usart_Rx_Sta |= 0xC000;        /*      DMA 開啟,等待數據。注意,如果中斷髮送數據幀的速率很快,MCU來不及處理此次接收到的數據,      中斷又發來數據的話,這裡不能開啟,否則數據會被覆蓋。有2種方式解決:        1. 在重新開啟接收DMA通道之前,將Rx_Buf緩衝區裡面的數據複製到另外一個數組中,      然後再開啟DMA,然後馬上處理複製出來的數據。        2. 建立雙緩衝,重新配置DMA_MemoryBaseAddr的緩衝區地址,那麼下次接收到的數據就會      保存到新的緩衝區中,不至於被覆蓋。      */  }

f1使用dma是非常簡單的,我在f4用dma的時候也遇到一些問題,最後看手冊解決了,打算下一篇文章就寫一下調試過程,沒有什麼是debug不能解決的,如果有,那就兩次。今天颱風天氣,連著舍友的WiFi更新的文章~中國電信還是強,颱風天氣訊號一點都不虛,我的移動卡一動不動-_-.

喜歡就關注我吧!

歡迎關注我公眾號

相關程式碼可以在公眾號後台回復獲取。

更多資料歡迎關注「物聯網IoT開發」公眾號!