本章參考資料:《STM32F76xxx參考手冊》DMA控制器章節。
學習本章時,配合《STM32F76xxx參考手冊》DMA控制器章節一起閱讀,效果會更佳,特別是涉及到寄存器說明的部分。本章內容專業名稱較多,內容豐富也較難理解,但非常有必要細讀研究。
特別說明,本章內容是以STM32F76xxx系列資源講解。
21.1 DMA簡介
DMA(Direct Memory Access,直接存儲區訪問)為實現數據高速在外設寄存器與存儲器之間或者存儲器與存儲器之間傳輸提供了高效的方法。之所以稱之為高效,是因為DMA傳輸實現高速數據移動過程無需任何CPU操作控制。從硬件層次上來說,DMA控制器是獨立於Cortex-M7內核的,有點類似GPIO、USART外設一般,只是DMA的功能是可以快速移動內存數據。
STM32F7xx系列的DMA功能齊全,工作模式眾多,適合不同編程環境要求。STM32F7xx系列的DMA支持外設到存儲器傳輸、存儲器到外設傳輸和存儲器到存儲器傳輸三種傳輸模式。這里的外設一般指外設的數據寄存器,比如ADC、SPI、I2C、DCMI等等外設的數據寄存器,存儲器一般是指片內SRAM、外部存儲器、片內Flash等等。
外設到存儲器傳輸就是把外設數據寄存器內容轉移到指定的內存空間。比如進行ADC采集時我們可以利用DMA傳輸把AD轉換數據轉移到我們定義的存儲區中,這樣對於多通道采集、采樣頻率高、連續輸出數據的AD采集是非常高效的處理方法。
存儲區到外設傳輸就是把特定存儲區內容轉移至外設的數據寄存器中,這種多用於外設的發送通信。
存儲器到存儲器傳輸就是把一個指定的存儲區內容拷貝到另一個存儲區空間。功能類似於C語言內存拷貝函數memcpy,利用DMA傳輸可以達到更高的傳輸效率,特別是DMA傳輸是不占用CPU的,可以節省很多CPU資源。
21.2 DMA功能框圖
STM32F7xx系列的DMA可以實現外設寄存器與存儲器之間或者存儲器與存儲器之間傳輸三種模式,這要得益於DMA控制器是采樣AHB主總線的,可以控制AHB總線矩陣來啟動AHB事務。圖 21-1為DMA控制器的框圖。
圖 21-1 DMA框圖
1. ①外設通道選擇
STM32F7xx系列資源豐富,具有兩個DMA控制器,同時外設繁多,為實現正常傳輸,DMA需要通道選擇控制。DMA控制器具有8個數據流,每個數據流可以提供多達16個外設請求。實際上STM32F767xx中DMA1只用了10個,DMA2只用了12個。在實現DMA傳輸之前,DMA控制器會通過DMA數據流x配置寄存器DMA_SxCR(x為0~7,對應8個DMA數據流)的CHSEL[3:0]位選擇對應的通道作為該數據流的目標外設。
外設通道選擇要解決的主要問題是決定哪一個外設作為該數據流的源地址或者目標地址。
DMA請求映射情況參考表 21-1和表 21-2。
表 21-1 DMA1請求映射
外設請求 |
數據流0 |
數據流1 |
數據流2 |
數據流3 |
數據流4 |
數據流5 |
數據流6 |
數據流7 |
通道0 |
SPI3_RX |
SPDIFRX_DT |
SPI3_RX |
SPI2_RX |
SPI2_TX |
SPI3_TX |
SPDIFRX_CS |
SPI3_TX |
通道1 |
I2C1_RX |
I2C3_RX |
TIM7_UP |
|
TIM7_UP |
I2C1_RX |
I2C1_TX |
I2C1_TX |
通道2 |
TIM4_CH1 |
|
I2S3_EXT_RX |
TIM4_CH2 |
|
|
TIM4_UP |
TIM4_CH3 |
通道3 |
- |
TIM2_UP |
I2C3_RX |
|
I2C3_TX |
TIM2_CH1 |
TIM2_CH2 |
TIM2_UP |
通道4 |
UART5_RX |
USART3_RX |
UART4_RX |
USART3_TX |
UART4_TX |
USART2_RX |
USART2_TX |
UART5_TX |
通道5 |
UART8_TX |
UART7_TX |
TIM3_CH4 |
UART7_RX |
TIM3_CH1 |
TIM3_CH2 |
UART8_RX |
TIM3_CH3 |
通道6 |
TIM5_CH3 |
TIM5_CH4 |
TIM5_CH1 |
TIM5_CH4 |
TIM5_CH2 |
|
TIM5_UP |
|
通道7 |
|
TIM6_UP |
I2C2_RX |
I2C2_RX |
USART3_TX |
DAC1 |
DAC2 |
I2C2_TX |
通道8 |
I2C3_TX |
I2C4_RX |
|
|
I2C2_TX |
|
I2C4_TX |
|
通道9 |
|
SPI2_RX |
|
|
|
|
SPI2_TX |
|
表 21-2 DMA2請求映射
外設請求 |
數據流0 |
數據流1 |
數據流2 |
數據流3 |
數據流4 |
數據流5 |
數據流6 |
數據流7 |
通道0 |
ADC1 |
SAI1_A |
TIM8_CH1 |
SAI1_A |
ADC1 |
SAI1_B |
TIM1_CH1 |
SAI2_B |
通道1 |
|
DCMI |
ADC2 |
ADC2 |
SAI1_B |
SPI6_TX |
SPI6_RX |
DCMI |
通道2 |
ADC3 |
ADC3 |
|
SPI5_TX |
SPI5_TX |
CRYP_OUT |
CRYP_IN |
HASH_IN |
通道3 |
SPI1_RX |
|
SPI1_RX |
SPI1_TX |
SAI2_A |
SPI1_TX |
SAI2_B |
QUADSPI |
通道4 |
SPI4_RX |
SPI4_TX |
USART1_RX |
SDMMC1 |
|
USART1_RX |
SDMMC1 |
USART1_TX |
通道5 |
|
USART6_RX |
USART6_RX |
SPI14_RX |
SPI4_TX |
|
USART6_TX |
USART6_TX |
通道6 |
TIM1_TRIG |
TIM1_CH1 |
TIM1_CH2 |
TIM1_CH1 |
TIM1_CH4 |
TIM1_UP |
TIM1_CH3 |
|
通道7 |
|
TIM8_UP |
TIM8_CH1 |
TIM8_CH2 |
TIM8_CH3 |
SPI5_RX |
SPI5_TX |
TIM8_CH4 |
通道8 |
DFSDM1_FLT0 |
DFSDM1_FLT1 |
DFSDM1_FLT2 |
DFSDM1_FLT3 |
DFSDM1_FLT0 |
DFSDM1_FLT1 |
DFSDM1_FLT2 |
DFSDM1_FLT3 |
通道9 |
JPEG_IN |
JPEG_OUT |
SPI4_TX |
JPEG_IN |
JPEG_OUT |
SPI5_RX |
|
|
通道10 |
SAI1_B |
SAI2_B |
SAI2_A |
|
|
|
SAI1_A |
|
通道11 |
SDMMC2 |
|
QUADSPI |
|
|
SDMMC2 |
|
|
每個外設請求都占用一個數據流通道,相同外設請求可以占用不同數據流通道。比如SPI3_RX請求,即SPI3數據發送請求,占用DMA1的數據流0的通道0,因此當我們使用該請求時,我們需要在把DMA_S0CR寄存器的CHSEL[3:0]設置為“0000”,此時相同數據流的其他通道不被選擇,處於不可用狀態,比如此時不能使用數據流0的通道1即I2C1_RX請求等等。
查閱表 211可以發現SPI3_RX請求不僅僅在數據流0的通道0,同時數據流2的通道0也是SPI3_RX請求,實際上其他外設基本上都有兩個對應數據流通道,這兩個數據流通道都是可選的,這樣設計是盡可能提供多個數據流同時使用情況選擇。
2. ②仲裁器
一個DMA控制器對應8個數據流,數據流包含要傳輸數據的源地址、目標地址、數據等等信息。如果我們需要同時使用同一個DMA控制器(DMA1或DMA2)多個外設請求時,那必然需要同時使用多個數據流,那究竟哪一個數據流具有優先傳輸的權利呢?這就需要仲裁器來管理判斷了。
仲裁器管理數據流方法分為兩個階段。第一階段屬於軟件階段,我們在配置數據流時可以通過寄存器設定它的優先級別,具體配置DMA_SxCR寄存器PL[1:0]位,可以設置為非常高、高、中和低四個級別。第二階段屬於硬件階段,如果兩個或以上數據流軟件設置優先級一樣,則他們優先級取決於數據流編號,編號越低越具有優先權,比如數據流2優先級高於數據流3。
3. ③FIFO
每個數據流都獨立擁有四級32位FIFO(先進先出存儲器緩沖區)。DMA傳輸具有FIFO模式和直接模式。
直接模式在每個外設請求都立即啟動對存儲器傳輸。在直接模式下,如果DMA配置為存儲器到外設傳輸那DMA會見一個數據存放在FIFO內,如果外設啟動DMA傳輸請求就可以馬上將數據傳輸過去。
FIFO用於在源數據傳輸到目標地址之前臨時存放這些數據。可以通過DMA數據流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位來控制FIFO的閾值,分別為1/4、1/2、3/4和滿。如果數據存儲量達到閾值級別時,FIFO內容將傳輸到目標中。
FIFO對於要求源地址和目標地址數據寬度不同時非常有用,比如源數據是源源不斷的字節數據,而目標地址要求輸出字寬度的數據,即在實現數據傳輸時同時把原來4個8位字節的數據拼湊成一個32位字數據。此時使用FIFO功能先把數據緩存起來,分別根據需要輸出數據。
FIFO另外一個作用使用於突發(burst)傳輸。
4. ④存儲器端口、⑤外設端口
DMA控制器實現雙AHB主接口,更好利用總線矩陣和並行傳輸。DMA控制器通過存儲器端口和外設端口與存儲器和外設進行數據傳輸,關系見錯誤!未找到引用源。。DMA控制器的功能是快速轉移內存數據,需要一個連接至源數據地址的端口和一個連接至目標地址的端口。
DMA2(DMA控制器2)的存儲器端口和外設端口都是連接到AHB總線矩陣,可以使用AHB總線矩陣功能。DMA2存儲器和外設端口可以訪問相關的內存地址,包括有內部Flash、內部SRAM、AHB1外設、AHB2外設、APB2外設和外部存儲器空間。
DMA1的存儲區端口相比DMA2的要減少AHB2外設的訪問權,同時DMA1外設端口是沒有連接至總線矩陣的,只有連接到APB1外設,所以DMA1不能實現存儲器到存儲器傳輸。
5. ⑥編程端口
AHB從器件編程端口是連接至AHB2外設的。AHB2外設在使用DMA傳輸時需要相關控制信號。
21.3 DMA數據配置
DMA工作模式多樣,具有多種可能工作模式,具體可能配置見表 21-3。
表 21-3 DMA配置可能情況
DMA傳輸模式 |
源 |
目標 |
流控制器 |
循環模式 |
傳輸類型 |
直接模式 |
雙緩沖模式 |
外設 到存儲器 |
AHB 外設端口 |
AHB 存儲器端口 |
DMA |
允許 |
單次 |
允許 |
允許 |
突發 |
禁止 |
||||||
外設 |
禁止 |
單次 |
允許 |
禁止 |
|||
突發 |
禁止 |
||||||
存儲器 到外設 |
AHB 存儲器端口 |
AHB 外設端口 |
DMA |
允許 |
單次 |
允許 |
允許 |
突發 |
禁止 |
||||||
外設 |
禁止 |
單次 |
允許 |
禁止 |
|||
突發 |
禁止 |
||||||
存儲器 到存儲器 |
AHB 存儲器端口 |
AHB 存儲器端口 |
僅DMA |
禁止 |
單次 |
禁止 |
禁止 |
突發 |
1. DMA傳輸模式
DMA2支持全部三種傳輸模式,而DMA1只有外設到存儲器和存儲器到外設兩種模式。模式選擇可以通過DMA_SxCR寄存器的DIR[1:0]位控制,進而將DMA_SxCR寄存器的EN位置1就可以使能DMA傳輸。
在DMA_SxCR寄存器的PSIZE[1:0]和MSIZE[1:0]位分別指定外設和存儲器數據寬度大小,可以指定為字節(8位)、半字(16位)和字(32位),我們可以根據實際情況設置。直接模式要求外設和存儲器數據寬度大小一樣,實際上在這種模式下DMA數據流直接使用PSIZE,MSIZE不被使用。
2. 源地址和目標地址
DMA數據流x外設地址DMA_SxPAR(x為0~7)寄存器用來指定外設地址,它是一個32位數據有效寄存器。DMA數據流x存儲器0地址DMA_SxM0AR(x為0~7) 寄存器和DMA數據流x存儲器1地址DMA_SxM1AR(x為0~7) 寄存器用來存放存儲器地址,其中DMA_SxM1AR只用於雙緩沖模式,DMA_SxM0AR和DMA_SxM1AR都是32位數據有效的。
當選擇外設到存儲器模式時,即設置DMA_SxCR寄存器的DIR[1:0] 位為“00”,DMA_SxPAR寄存器為外設地址,也是傳輸的源地址,DMA_SxM0AR寄存器為存儲器地址,也是傳輸的目標地址。對於存儲器到存儲器傳輸模式,即設置DIR[1:0] 位為“10”時,采用與外設到存儲器模式相同配置。而對於存儲器到外設,即設置DIR[1:0]位為“01”時,DMA_SxM0AR寄存器作為為源地址,DMA_SxPAR寄存器作為目標地址。
3. 流控制器
流控制器主要涉及到一個控制DMA傳輸停止問題。DMA傳輸在DMA_SxCR寄存器的EN位被置1后就進入准備傳輸狀態,如果有外設請求DMA傳輸就可以進行數據傳輸。很多情況下,我們明確知道傳輸數據的數目,比如要傳1000個或者2000個數據,這樣我們就可以在傳輸之前設置DMA_SxNDTR寄存器為要傳輸數目值,DMA控制器在傳輸完這么多數目數據后就可以控制DMA停止傳輸。
DMA數據流x數據項數DMA_SxNDTR(x為0~7)寄存器用來記錄當前仍需要傳輸數目,它是一個16位數據有效寄存器,即最大值為65535,這個值在程序設計是非常有用也是需要注意的地方。我們在編程時一般都會明確指定一個傳輸數量,在完成一次數目傳輸后DMA_SxNDTR計數值就會自減,當達到零時就說明傳輸完成。
如果某些情況下在傳輸之前我們無法確定數據的數目,那DMA就無法自動控制傳輸停止了,此時需要外設通過硬件通信向DMA控制器發送停止傳輸信號。這里有一個大前提就是外設必須是可以發出這個停止傳輸信號,只有SDIO才有這個功能,其他外設不具備此功能。
4. 循環模式
循環模式相對應於一次模式。一次模式就是傳輸一次就停止傳輸,下一次傳輸需要手動控制,而循環模式在傳輸一次后會自動按照相同配置重新傳輸,周而復始直至被控制停止或傳輸發生錯誤。
通過DMA_SxCR寄存器的CIRC位可以使能循環模式。
5. 傳輸類型
DMA傳輸類型有單次(Single)傳輸和突發(Burst)傳輸。突發傳輸就是用非常短時間結合非常高數據信號率傳輸數據,相對正常傳輸速度,突發傳輸就是在傳輸階段把速度瞬間提高,實現高速傳輸,在數據傳輸完成后恢復正常速度,有點類似達到數據塊“秒傳”效果。為達到這個效果突發傳輸過程要占用AHB總線,保證要求每個數據項在傳輸過程不被分割,這樣一次性把數據全部傳輸完才釋放AHB總線;而單次傳輸時必須通過AHB的總線仲裁多次控制才傳輸完成。
單次和突發傳輸數據使用具體情況參考表 214。其中PBURST[1:0]和MBURST[1:0]位是位於DMA_SxCR寄存器中的,用於分別設置外設和存儲器不同節拍數的突發傳輸,對應為單次傳輸、4個節拍增量傳輸、8個節拍增量傳輸和16個節拍增量傳輸。PINC位和MINC位是寄存器DMA_SxCR寄存器的第9和第10位,如果位被置1則在每次數據傳輸后數據地址指針自動遞增,其增量由PSIZE和MSIZE值決定,比如,設置PSIZE為半字大小,那么下一次傳輸地址將是前一次地址遞增2。
表 21-4 DMA傳輸類型
AHB主端口 |
項目 |
單次傳輸 |
突發傳輸 |
外設 |
寄存器 |
PBURST[1:0]=00,PINC無要求 |
PBURST[1:0]不為0,PINC必須為1 |
描述 |
每次DMA請求就傳輸一次字節/半字/字(取決於PSIZE)數據 |
每次DMA請求就傳輸4/8/16個(取決於PBURST[1:0])字節/半字/字(取決於PSIZE)數據 |
|
存儲器 |
寄存器 |
MBURST[1:0]=00,MINC無要求 |
MBURST[1:0]不為0,MINC必須為1 |
描述 |
每次DMA請求就傳輸一次字節/半字/字(取決於MSIZE)數據 |
每次DMA請求就傳輸4/8/16個(取決於MBURST[1:0])字節/半字/字(取決於MSIZE)數據 |
突發傳輸與FIFO密切相關,突發傳輸需要結合FIFO使用,具體要求FIFO閾值一定要是內存突發傳輸數據量的整數倍。FIFO閾值選擇和存儲器突發大小必須配合使用,具體參考表 21-5。
表 21-5 FIFO閾值配置
MSIZE |
FIFO級別 |
MBURST=INCR4 |
MBURST=INCR8 |
MBURST=INCR16 |
字節 |
1/4 |
4個節拍的1次突發 |
禁止 |
禁止 |
1/2 |
4個節拍的2次突發 |
8個節拍的1次突發 |
||
3/4 |
4個節拍的3次突發 |
禁止 |
||
滿 |
4個節拍的4次突發 |
8個節拍的2次突發 |
16個節拍的1次突發 |
|
半字 |
1/4 |
禁止 |
禁止 |
禁止 |
1/2 |
4個節拍的1次突發 |
|||
3/4 |
禁止 |
|||
滿 |
4個節拍的2次突發 |
8個節拍的1次突發 |
||
字 |
1/4 |
禁止 |
禁止 |
|
1/2 |
||||
3/4 |
||||
滿 |
4個節拍的1次突發 |
6. 直接模式
默認情況下,DMA工作在直接模式,不使能FIFO閾值級別。
直接模式在每個外設請求都立即啟動對存儲器傳輸的單次傳輸。直接模式要求源地址和目標地址的數據寬度必須一致,所以只有PSIZE控制,而MSIZE值被忽略。突發傳輸是基於FIFO的所以直接模式不被支持。另外直接模式不能用於存儲器到存儲器傳輸。
在直接模式下,如果DMA配置為存儲器到外設傳輸那DMA會見一個數據存放在FIFO內,如果外設啟動DMA傳輸請求就可以馬上將數據傳輸過去。
7. 雙緩沖模式
設置DMA_SxCR寄存器的DBM位為1可啟動雙緩沖傳輸模式,並自動激活循環模式。雙緩沖不應用與存儲器到存儲器的傳輸。雙緩沖模式下,兩個存儲器地址指針都有效,即DMA_SxM1AR寄存器將被激活使用。開始傳輸使用DMA_SxM0AR寄存器的地址指針所對應的存儲區,當這個存儲區數據傳輸完DMA控制器會自動切換至DMA_SxM1AR寄存器的地址指針所對應的另一塊存儲區,如果這一塊也傳輸完成就再切換至DMA_SxM0AR寄存器的地址指針所對應的存儲區,這樣循環調用。
當其中一個存儲區傳輸完成時都會把傳輸完成中斷標志TCIF位置1,如果我們使能了DMA_SxCR寄存器的傳輸完成中斷,則可以產生中斷信號,這個對我們編程非常有用。另外一個非常有用的信息是DMA_SxCR寄存器的CT位,當DMA控制器是在訪問使用DMA_SxM0AR時CT=0,此時CPU不能訪問DMA_SxM0AR,但可以向DMA_SxM1AR填充或者讀取數據;當DMA控制器是在訪問使用DMA_SxM1AR時CT=1,此時CPU不能訪問DMA_SxM1AR,但可以向DMA_SxM0AR填充或者讀取數據。另外在未使能DMA數據流傳輸時,可以直接寫CT位,改變開始傳輸的目標存儲區。
雙緩沖模式應用在需要解碼程序的地方是非常有效的。比如MP3格式音頻解碼播放,MP3是被壓縮的文件格式,我們需要特定的解碼庫程序來解碼文件才能得到可以播放的PCM信號,解碼需要一定的時間,按照常規方法是讀取一段原始數據到緩沖區,然后對緩沖區內容進行解碼,解碼后才輸出到音頻播放電路,這種流程對CPU運算速度要求高,很容易出現播放不流暢現象。如果我們使用DMA雙緩沖模式傳輸數據就可以非常好的解決這個問題,達到解碼和輸出音頻數據到音頻電路同步進行的效果。
8. DMA中斷
每個DMA數據流可以在發送以下事件時產生中斷:
1) 達到半傳輸:DMA數據傳輸達到一半時HTIF標志位被置1,如果使能HTIE中斷控制位將產生達到半傳輸中斷;
2) 傳輸完成:DMA數據傳輸完成時TCIF標志位被置1,如果使能TCIE中斷控制位將產生傳輸完成中斷;
3) 傳輸錯誤:DMA訪問總線發生錯誤或者在雙緩沖模式下試圖訪問“受限”存儲器地址寄存器時TEIF標志位被置1,如果使能TEIE中斷控制位將產生傳輸錯誤中斷;
4) FIFO錯誤:發生FIFO下溢或者上溢時FEIF標志位被置1,如果使能FEIE中斷控制位將產生FIFO錯誤中斷;
5) 直接模式錯誤:在外設到存儲器的直接模式下,因為存儲器總線沒得到授權,使得先前數據沒有完成被傳輸到存儲器空間上,此時DMEIF標志位被置1,如果使能DMEIE中斷控制位將產生直接模式錯誤中斷。
21.4 DMA初始化結構體詳解
標准庫函數對每個外設都建立了一個初始化結構體xxx_InitTypeDef(xxx為外設名稱),結構體成員用於設置外設工作參數,並由標准庫函數xxx_Init()調用這些設定參數進入設置外設相應的寄存器,達到配置外設工作環境的目的。
結構體xxx_InitTypeDef和庫函數xxx_Init配合使用是標准庫精髓所在,理解了結構體xxx_InitTypeDef每個成員意義基本上就可以對該外設運用自如了。結構體xxx_InitTypeDef定義在stm32f7xx_xxx.h(后面xxx為外設名稱)文件中,庫函數xxx_Init定義在stm32f7xx_xxx.c文件中,編程時我們可以結合這兩個文件內注釋使用。
DMA_ InitTypeDef初始化結構體
1 typedef struct {
2 uint32_t Channel; //通道選擇
5 uint32_t Direction; //傳輸方向
6 uint32_t DMA_BufferSize; //數據數目
7 uint32_t PeriphInc; //外設遞增
8 uint32_t MemInc; //存儲器遞增
9 uint32_t PeriphDataAlignment; //外設數據寬度
10 uint32_t MemDataAlignment; //存儲器數據寬度
11 uint32_t Mode; //模式選擇
12 uint32_t Priority; //優先級
13 uint32_t FIFOMode; //FIFO模式
14 uint32_t FIFOThreshold; //FIFO閾值
15 uint32_t MemBurst; //存儲器突發傳輸
16 uint32_t PeriphBurst; //外設突發傳輸
17 } DMA_InitTypeDef;
1) Channel:DMA請求通道選擇,可選通道0至通道15,每個外設對應固定的通道,具體設置值需要查表 21-1和表 21-2;它設定DMA_SxCR寄存器的CHSEL[3:0]位的值。例如,我們使用模擬數字轉換器ADC3規則采集4個輸入通道的電壓數據,查表 21-2可知使用通道2。
2) Direction:傳輸方向選擇,可選外設到存儲器、存儲器到外設以及存儲器到存儲器。它設定DMA_SxCR寄存器的DIR[1:0]位的值。ADC采集顯然使用外設到存儲器模式。
3) PeriphInc:如果配置為PeriphInc_Enable,使能外設地址自動遞增功能,它設定DMA_SxCR寄存器的PINC位的值;一般外設都是只有一個數據寄存器,所以一般不會使能該位。ADC3的數據寄存器地址是固定並且只有一個所以不使能外設地址遞增。
4) MemInc:如果配置為MemInc_Enable,使能存儲器地址自動遞增功能,它設定DMA_SxCR寄存器的MINC位的值;我們自定義的存儲區一般都是存放多個數據的,所以使能存儲器地址自動遞增功能。我們之前已經定義了一個包含4個元素的數字用來存放數據,使能存儲區地址遞增功能,自動把每個通道數據存放到對應數組元素內。
5) PeriphDataAlignment:外設數據寬度,可選字節(8位)、半字(16位)和字(32位),它設定DMA_SxCR寄存器的PSIZE[1:0]位的值。ADC數據寄存器只有低16位數據有效,使用半字數據寬度。
6) MemDataAlignment:存儲器數據寬度,可選字節(8位)、半字(16位)和字(32位),它設定DMA_SxCR寄存器的MSIZE[1:0]位的值。保存ADC轉換數據也要使用半字數據寬度,這跟我們定義的數組是相對應的。
7) Mode:DMA傳輸模式選擇,可選一次傳輸或者循環傳輸,它設定DMA_SxCR寄存器的CIRC位的值。我們希望ADC采集是持續循環進行的,所以使用循環傳輸模式。
8) Priority:軟件設置數據流的優先級,有4個可選優先級分別為非常高、高、中和低,它設定DMA_SxCR寄存器的PL[1:0]位的值。DMA優先級只有在多個DMA數據流同時使用時才有意義,這里我們設置為非常高優先級就可以了。
9) FIFOMode:FIFO模式使能,如果設置為DMA_FIFOMode_Enable表示使能FIFO模式功能;它設定DMA_SxFCR寄存器的DMDIS位。ADC采集傳輸使用直接傳輸模式即可,不需要使用FIFO模式。
10) FIFOThreshold:FIFO閾值選擇,可選4種狀態分別為FIFO容量的1/4、1/2、3/4和滿;它設定DMA_SxFCR寄存器的FTH[1:0]位; DMA_FIFOMode設置為DMA_FIFOMode_Disable,那DMA_FIFOThreshold值無效。ADC采集傳輸不使用FIFO模式,設置改值無效。
11) MemBurst:存儲器突發模式選擇,可選單次模式、4節拍的增量突發模式、8節拍的增量突發模式或16節拍的增量突發模式,它設定DMA_SxCR寄存器的MBURST[1:0]位的值。ADC采集傳輸是直接模式,要求使用單次模式。
12) PeriphBurst:外設突發模式選擇,可選單次模式、4節拍的增量突發模式、8節拍的增量突發模式或16節拍的增量突發模式,它設定DMA_SxCR寄存器的PBURST[1:0]位的值。ADC采集傳輸是直接模式,要求使用單次模式。
DMA_HandleTypeDef初始化結構體
1 typedef struct __DMA_HandleTypeDef {
2 DMA_Stream_TypeDef *Instance; //注冊基地址
5 DMA_InitTypeDef Init; //DMA通信參數
6 HAL_LockTypeDef Lock; //DMA鎖定對象
7 __IO HAL_DMA_StateTypeDef State; //DMA傳輸狀態
8 void *Parent; //父類指針
9 void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
10 //DMA傳輸完成回調函數
11 void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); 12 //DMA傳輸完成一半回調函數
13 void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
14 //Memory1 DMA傳輸完成回調函數
15 void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); 16 //Memory1 DMA傳輸完成一半回調函數
17 void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma); 18 //DMA傳輸錯誤回調函數
19 void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma); 20 //DMA傳輸中止回調函數
21 __IO uint32_t ErrorCode; //DMA錯誤碼
22 uint32_t StreamBaseAddress; //DMA數據流基地址
23 uint32_t StreamIndex; //DMA數據流索引
24 } DMA_HandleTypeDef;
1) *Instance: 指向DMA數據流基地址的指針,即指定使用哪個DMA數據流。可選數據流0至數據流7。例如,我們使用模擬數字轉換器ADC3規則采集4個輸入通道的電壓數據,查表 212可知可以使用數據流0或者數據流1,這里支持兩個數據流是為了避免多個通道使用時發生沖突,提供備選數據流可選。
2) Init:這里包含上面介紹DMA_InitTypeDef結構體的所有參數的初始化。
3) Lock:DMA鎖定對象。DMA進程鎖,通常都在DMA傳輸設置開始前鎖上進程鎖,設置完畢后釋放進程鎖。
4) State:DMA傳輸狀態。它包含六種狀態,1、復位狀態,尚未初始化或者禁能。2、就緒狀態,已經完成初始化,隨時可以傳輸數據。3、傳輸忙,DMA傳輸進程正在進行。4、傳輸超時狀態。5、傳輸錯誤狀態。6、傳輸中止狀態。
5) *Parent:父類指針。只要將該指針指向一些ADC、UART等外設的handle類,就等於完成了繼承。
6) DMA傳輸過程中的回調函數。包括傳輸完成,傳輸完成一半,傳輸錯誤,傳輸中止回調函數。這些回調函數中可以加入用戶的處理代碼。
7) ErrorCode:DMA錯誤碼,包含無錯誤:HAL_DMA_ERROR_NONE,傳輸錯誤HAL_DMA_ERROR_TE,FIFO錯誤HAL_DMA_ERROR_FE,直接模式錯誤:HAL_DMA_ERROR_DME,超時錯誤:HAL_DMA_ERROR_TIMEOUT,參數錯誤:HAL_DMA_ERROR_PARAM,沒有回調函數正在執行退出請求錯誤:HAL_DMA_ERROR_NO_XFER,不支持模式錯誤:HAL_DMA_ERROR_NOT_SUPPORTED。
8) StreamBaseAddress:DMA數據流基地址,用來根據定義句柄計算數據流的基地址。
9) StreamIndex:DMA數據流索引,根據數據流的序號來確定數據流的偏移地址。
21.5 DMA存儲器到存儲器模式實驗
DMA工作模式多樣,具體如何使用需要配合實際傳輸條件具體分析。接下來我們通過兩個實驗詳細講解DMA不同模式下的使用配置,加深我們對DMA功能的理解。
DMA運行高效,使用方便,在很多測試實驗都會用到,這里先詳解存儲器到存儲器和存儲器到外設這兩種模式,其他功能模式在其他章節會有很多使用到的情況,也會有相關的分析。
存儲器到存儲器模式可以實現數據在兩個內存的快速拷貝。我們先定義一個靜態的源數據,然后使用DMA傳輸把源數據拷貝到目標地址上,最后對比源數據和目標地址的數據,看看是否傳輸准確。
21.5.1 硬件設計
DMA存儲器到存儲器實驗不需要其他硬件要求,只用到RGB彩色燈用於指示程序狀態,關於RGB彩色燈電路可以參考GPIO章節。
21.5.2 軟件設計
這里只講解核心的部分代碼,有些變量的設置,頭文件的包含等並沒有涉及到,完整的代碼請參考本章配套的工程。這個實驗代碼比較簡單,主要程序代碼都在main.c文件中。
1. 編程要點
1) 使能DMA數據流時鍾並復位初始化DMA數據流;
2) 配置DMA數據流參數;
3) 使能DMA數據流,進行傳輸;
4) 等待傳輸完成,並對源數據和目標地址數據進行比較。
2. 代碼分析
DMA宏定義及相關變量定義
代碼清單 21-1 DMA數據流和相關變量定義
1 /* 相關宏定義,使用存儲器到存儲器傳輸必須使用DMA2 */
2 DMA_HandleTypeDef DMA_Handle;
3
4 #define DMA_STREAM DMA2_Stream0
5 #define DMA_CHANNEL DMA_CHANNEL_0
6 #define DMA_STREAM_CLOCK() __DMA2_CLK_ENABLE()
7
8 #define BUFFER_SIZE 32
9
10 /* 定義aSRC_Const_Buffer數組作為DMA傳輸數據源
11 const關鍵字將aSRC_Const_Buffer數組變量定義為常量類型 */
12 const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= {
13 0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
14 0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
15 0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
16 0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
17 0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
18 0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
19 0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
20 0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80
21 };
22 /* 定義DMA傳輸目標存儲器 */
23 uint32_t aDST_Buffer[BUFFER_SIZE];
使用宏定義設置外設配置方便程序修改和升級。
存儲器到存儲器傳輸必須使用DMA2,但對數據流編號以及通道選擇就沒有硬性要求,可以自由選擇。
aSRC_Const_Buffer[BUFFER_SIZE]是定義用來存放源數據的,並且使用了const關鍵字修飾,即常量類型,使得變量是存儲在內部flash空間上。
DMA數據流配置
代碼清單 212 DMA傳輸參數配置
1 static void DMA_Config(void)
2 {
3 HAL_StatusTypeDef DMA_status = HAL_ERROR;
4 /* 使能DMA時鍾 */
5 DMA_STREAM_CLOCK();
6
7 DMA_Handle.Instance = DMA_STREAM;
8 /* DMA數據流通道選擇 */
9 DMA_Handle.Init .Channel = DMA_CHANNEL;
10 /* 存儲器到存儲器模式 */
11 DMA_Handle.Init.Direction = DMA_MEMORY_TO_MEMORY;
12 /* 使能自動遞增功能 */
13 DMA_Handle.Init.PeriphInc = DMA_PINC_ENABLE;
14 /* 使能自動遞增功能 */
15 DMA_Handle.Init.MemInc = DMA_MINC_ENABLE;
16 /* 源數據是字大小(32位) */
17 DMA_Handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
18 /* 目標數據也是字大小(32位) */
19 DMA_Handle.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
20 /* 一次傳輸模式,存儲器到存儲器模式不能使用循環傳輸 */
21 DMA_Handle.Init.Mode = DMA_NORMAL;
22 /* DMA數據流優先級為高 */
23 DMA_Handle.Init.Priority = DMA_PRIORITY_HIGH;
24 /* 禁用FIFO模式 */
25 DMA_Handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
26 DMA_Handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
27 /* 單次模式 */
28 DMA_Handle.Init.MemBurst = DMA_MBURST_SINGLE;
29 /* 單次模式 */
30 DMA_Handle.Init.PeriphBurst = DMA_PBURST_SINGLE;
31 /* 完成DMA數據流參數配置 */
32 HAL_DMA_Init(&DMA_Handle);
33
34 DMA_status = HAL_DMA_Start(&DMA_Handle,(uint32_t)aSRC_Const_Buffer,
35 (uint32_t)aDST_Buffer,BUFFER_SIZE);
36 /* 判斷DMA狀態 */
37 if (DMA_status != HAL_OK) {
38 /* DMA出錯就讓程序運行下面循環:RGB彩色燈閃爍 */
39 while (1) {
40 LED_RED;
41 Delay(0xFFFFFF);
42 LED_RGBOFF;
43 Delay(0xFFFFFF);
44 }
45 }
46}
使用DMA_ DMA_HandleTypeDef結構體定義一個DMA數據流初始化變量,這個結構體內容我們之前已經有詳細講解。
調用DMA_STREAM_CLOCK函數開啟DMA數據流時鍾,使用DMA控制器之前必須開啟對應的時鍾。
存儲器到存儲器模式通道選擇沒有具體規定,只能使用一次傳輸模式不能循環傳輸,最后我調用HAL_DMA_Init函數完成DMA數據流的初始化配置。
HAL_DMA_Start函數用於啟動DMA數據流傳輸,源地址和目標地址使用之前定義的數組首地址,返回DMA傳輸狀態。
如果DMA傳輸沒有就緒就會閃爍RGB彩燈提示。
存儲器數據對比
代碼清單 21-3 源數據與目標地址數據對比
1 uint8_t Buffercmp(const uint32_t* pBuffer,
2 uint32_t* pBuffer1, uint16_t BufferLength)
3 {
4 /* 數據長度遞減 */
5 while (BufferLength--) {
6 /* 判斷兩個數據源是否對應相等 */
7 if (*pBuffer != *pBuffer1) {
8 /* 對應數據源不相等馬上退出函數,並返回0 */
9 return 0;
10 }
11 /* 遞增兩個數據源的地址指針 */
12 pBuffer++;
13 pBuffer1++;
14 }
15 /* 完成判斷並且對應數據相對 */
16 return 1;
17 }
判斷指定長度的兩個數據源是否完全相等,如果完全相等返回1;只要其中一對數據不相等返回0。它需要三個形參,前兩個是兩個數據源的地址,第三個是要比較數據長度。
主函數
代碼清單 21-4 存儲器到存儲器模式主函數
1 int main(void)
2 {
3 /* 定義存放比較結果變量 */
4 uint8_t TransferStatus;
5 /* 系統時鍾初始化成216 MHz */
6 SystemClock_Config();
7 /* LED 端口初始化 */
8 LED_GPIO_Config();
9 /* 設置RGB彩色燈為紫色 */
10 LED_PURPLE;
11
12 /* 簡單延時函數 */
13 Delay(0xFFFFFF);
14
15 /* DMA傳輸配置 */
16 DMA_Config();
17
18 /* 等待DMA傳輸完成 */
19 while (__HAL_DMA_GET_FLAG(&DMA_Handle,DMA_FLAG_TCIF0_4)==DISABLE) {
20
21 }
22
23 /* 比較源數據與傳輸后數據 */
24 TransferStatus=Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE);
25
26 /* 判斷源數據與傳輸后數據比較結果*/
27 if (TransferStatus==0) {
28 /* 源數據與傳輸后數據不相等時RGB彩色燈顯示紅色 */
29 LED_RED;
30 } else {
31 /* 源數據與傳輸后數據相等時RGB彩色燈顯示藍色 */
32 LED_BLUE;
33 }
34
35 while (1) {
36 }
37 }
首先定義一個變量用來保存存儲器數據比較結果。
SystemClock_Config函數初始化系統時鍾。
RGB彩色燈用來指示程序進程,使用之前需要初始化它,LED_GPIO_Config定義在bsp_led.c文件中。開始設置RGB彩色燈為紫色,LED_PURPLE是定義在bsp_led.h文件的一個宏定義。
Delay函數只是一個簡單的延時函數。
調用DMA_Config函數完成DMA數據流配置並啟動DMA數據傳輸。
__HAL_DMA_GET_FLAG函數獲取DMA數據流事件標志位的當前狀態,這里獲取DMA數據傳輸完成這個標志位,使用循環持續等待直到該標志位被置位,即DMA傳輸完成這個事件發生,然后退出循環,運行之后程序。
確定DMA傳輸完成之后就可以調用Buffercmp函數比較源數據與DMA傳輸后目標地址的數據是否一一對應。TransferStatus保存比較結果,如果為1表示兩個數據源一一對應相等說明DMA傳輸成功;相反,如果為0表示兩個數據源數據存在不等情況,說明DMA傳輸出錯。
如果DMA傳輸成功設置RGB彩色燈為藍色,如果DMA傳輸出錯設置RGB彩色燈為紅色。
21.5.3 下載驗證
確保開發板供電正常,編譯程序並下載。觀察RGB彩色燈變化情況。正常情況下RGB彩色燈先為紫色,然后變成藍色。如果DMA傳輸出錯才會為紅色。
21.6 DMA存儲器到外設模式實驗
DMA存儲器到外設傳輸模式非常方便把存儲器數據傳輸外設數據寄存器中,這在STM32芯片向其他目標主機,比如電腦、另外一塊開發板或者功能芯片,發送數據是非常有用的。RS-232串口通信是我們常用開發板與PC端通信的方法。我們可以使用DMA傳輸把指定的存儲器數據轉移到USART數據寄存器內,並發送至PC端,在串口調試助手顯示。
21.6.1 硬件設計
存儲器到外設模式使用到USART1功能,具體電路設置參考USART章節,無需其他硬件設計。
21.6.2 軟件設計
這里只講解核心的部分代碼,有些變量的設置,頭文件的包含等並沒有涉及到,完整的代碼請參考本章配套的工程。我們編寫兩個串口驅動文件bsp_usart_dma.c和bsp_usart_dma.h,有關串口和DMA的宏定義以及驅動函數都在里邊。
1. 編程要點
1) 配置USART通信功能;
2) 設置DMA為存儲器到外設模式,設置數據流通道,指定USART數據寄存器為目標地址,循環發送模式;
3) 使能DMA數據流;
4) 使能USART的DMA發送請求;
5) DMA傳輸同時CPU可以運行其他任務。
2. 代碼分析
USART和DMA宏定義
代碼清單 215 USART和DMA相關宏定義
1 //串口波特率
2 #define DEBUG_USART_BAUDRATE 115200
3 //引腳定義
4 /*******************************************************/
5 #define DEBUG_USART USART1
6 #define DEBUG_USART_CLK_ENABLE() __USART1_CLK_ENABLE();
7
8 #define DEBUG_USART_RX_GPIO_PORT GPIOA
9 #define DEBUG_USART_RX_GPIO_CLK_ENABLE() __GPIOA_CLK_ENABLE()
10 #define DEBUG_USART_RX_PIN GPIO_PIN_10
11 #define DEBUG_USART_RX_AF GPIO_AF7_USART1
12
13 #define DEBUG_USART_TX_GPIO_PORT GPIOA
14 #define DEBUG_USART_TX_GPIO_CLK_ENABLE() __GPIOA_CLK_ENABLE()
15 #define DEBUG_USART_TX_PIN GPIO_PIN_9
16 #define DEBUG_USART_TX_AF GPIO_AF7_USART1
17
18 #define DEBUG_USART_IRQHandler USART1_IRQHandler
19 #define DEBUG_USART_IRQ USART1_IRQn
20 /************************************************************/
21 //DMA
22 #define SENDBUFF_SIZE 5000 //發送的數據量
23 #define DEBUG_USART_DMA_CLK_ENABLE() __DMA2_CLK_ENABLE()
24 #define DEBUG_USART_DMA_CHANNEL DMA_CHANNEL_4
25 #define DEBUG_USART_DMA_STREAM DMA2_Stream7
使用宏定義設置外設配置方便程序修改和升級。
USART部分設置與USART章節內容相同,可以參考USART章節內容理解。
查閱表 21-2可知USART1對應DMA2的數據流7通道4。
串口DMA傳輸配置
代碼清單 21-6 USART1 發送請求DMA設置
1 void USART_DMA_Config(void)
2 {
3
4 /*開啟DMA時鍾*/
5 DEBUG_USART_DMA_CLK_ENABLE();
6
7 DMA_Handle.Instance = DEBUG_USART_DMA_STREAM;
8 /*usart1 tx對應dma2,通道4,數據流7*/
9 DMA_Handle.Init.Channel = DEBUG_USART_DMA_CHANNEL;
10 /*方向:從內存到外設*/
11 DMA_Handle.Init.Direction= DMA_MEMORY_TO_PERIPH;
12 /*外設地址不增*/
13 DMA_Handle.Init.PeriphInc = DMA_PINC_DISABLE;
14 /*內存地址自增*/
15 DMA_Handle.Init.MemInc = DMA_MINC_ENABLE;
16 /*外設數據單位*/
17 DMA_Handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
18 /*內存數據單位 8bit*/
19 DMA_Handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
20 /*DMA模式:不斷循環*/
21 DMA_Handle.Init.Mode = DMA_CIRCULAR;
22 /*優先級:中*/
23 DMA_Handle.Init.Priority = DMA_PRIORITY_MEDIUM;
24 /*禁用FIFO*/
25 DMA_Handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
26 DMA_Handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
27 /*存儲器突發傳輸 16個節拍*/
28 DMA_Handle.Init.MemBurst = DMA_MBURST_SINGLE;
29 /*外設突發傳輸 1個節拍*/
30 DMA_Handle.Init.PeriphBurst = DMA_PBURST_SINGLE;
31 /*配置DMA2的數據流7*/
32 /* Deinitialize the stream for new transfer */
33 HAL_DMA_DeInit(&DMA_Handle);
34 /* Configure the DMA stream */
35 HAL_DMA_Init(&DMA_Handle);
36
37 /* Associate the DMA handle */
38 __HAL_LINKDMA(&UartHandle, hdmatx, DMA_Handle);
39
40 }
使用DMA_HandleTypeDef結構體定義一個DMA數據流初始化變量,這個結構體內容我們之前已經有詳細講解。
調用DEBUG_USART_DMA_CLK_ENABLE宏開啟DMA數據流時鍾,使用DMA控制器之前必須開啟對應的時鍾。
USART有固定的DMA通道,USART數據寄存器地址也是固定的,外設地址不可以使用自動遞增,源數據使用我們自定義的數組空間,存儲器地址使用自動遞增,采用循環發送模式,最后我調用HAL_DMA_DeInit函數復位到缺省配置狀態,DMA_Init函數完成DMA數據流的初始化配置。
__HAL_LINKDMA函數用於鏈接DMA數據流及通道到串口外設通道上。
主函數
代碼清單 21-7 存儲器到外設模式主函數
1 int main(void)
2 {
3 uint16_t i;
4
5 /* 系統時鍾初始化成216 MHz */
6 SystemClock_Config();
7 /* 初始化USART */
8 Debug_USART_Config();
9
10 /* 配置使用DMA模式 */
11 USART_DMA_Config();
12
13 /* 配置RGB彩色燈 */
14 LED_GPIO_Config();
15
16 printf("\r\n USART1 DMA TX 測試 \r\n");
17
18 /*填充將要發送的數據*/
19 for (i=0; i<SENDBUFF_SIZE; i++) {
20 SendBuff[i] = 'A';
21
22 }
23
24
25 /*為演示DMA持續運行而CPU還能處理其它事情,持續使用DMA發送數據,量非常大,
26 *長時間運行可能會導致電腦端串口調試助手會卡死,鼠標亂飛的情況,
27 *或把DMA配置中的循環模式改為單次模式*/
28
29 HAL_UART_Transmit_DMA (&UartHandle,(uint8_t *)SendBuff,SENDBUFF_SIZE);
30 /* 此時CPU是空閑的,可以干其他的事情 */
31 //例如同時控制LED
32 while (1) {
33 LED1_TOGGLE
34 Delay(0xFFFFFF);
35 }
36 }
SystemClock_Config函數初始化系統時鍾。
Debug_USART_Config函數定義在bsp_usart_dma.c中,它完成USART初始化配置,包括GPIO初始化,USART通信參數設置等等,具體可參考USART章節講解。
USART_DMA_Config函數也是定義在bsp_usart_dma.c中,之前我們已經詳細分析了。
LED_GPIO_Config函數定義在bsp_led.c中,它完成RGB彩色燈初始化配置,具體可參考GPIO章節講解。
使用for循環填充源數據,SendBuff[SENDBUFF_SIZE]是一個全局無符號8位整數數組,是DMA傳輸的源數據。
HAL_UART_Transmit_DMA函數用於啟動USART的DMA傳輸。只需要指定源數據地址及長度,運行該函數后USART的DMA發送傳輸就開始了,根據配置它會通過USART循環發送數據。
DMA傳輸過程是不占用CPU資源的,可以一邊傳輸一次運行其他任務。
21.6.3 下載驗證
保證開發板相關硬件連接正確,用USB線連接開發板“USB TO UART”接口跟電腦,在電腦端打開串口調試助手,把編譯好的程序下載到開發板。程序運行后在串口調試助手可接收到大量的數據,同時開發板上RGB彩色燈不斷閃爍。
這里要注意為演示DMA持續運行並且CPU還能處理其它事情,持續使用DMA發送數據,量非常大,長時間運行可能會導致電腦端串口調試助手會卡死,鼠標亂飛的情況,所以在測試時最好把串口調試助手的自動清除接收區數據功能勾選上或把DMA配置中的循環模式改為單次模式。