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請求的優先權。
而STM32F4/F7/H7系列的MCU有兩個DMA控制器總共有16個資料流(每個DMA控制器8個),每一個DMA控制器都用于管理一個或多個外設的存儲器通路請求。每個資料流總共可以有多達8個通道(或稱請求)。每個通道都有一個仲裁器,用于處理 DMA 請求間的優先級。
DMA接收資料
DMA在接收資料的時候,序列槽接收DMA在初始化的時候就處于開啟狀态,一直等待資料的到來,在軟體上無需做任何事情,隻要在初始化配置的時候設定好配置就可以了。等到接收到資料的時候,告訴CPU去處理即可。
判斷資料接收完成
那麼問題來了,怎麼知道資料是否接收完成呢?
其實,有很多方法:
- 對于定長的資料,隻需要判斷一下資料的接收個數,就知道是否接收完成,這個很簡單,暫不讨論。
- 對于不定長的資料,其實也有好幾種方法,麻煩的我肯定不會介紹,有興趣做複雜工作的同學可以在網上看看别人怎麼做,下面這種方法是最簡單的,充分利用了stm32的序列槽資源,效率也是非常之高。
DMA+序列槽空閑中斷
這兩個資源配合,簡直就是天衣無縫啊,無論接收什麼不定長的資料,管你資料有多少,來一個我就收一個,就像廣東人吃“山竹”,來一個吃一個~(最近風好大,我好怕)。
可能很多人在學習stm32的時候,都不知道idle是啥東西,先看看stm32序列槽的狀态寄存器:
當我們檢測到觸發了序列槽總線空閑中斷的時候,我們就知道這一波資料傳輸完成了,然後我們就能得到這些資料,去進行處理即可。這種方法是最簡單的,根本不需要我們做多的處理,隻需要配置好,序列槽就等着資料的到來,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來不及處理此次接收到的資料,中斷又發來資料的話,這裡不能開啟,否則資料會被覆寫。有兩種方式解決:
- 在重新開啟接收DMA通道之前,将Rx_Buf緩沖區裡面的資料複制到另外一個數組中,然後再開啟DMA,然後馬上處理複制出來的資料。
- 建立雙緩沖,重新配置DMA_MemoryBaseAddr的緩沖區位址,那麼下次接收到的資料就會儲存到新的緩沖區中,不至于被覆寫。
程式實作
實驗效果:當外部給單片機發送數 據的時候,假設這幀資料長度是1000個位元組,那麼在單片機接收到一個位元組的時候并不會産生序列槽中斷,隻是DMA在背後默默地把資料搬運到你指定的緩沖區裡面。當整幀資料發送完畢之後序列槽才會産生一次中斷,此時可以利用DMA_GetCurrDataCounter()函數計算出本次的資料接受長度,進而進行資料處理。
序列槽的配置很簡單,基本與使用序列槽的時候一緻,隻不過一般我們是打開接收緩沖區非空中斷,而現在是打開空閑中斷——USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE); 。
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);
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中斷,如果是則進行下一步,否則就無需理會。
void DEBUG_USART_IRQHandler(void)
{
#if USE_USART_DMA_RX
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+空閑中斷,是以,供前台查詢的信号量就有可能不一樣,可能需要修改,我就把它封裝起來了。不過無所謂,都是一樣的。
#if USE_USART_DMA_RX
void Receive_DataPack(void)
{
uint32_t buff_length;
DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE);
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