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

来源:https://www.cnblogs.com/iot-dev/archive/2019/10/16/11688743.html
-Advertisement-
Play Games

STM32之串口DMA接收不定長數據 引言 在使用stm32或者其他單片機的時候,會經常使用到串口通訊,那麼如何有效地接收數據呢?假如這段數據是不定長的有如何高效接收呢? 同學A:數據來了就會進入串口中斷,在中斷中讀取數據就行了! 中斷就是打斷程式正常運行,怎麼能保證高效呢?經常把主程式打斷,主程式 ...


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 = %d\n ",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開發”公眾號!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 任務的基本概念 從系統的角度看,任務是競爭系統資源的最小運行單元。TencentOS tiny是一個支持多任務的操作系統,任務可以使用或等待CPU、使用記憶體空間等系統資源,並獨立於其它任務運行,理論上任何數量的任務都可以共用同一個優先順序,這樣子處於就緒態的多個相同優先順序任務將會以時間片切換的方式共用 ...
  • 移植前的準備工作 1. 獲取STM32的裸機工程模板 STM32的裸機工程模板直接使用野火STM32開發板配套的固件庫常式即可。可以從我 上獲取 "https://github.com/jiejieTop/TencentOS Demo" 下載TencentOS tiny 源碼 TencentOS t ...
  • 新聞 2019年9月18日,騰訊宣佈將 `自主研發 輕量級`物聯網實時操作系統 TencentOS tiny 。相比市場上其它系統,騰訊TencentOS tiny在資源占用、設備成本、功耗管理以及安全穩定等層面極具競爭力。該系統的開源可大幅降低物聯網應用開發成本,提升開發效率,同時支持一鍵上雲,對 ...
  • 今天,需要再本地使用git管理代碼,但是當代碼創建好的時候,想發佈到github上面的私有倉庫中,但是沒有提前創建遠端倉庫,所以需要把本地git倉庫推送到遠端另外一個倉庫了,下麵進行簡要記錄,剛剛經過的過程,方便之後再次使用的時候,不會重覆勞動: git 設置 git config user.nam ...
  • button drive 傑傑自己寫的一個按鍵驅動,支持單雙擊、連按、長按;採用回調處理按鍵事件(自定義消抖時間),使用只需3步,創建按鍵,按鍵事件與回調處理函數鏈接映射,周期檢查按鍵。 源碼地址: "https://github.com/jiejieTop/ButtonDrive" 。作者: "傑 ...
  • 基於Linux的kfifo移植到STM32(支持os的互斥訪問) 關於kfifo kfifo是內核裡面的一個First In First Out數據結構,它採用環形迴圈隊列的數據結構來實現;它提供一個無邊界的位元組流服務,最重要的一點是,它使用並行無鎖編程技術,即當它用於只有一個入隊線程和一個出隊線程 ...
  • 隊列的概念 在此之前,我們來回顧一下隊列的基本概念: 隊列 (Queue):是一種先進先出(First In First Out ,簡稱 FIFO)的線性表,只允許在一端插入(入隊),在另一端進行刪除(出隊)。 隊列的特點 類似售票排隊視窗,先到的人看到能先買到票,然後先走,後來的人只能後買到票 隊 ...
  • 本文介紹一種Cortex M內核中的精確延時方法 前言 為什麼要學習這種延時的方法? 1. 很多時候我們跑操作系統,就一般會占用一個硬體定時器——SysTick,而我們一般操作系統的時鐘節拍一般是設置100 1000HZ,也就是1ms——10ms產生一次中斷。很多裸機教程使用延時函數又是基於SysT ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...