一文講透 DMA,超級詳細!!!

嵌入式開發中,DMA 是個繞不開的話題。今天我們不講枯燥的配置步驟,畢竟每個處理器、每個外設的 DMA 實現都不盡相同。我們目標是帶大家搞懂 DMA 的本質、它爲什麼能讓系統更快。

數據搬運工的煩惱

想象一下,你在開發一個數據採集系統,比如一個工業監控設備。核心任務是從 ADC 採集數據,通過 SPI 存到 SD 卡,同時通過 USB 傳到上位機,UART 還得處理一些命令交互。這種場景在工業控制、物聯網設備非常常見。

在這個系統中,CPU 就像個大忙人。每次 ADC 採集完數據,就會觸發中斷,CPU 得趕緊跑去把數據從 ADC 的寄存器搬到內存,再從內存搬到 SPI 接口,存到 SD 卡,或者通過 USB 發出去。整個過程,CPU 就像個勤勤懇懇的搬運工,每字節數據都要親手摸一遍。

問題來了:CPU 可是個高智商選手,幹這種機械的搬運活兒,簡直是大材小用,更別說搬運數據佔用了大量 CPU 時間,留給真正有價值的工作,比如數據分析、算法處理的時間就少了。更煩的是,如果數據量大、傳輸頻繁,CPU 可能會忙到連喝口水的時間都沒有,系統性能直接拉胯。

這時候,DMA 就登場了。它就像僱了個專職物流小哥,把搬運數據的活兒全包了,讓 CPU 能專心幹大事,比如跑算法、優化邏輯。

DMA 是什麼,怎麼理解

要理解 DMA,我們先拋開技術術語,用個貼近生活的比喻:假設你每天得去幹洗店取衣服,拿回家掛進衣櫃。活兒不復雜,但每週跑幾次,時間久了就煩。你巴不得找個幫手,把這事外包出去。有一天,你老闆看你忙得焦頭爛額,給你配了個智能快遞小哥。這個小哥很專精,只能幹特定的事兒,比如取乾洗衣服。你得先給他詳細交代:

這些信息有些是一次性交代的,比如干洗店地址、衣櫃滿了的預案;有些是每次取衣服時得更新,比如這次要取幾件衣服,什麼時候去取。交代完,小哥就自動去幹活,幹完還發個微信告訴你:衣服到手了,主人可以穿新衣服了!

這個智能快遞小哥,就是 DMA 的化身。DMA 全稱是直接內存訪問,顧名思義,它讓外設和內存之間直接傳輸數據,不用每次都麻煩處理器。處理器只負責給 DMA 下命令,比如告訴它數據從哪兒搬到哪兒、搬多少、搬完咋通知。DMA 接手後,處理器就可以去幹別的了,等 DMA 幹完活兒再通過中斷通知處理器:數據搞定,過來驗收!

DMA 的魅力

回到我們的工業監控系統。正常情況下,ADC 採集到數據,處理器得親自從 ADC 寄存器讀數據,存到內存,再從內存搬到 SPI 或者 USB 接口。這過程就像你親自跑乾洗店,費時費力。如果用了 DMA,處理器只需要配置一次 DMA 控制器,比如告訴它:

配置好後,DMA 就像快遞小哥,默默把數據從 ADC 搬到內存,或者從內存搬到 SD 卡、USB 接口。處理器完全不用管搬運細節,可以專心處理數據分析、算法優化,或者響應用戶命令。

這帶來的好處顯而易見:

  1. 解放 CPU:搬運數據的活兒交給 DMA,處理器可以把寶貴的計算資源用在刀刃上,比如跑複雜的控制算法。

  2. 提升效率:DMA 傳輸數據不依賴處理器,速度更快,尤其在高頻、大數據量場景下,效果立竿見影。

  3. 降低延遲:外設不用等處理器有空,直接通過 DMA 和內存交互,數據傳輸更及時。

舉個例子,假設你在搞一個基於 STM32 的溫溼度監控系統,ADC 採樣率 10kHz,每次採樣 16 位數據。如果全靠 CPU 搬運,CPU 得頻繁響應中斷,忙得不可開交。用了 DMA,處理器只需要配置好 DMA 通道,數據自動從 ADC 流到內存,搬完後 DMA 觸發一次中斷,通知處理器處理這批數據。CPU 輕鬆了不少,系統實時性也更強。

DMA 的代價

當然,DMA 也不是萬能靈藥。配置 DMA 就像給快遞小哥交代任務,得事無鉅細。比如在 STM32 上,你需要設置 DMA 通道、源地址、目標地址、傳輸方向、數據寬度、觸發條件等等。稍微配錯一點,數據可能搬到不知名的地方,或者壓根沒搬,數據都不知道去哪了。

而且,不同處理器的 DMA 實現差異很大。比如 STM32 的 DMA 控制器功能強大,支持多種觸發模式和外設;但有些低端 MCU 可能只支持簡單內存到內存的 DMA,甚至壓根沒有 DMA 功能。所以,配置 DMA 前,得老老實實啃芯片手冊,搞清楚具體實現細節。

還有一點,DMA 雖然能解放處理器,但它佔用系統總線資源。如果 DMA 和處理器同時訪問內存,可能會引發總線競爭,影響性能。所以,實際開發中得合理規劃 DMA 優先級和總線使用,避免搬了芝麻丟了西瓜。

實際案例:SPI+DMA 的典型應用

我們以一個常見的場景爲例:用 SPI 從 ADC 採集數據,存到內存。假設用的是 STM32F4 系列,開發環境是 Keil+HAL 庫。

正常流程是這樣的:ADC 採樣完觸發中斷,處理器從 SPI 數據寄存器讀數據,存到內存,再通過 SPI 發給 SD 卡。整個過程,處理器得全程盯着,稍有延遲就可能丟數據。

用 DMA 優化後,流程變成這樣:

  1. 初始化 DMA,配置通道爲 SPI_RX,源地址是 SPI 數據寄存器,目標地址是內存緩衝區,設置傳輸長度和觸發條件(比如 SPI 接收完成)。

  2. 配置好後,啓動 DMA,ADC 採樣數據通過 SPI 自動搬到內存。

  3. 傳輸完後,DMA 觸發中斷,通知處理器數據就位。

  4. 處理器收到中斷,去處理這批數據,比如存到 SD 卡或發給上位機。

僞代碼大概長這樣:

void DMA_SPI_ADC_Init(void) {
    // 配置DMA通道
    DMA_InitStructure.Channel = DMA_CHANNEL_X;
    DMA_InitStructure.PeripheralBaseAddr = (uint32_t)&SPIx->DR; // SPI數據寄存器
    DMA_InitStructure.MemoryBaseAddr = (uint32_t)adc_buffer;   // 內存緩衝區
    DMA_InitStructure.Direction = DMA_PERIPH_TO_MEMORY;        // 外設到內存
    DMA_InitStructure.NbData = ADC_BUFFER_SIZE;                // 傳輸長度
    DMA_InitStructure.Priority = DMA_PRIORITY_HIGH;            // 高優先級
    DMA_Init(DMAx, &DMA_InitStructure);
    
    // 使能DMA中斷
    DMA_ITConfig(DMAx, DMA_IT_TC, ENABLE);
    
    // 啓動DMA
    DMA_Cmd(DMAx, ENABLE);
}

// DMA傳輸完成中斷
void DMAx_IRQHandler(void) {
    if (DMA_GetITStatus(DMAx, DMA_IT_TC)) {
        // 數據搬運完成,處理adc_buffer中的數據
        ProcessADCData(adc_buffer);
        // 重新啓動DMA,準備下一次傳輸
        DMA_Cmd(DMAx, ENABLE);
        DMA_ClearITPendingBit(DMAx, DMA_IT_TC);
    }
}

這樣,CPU 只在數據搬運完成時介入,其他時間可以幹別的,比如跑個 PID 控制算法,或者處理用戶通過 UART 發來的命令。效率提升不是一星半點。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BPP6K51Er3wqEy2zOYkuCg