# STM32之串口DMA接收不定長(zhǎng)數(shù)據(jù)
## 引言
在使用stm32或者其他單片機(jī)的時(shí)候伊脓,會(huì)經(jīng)常使用到串口通訊蒙兰,那么如何有效地接收數(shù)據(jù)呢?假如這段數(shù)據(jù)是不定長(zhǎng)的有如何高效接收呢?
> 同學(xué)A:數(shù)據(jù)來(lái)了就會(huì)進(jìn)入串口中斷盅藻,在中斷中讀取數(shù)據(jù)就行了!
> **中斷就是打斷程序正常運(yùn)行畅铭,怎么能保證高效呢氏淑?經(jīng)常把主程序打斷,主程序還要不要運(yùn)行了硕噩?**
> 同學(xué)B:串口可以配置成用DMA的方式接收數(shù)據(jù)假残,等接收完畢就可以去讀取了!
> **這個(gè)同學(xué)是對(duì)的,我們可以使用DMA去接收數(shù)據(jù)辉懒,不過(guò)DMA需要定長(zhǎng)才能產(chǎn)生接收中斷,如何接收不定長(zhǎng)的數(shù)據(jù)呢阳惹?**
## DMA簡(jiǎn)介
> 題外話:其實(shí),上面的問(wèn)題是很有必要思考一下的眶俩,不斷思考莹汤,才能進(jìn)步。
### 什么是DMA
**DMA**:全稱Direct Memory Access颠印,即直接存儲(chǔ)器訪問(wèn)
DMA 傳輸將數(shù)據(jù)從一個(gè)地址空間復(fù)制到另外一個(gè)地址空間纲岭。CPU只需初始化DMA即可,傳輸動(dòng)作本身是由 DMA 控制器來(lái)實(shí)現(xiàn)和完成线罕。典型的例子就是移動(dòng)一個(gè)外部?jī)?nèi)存的區(qū)塊到芯片內(nèi)部更快的內(nèi)存區(qū)止潮。這樣的操作并沒有讓處理器參與處理,CPU可以干其他事情钞楼,當(dāng)DMA傳輸完成的時(shí)候產(chǎn)生一個(gè)中斷喇闸,告訴CPU我已經(jīng)完成了,然后CPU知道了就可以去處理數(shù)據(jù)了,這樣子提高了CPU的利用率,因?yàn)镃PU是大腦柏副,主要做數(shù)據(jù)運(yùn)算的工作,而不是去搬運(yùn)數(shù)據(jù)橘沥。DMA 傳輸對(duì)于高效能嵌入式系統(tǒng)算法和網(wǎng)絡(luò)是很重要的。
### 在STM32的DMA資源
**STM32F1系列**的MCU有兩個(gè)DMA控制器(DMA2只存在于大容量產(chǎn)品中)夯秃,DMA1有7個(gè)通道座咆,DMA2有5個(gè)通道,每個(gè)通道專門用來(lái)管理來(lái)自于一個(gè)或者多個(gè)外設(shè)對(duì)存儲(chǔ)器的訪問(wèn)請(qǐng)求仓洼。還有一個(gè)仲裁器來(lái)協(xié)調(diào)各個(gè)DMA請(qǐng)求的優(yōu)先權(quán)介陶。
![STM32F1](https://note.youdao.com/yws/api/personal/file/1A5309B80FAB41709CC8A229D588403B?method=download&shareKey=7e8848c6228cc98c6df45ec4ac934052)
![STM32F1](https://note.youdao.com/yws/api/personal/file/E54C58EFB905499CAFA4D678A5DB0FB7?method=download&shareKey=f7973380c4d883e932fc9d1026f96471)
**而STM32F4/F7/H7系列**的MCU有兩個(gè)DMA控制器總共有16個(gè)數(shù)據(jù)流(每個(gè)DMA控制器8個(gè)),每一個(gè)DMA控制器都用于管理一個(gè)或多個(gè)外設(shè)的存儲(chǔ)器訪問(wèn)請(qǐng)求色建。每個(gè)數(shù)據(jù)流總共可以有多達(dá)8個(gè)通道(或稱請(qǐng)求)哺呜。每個(gè)通道都有一個(gè)仲裁器,用于處理 DMA 請(qǐng)求間的優(yōu)先級(jí)箕戳。
![STM32F4](https://note.youdao.com/yws/api/personal/file/4A54FECA588047CDB48D3C699D8612EA?method=download&shareKey=2b1058e7f1a578b1f8e56a52c0894f66)
![STM32F4](https://note.youdao.com/yws/api/personal/file/B0059ABB61B64806AC6056A03AD56992?method=download&shareKey=b3502264cc2d29f2275646f5fcb81dda )
### DMA接收數(shù)據(jù)
DMA在接收數(shù)據(jù)的時(shí)候某残,串口接收DMA在初始化的時(shí)候就處于開啟狀態(tài),一直等待數(shù)據(jù)的到來(lái)陵吸,在軟件上無(wú)需做任何事情玻墅,只要在初始化配置的時(shí)候設(shè)置好配置就可以了。等到接收到數(shù)據(jù)的時(shí)候壮虫,告訴CPU去處理即可澳厢。
### 判斷數(shù)據(jù)接收完成
> 那么問(wèn)題來(lái)了环础,怎么知道數(shù)據(jù)是否接收完成呢?
其實(shí)剩拢,有很多方法:
- 對(duì)于定長(zhǎng)的數(shù)據(jù)线得,只需要判斷一下數(shù)據(jù)的接收個(gè)數(shù),就知道是否接收完成徐伐,這個(gè)很簡(jiǎn)單贯钩,暫不討論。
- 對(duì)于不定長(zhǎng)的數(shù)據(jù)办素,其實(shí)也有好幾種方法角雷,麻煩的我肯定不會(huì)介紹,有興趣做復(fù)雜工作的同學(xué)可以在網(wǎng)上看看別人怎么做摸屠,下面這種方法是最簡(jiǎn)單的谓罗,充分利用了stm32的串口資源粱哼,效率也是非常之高季二。
**DMA+串口空閑中斷**
這兩個(gè)資源配合,簡(jiǎn)直就是天衣無(wú)縫啊揭措,無(wú)論接收什么不定長(zhǎng)的數(shù)據(jù)胯舷,管你數(shù)據(jù)有多少,來(lái)一個(gè)我就收一個(gè)绊含,就像廣東人吃“山竹”桑嘶,來(lái)一個(gè)吃一個(gè)~(最近風(fēng)好大,我好怕)躬充。
可能很多人在學(xué)習(xí)stm32的時(shí)候逃顶,都不知道idle是啥東西,先看看stm32串口的狀態(tài)寄存器:
![idle](https://note.youdao.com/yws/api/personal/file/WEBf3c67833dc9c0ce2be4ac912925d1bcb?method=download&shareKey=3a524caa59faf8819048d98121e76b5c)
![idle說(shuō)明](https://note.youdao.com/yws/api/personal/file/WEBb144880886147b85f8602a8f7f117941?method=download&shareKey=04179de3da48c34ee0c3ca78920d7ccf)
當(dāng)我們檢測(cè)到觸發(fā)了串口總線空閑中斷的時(shí)候充甚,我們就知道這一波數(shù)據(jù)傳輸完成了以政,然后我們就能得到這些數(shù)據(jù),去進(jìn)行處理即可伴找。這種方法是最簡(jiǎn)單的盈蛮,根本不需要我們做多的處理,只需要配置好技矮,串口就等著數(shù)據(jù)的到來(lái)抖誉,dma也是處于工作狀態(tài)的,來(lái)一個(gè)數(shù)據(jù)就自動(dòng)搬運(yùn)一個(gè)數(shù)據(jù)衰倦。
### 接收完數(shù)據(jù)時(shí)處理
串口接收完數(shù)據(jù)是要處理的袒炉,那么處理的步驟是怎么樣呢?
- 暫時(shí)關(guān)閉串口接收DMA通道樊零,有兩個(gè)原因:1.防止后面又有數(shù)據(jù)接收到梳杏,產(chǎn)生干擾,因?yàn)榇藭r(shí)的數(shù)據(jù)還未處理。2.DMA需要重新配置十性。
- 清DMA標(biāo)志位叛溢。
- 從DMA寄存器中獲取接收到的數(shù)據(jù)字節(jié)數(shù)(可有可無(wú))。
- 重新設(shè)置DMA下次要接收的數(shù)據(jù)字節(jié)數(shù)劲适,注意楷掉,數(shù)據(jù)傳輸數(shù)量范圍為0至65535。這個(gè)寄存器只能在通道不工作(DMA_CCRx的EN=0)時(shí)寫入霞势。通道開啟后該寄存器變?yōu)橹蛔x烹植,指示剩余的待傳輸字節(jié)數(shù)目。寄存器內(nèi)容在每次DMA傳輸后遞減愕贡。數(shù)據(jù)傳輸結(jié)束后草雕,寄存器的內(nèi)容或者變?yōu)?;或者當(dāng)該通道配置為自動(dòng)重加載模式時(shí)固以,寄存器的內(nèi)容將被自動(dòng)重新加載為之前配置時(shí)的數(shù)值墩虹。當(dāng)寄存器的內(nèi)容為0時(shí),無(wú)論通道是否開啟憨琳,都不會(huì)發(fā)生任何數(shù)據(jù)傳輸诫钓。
- 給出信號(hào)量,發(fā)送接收到新數(shù)據(jù)標(biāo)志篙螟,供前臺(tái)程序查詢菌湃。
- 開啟DMA通道,等待下一次的數(shù)據(jù)接收遍略,注意惧所,對(duì)DMA的相關(guān)寄存器配置寫入,如重置DMA接收數(shù)據(jù)長(zhǎng)度绪杏,必須要在關(guān)閉DMA的條件進(jìn)行下愈,否則操作無(wú)效。
**注意事項(xiàng)**
STM32的IDLE的中斷在串口無(wú)數(shù)據(jù)接收的情況下寞忿,是不會(huì)一直產(chǎn)生的驰唬,產(chǎn)生的條件是這樣的,當(dāng)清除IDLE標(biāo)志位后腔彰,必須有接收到第一個(gè)數(shù)據(jù)后叫编,才開始觸發(fā),一斷接收的數(shù)據(jù)斷流霹抛,沒有接收到數(shù)據(jù)搓逾,即產(chǎn)生IDLE中斷。如果中斷發(fā)送數(shù)據(jù)幀的速率很快杯拐,MCU來(lái)不及處理此次接收到的數(shù)據(jù)霞篡,中斷又發(fā)來(lái)數(shù)據(jù)的話世蔗,這里不能開啟,否則數(shù)據(jù)會(huì)被覆蓋朗兵。有兩種方式解決:
1. 在重新開啟接收DMA通道之前污淋,將Rx_Buf緩沖區(qū)里面的數(shù)據(jù)復(fù)制到另外一個(gè)數(shù)組中,然后再開啟DMA余掖,然后馬上處理復(fù)制出來(lái)的數(shù)據(jù)寸爆。
2. 建立雙緩沖,重新配置DMA_MemoryBaseAddr的緩沖區(qū)地址盐欺,那么下次接收到的數(shù)據(jù)就會(huì)保存到新的緩沖區(qū)中赁豆,不至于被覆蓋。
### 程序?qū)崿F(xiàn)
實(shí)驗(yàn)效果:
當(dāng)外部給單片機(jī)發(fā)送數(shù) 據(jù)的時(shí)候冗美,假設(shè)這幀數(shù)據(jù)長(zhǎng)度是1000個(gè)字節(jié)魔种,那么在單片機(jī)接收到一個(gè)字節(jié)的時(shí)候并不會(huì)產(chǎn)生串口中斷,只是DMA在背后默默地把數(shù)據(jù)搬運(yùn)到你指定的緩沖區(qū)里面粉洼。當(dāng)整幀數(shù)據(jù)發(fā)送完畢之后串口才會(huì)產(chǎn)生一次中斷节预,此時(shí)可以利用`DMA_GetCurrDataCounter()`函數(shù)計(jì)算出本次的數(shù)據(jù)接受長(zhǎng)度,從而進(jìn)行數(shù)據(jù)處理漆改。
**串口的配置**
很簡(jiǎn)單心铃,基本與使用串口的時(shí)候一致准谚,只不過(guò)一般我們是打開接收緩沖區(qū)非空中斷挫剑,而現(xiàn)在是打開空閑中斷——`USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);? `。
```
/**
? * @brief? USART GPIO 配置,工作參數(shù)配置
? * @param? 無(wú)
? * @retval 無(wú)
? */
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打開串口GPIO的時(shí)鐘
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打開串口外設(shè)的時(shí)鐘
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 將USART Tx的GPIO配置為推挽復(fù)用模式
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);
// 配置串口的工作參數(shù)
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 針數(shù)據(jù)字長(zhǎng)
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置校驗(yàn)位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 配置硬件流控制
USART_InitStructure.USART_HardwareFlowControl =
USART_HardwareFlowControl_None;
// 配置工作模式柱衔,收發(fā)一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(DEBUG_USARTx, &USART_InitStructure);
// 串口中斷優(yōu)先級(jí)配置
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發(fā)送
// USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Tx, ENABLE);
USARTx_DMA_Tx_Config();
#endif
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE); ? ?
}
```
**串口DMA配置**
把DMA配置完成樊破,就可以直接打開DMA了,讓它處于工作狀態(tài)唆铐,當(dāng)有數(shù)據(jù)的時(shí)候就能直接搬運(yùn)了哲戚。
```
#if USE_USART_DMA_RX
static void USARTx_DMA_Rx_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 開啟DMA時(shí)鐘
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 設(shè)置DMA源地址:串口數(shù)據(jù)寄存器地址*/
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS;
// 內(nèi)存地址(要傳輸?shù)淖兞康闹羔?
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Rx_Buf;
// 方向:從內(nèi)存到外設(shè)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
// 傳輸大小
DMA_InitStructure.DMA_BufferSize = USART_RX_BUFF_SIZE;
// 外設(shè)地址不增 ? ?
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// 內(nèi)存地址自增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 外設(shè)數(shù)據(jù)單位
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_Byte;
// 內(nèi)存數(shù)據(jù)單位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
// DMA模式,一次或者循環(huán)模式
//DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
// 優(yōu)先級(jí):中
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
// 禁止內(nèi)存到內(nèi)存的傳輸
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
// 配置DMA通道 ?
DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure);
// 清除DMA所有標(biāo)志
DMA_ClearFlag(DMA1_FLAG_TC5);
DMA_ITConfig(USART_RX_DMA_CHANNEL, DMA_IT_TE, ENABLE);
// 使能DMA
DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE);
}
#endif
```
**接收完數(shù)據(jù)處理**
因?yàn)榻邮胀陻?shù)據(jù)之后艾岂,會(huì)產(chǎn)生一個(gè)idle中斷顺少,也就是空閑中斷,那么我們就可以在中斷服務(wù)函數(shù)中知道已經(jīng)接收完了王浴,就可以處理數(shù)據(jù)了脆炎,但是中斷服務(wù)函數(shù)的上下文環(huán)境是中斷,所以氓辣,盡量是快進(jìn)快出秒裕,一般在中斷中將一些標(biāo)志置位,供前臺(tái)查詢钞啸。在中斷中先判斷我們的產(chǎn)生在中斷的類型是不是idle中斷几蜻,如果是則進(jìn)行下一步喇潘,否則就無(wú)需理會(huì)。
```
/**
? ******************************************************************
? * @brief? 串口中斷服務(wù)函數(shù)
? * @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)
{
/* 接收數(shù)據(jù) */
Receive_DataPack();
// 清除空閑中斷標(biāo)志位
USART_ReceiveData( DEBUG_USARTx );
}
#else
? /* 接收中斷 */
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
{
? ? Receive_DataPack();
}
#endif
}
```
**Receive_DataPack()**
這個(gè)才是真正的接收數(shù)據(jù)處理函數(shù)梭稚,為什么我要將這個(gè)函數(shù)單獨(dú)封裝起來(lái)呢颖低?因?yàn)檫@個(gè)函數(shù)其實(shí)是很重要的,因?yàn)槲业拇a兼容普通串口接收與空閑中斷弧烤,不一樣的接收類型其處理也不一樣枫甲,所以直接封裝起來(lái)更好,在源碼中通過(guò)宏定義實(shí)現(xiàn)選擇接收的方式扼褪!更考慮了兼容操作系統(tǒng)的想幻,可能我會(huì)在系統(tǒng)中使用dma+空閑中斷,所以话浇,供前臺(tái)查詢的信號(hào)量就有可能不一樣脏毯,可能需要修改,我就把它封裝起來(lái)了幔崖。不過(guò)無(wú)所謂食店,都是一樣的。
```
/************************************************************
? * @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 接收時(shí)調(diào)用的函數(shù)
? ***********************************************************/
#if USE_USART_DMA_RX
void Receive_DataPack(void)
{
/* 接收的數(shù)據(jù)長(zhǎng)度 */
uint32_t buff_length;
/* 關(guān)閉DMA 赏寇,防止干擾 */
DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE);? /* 暫時(shí)關(guān)閉dma吉嫩,數(shù)據(jù)尚未處理 */
/* 清DMA標(biāo)志位 */
DMA_ClearFlag( DMA1_FLAG_TC5 );?
/* 獲取接收到的數(shù)據(jù)長(zhǎng)度 單位為字節(jié)*/
buff_length = USART_RX_BUFF_SIZE - DMA_GetCurrDataCounter(USART_RX_DMA_CHANNEL);
? ? /* 獲取數(shù)據(jù)長(zhǎng)度 */
? ? Usart_Rx_Sta = buff_length;
PRINT_DEBUG("buff_length = %d\n ",buff_length);
/* 重新賦值計(jì)數(shù)值,必須大于等于最大可能接收到的數(shù)據(jù)幀數(shù)目 */
USART_RX_DMA_CHANNEL->CNDTR = USART_RX_BUFF_SIZE;? ?
/* 此處應(yīng)該在處理完數(shù)據(jù)再打開嗅定,如在 DataPack_Process() 打開*/
DMA_Cmd(USART_RX_DMA_CHANNEL, ENABLE);? ? ?
/* (OS)給出信號(hào) 自娩,發(fā)送接收到新數(shù)據(jù)標(biāo)志,供前臺(tái)程序查詢 */
? ? /* 標(biāo)記接收完成渠退,在 DataPack_Handle 處理*/
? ? Usart_Rx_Sta |= 0xC000;
? ? /*
? ? DMA 開啟忙迁,等待數(shù)據(jù)。注意碎乃,如果中斷發(fā)送數(shù)據(jù)幀的速率很快姊扔,MCU來(lái)不及處理此次接收到的數(shù)據(jù),
? ? 中斷又發(fā)來(lái)數(shù)據(jù)的話梅誓,這里不能開啟恰梢,否則數(shù)據(jù)會(huì)被覆蓋。有2種方式解決:
? ? 1. 在重新開啟接收DMA通道之前梗掰,將Rx_Buf緩沖區(qū)里面的數(shù)據(jù)復(fù)制到另外一個(gè)數(shù)組中嵌言,
? ? 然后再開啟DMA,然后馬上處理復(fù)制出來(lái)的數(shù)據(jù)愧怜。
? ? 2. 建立雙緩沖呀页,重新配置DMA_MemoryBaseAddr的緩沖區(qū)地址,那么下次接收到的數(shù)據(jù)就會(huì)
? ? 保存到新的緩沖區(qū)中拥坛,不至于被覆蓋蓬蝶。
*/
}
```
f1使用dma是非常簡(jiǎn)單的尘分,我在f4用dma的時(shí)候也遇到一些問(wèn)題,最后看手冊(cè)解決了丸氛,打算下一篇文章就寫一下調(diào)試過(guò)程培愁,沒有什么是debug不能解決的,如果有缓窜,那就兩次定续。今天臺(tái)風(fēng)天氣,連著舍友的WiFi更新的文章~中國(guó)電信還是強(qiáng)禾锤,臺(tái)風(fēng)天氣信號(hào)一點(diǎn)都不虛私股,我的移動(dòng)卡一動(dòng)不動(dòng)-_-\.