0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

DMA 傳輸完全解析:從架構到實作,STM32/ESP32 的 Direct Memory Access 教學

2026 年 5 月 23 日 2點熱度 0人點贊 0條評論

DMA 傳輸(Direct Memory Access,直接記憶體存取)是嵌入式系統中不可或缺的資料搬運技術。它允許周邊設備直接與記憶體交換資料,無需 CPU 逐字節干預——這在 ADC 連續取樣、SPI 高速通訊、UART 大量資料收發等高吞吐量場景中至關重要。

本文從 DMA 傳輸的硬體架構出發,深入解析 DMA 控制器的工作原理、傳輸模式、配置流程,並以 STM32 和 ESP32 為平台展示完整的程式實作。

什麼是 DMA?為什麼需要它?

在傳統的程式 I/O 模式中,CPU 必須逐字節/逐字地從周邊讀取資料並寫入記憶體。這意味著:

  • CPU 被「綁架」在資料搬運上,無法執行其他任務
  • 高頻率的資料傳輸(如 ADC 1MSps)會耗盡 CPU 頻寬
  • 即時性任務(控制迴路、通訊協定)可能因此延遲

DMA 控制器相當於一個專門的「資料搬運引擎」,它可以在背景獨立完成記憶體↔周邊的資料傳輸,只在傳輸完成或發生錯誤時通知 CPU。

DMA vs CPU 傳輸比較時序圖
圖 1:CPU vs DMA 傳輸比較 — 無 DMA 時 CPU 被 SPI 佔用;有 DMA 時 CPU 可處理其他任務

DMA 控制器架構

STM32 DMA 系統架構圖
圖 2:STM32 DMA 系統架構 — DMA 控制器透過 AHB 匯流排直接連接周邊與記憶體,CPU 可同時執行其他任務

以 STM32F4 為例,DMA2 有 8 個 Stream(資料流),每個 Stream 有 8 個 Channel(通道):

元件說明
DMA 控制器STM32F4 有 DMA1 和 DMA2,共 16 個 Stream
Stream(資料流)每個 Stream 管理一組傳輸,可配置優先級
Channel(通道)每個 Stream 可選 8 個觸發源之一(如 USART_TX、SPI_RX)
FIFO每個 Stream 有 4 word FIFO,用於資料寬度匹配
傳輸計數器每次傳輸遞減,歸零時觸發完成中斷

DMA 傳輸類型

  • M2M(Memory-to-Memory):記憶體區塊搬運(如 memcpy 的硬體加速)
  • M2P(Memory-to-Peripheral):記憶體 → 周邊(如 SPI 發送、DAC 輸出)
  • P2M(Peripheral-to-Memory):周邊 → 記憶體(如 ADC 取樣、UART 接收)
  • P2P(Peripheral-to-Peripheral):周邊直連(部分系列支援)

DMA 請求/確認握手機制

DMA 請求確認傳輸序列
圖 3:DMA 握手機制 — 周邊發出 Request,DMA 控制器回應 Ack 後開始傳輸

當周邊(如 USART)的 RX 寄存器收到一個字節後:

  1. 周邊拉高 DMA Request 訊號
  2. DMA 控制器仲裁(優先級 + 輪詢機制)
  3. DMA 回傳 Ack,取得匯流排控制權
  4. DMA 從周邊資料寄存器讀取資料,寫入記憶體位址
  5. 位址遞增、計數器遞減
  6. 釋放匯流排,等待下一次 Request

Ping-Pong Buffer(雙緩衝)

Ping-Pong 雙緩衝時序圖
圖 4:Ping-Pong 雙緩衝 — DMA 寫入 Buffer A 時 CPU 處理 Buffer B,互不干擾

Ping-Pong Buffer 是最經典的 DMA 優化技巧:使用兩個 Buffer 輪流由 DMA 寫入和 CPU 處理。DMA 正在寫入的 Buffer 不會被 CPU 讀取,CPU 正在處理的 Buffer 不會被 DMA 覆寫。

實作重點:

  • DMA 完成中斷中切換 Buffer 指標
  • 兩個 Buffer 大小必須相同
  • 使用 Circular Mode 或雙 Buffer Mode(STM32 部分系列支援硬體自動切換)

STM32 HAL 實作:DMA + ADC 連續取樣

#include "stm32f4xx_hal.h"

#define ADC_BUF_LEN  1024
uint16_t adc_buffer[ADC_BUF_LEN];

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc;

void MX_DMA_Init(void) {
    __HAL_RCC_DMA2_CLK_ENABLE();
    hdma_adc.Instance = DMA2_Stream0;
    hdma_adc.Init.Channel  = DMA_CHANNEL_0;
    hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;    // P2M
    hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;         // 周邊位址不遞增
    hdma_adc.Init.MemInc    = DMA_MINC_ENABLE;          // 記憶體位址遞增
    hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  // 16 bit
    hdma_adc.Init.MemDataAlignment    = DMA_MDATAALIGN_HALFWORD;
    hdma_adc.Init.Mode      = DMA_CIRCULAR;              // 循環模式
    hdma_adc.Init.Priority  = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_adc);

    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc);
}

void MX_ADC1_Init(void) {
    hadc1.Instance = ADC1;
    hadc1.Init.ScanConvMode  = DISABLE;
    hadc1.Init.ContinuousConvMode = ENABLE;   // 連續轉換
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.NbrOfConversion = 1;
    HAL_ADC_Init(&hadc1);
}

int main(void) {
    HAL_Init();
    MX_DMA_Init();
    MX_ADC1_Init();

    // 啟動 DMA + ADC(硬體自動搬運)
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUF_LEN);

    while (1) {
        // CPU 可以在這裡做其他事!
        // 最新的 ADC 資料一直在 adc_buffer[] 中更新
        uint16_t latest = adc_buffer[ADC_BUF_LEN - 1];
        HAL_Delay(10);
    }
}

// DMA 完成回呼(每填滿一次觸發)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    // 可在此切換 Ping-Pong Buffer
    // HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ping_buffer, ADC_BUF_LEN);
}

ESP32 實作:DMA + SPI 傳輸

ESP32 的 SPI 控制器內建硬體 DMA(GDMA),支援自動發送/接收大量資料:

#include "driver/spi_master.h"

#define DMA_CHUNK_SIZE  4096
uint8_t tx_buffer[DMA_CHUNK_SIZE];
uint8_t rx_buffer[DMA_CHUNK_SIZE];

void app_main(void) {
    spi_bus_config_t bus_cfg = {
        .miso_io_num = 12,
        .mosi_io_num = 13,
        .sclk_io_num = 14,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = DMA_CHUNK_SIZE * 2,
    };
    spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO);  // 啟用 DMA

    spi_device_handle_t spi;
    spi_device_interface_config_t dev_cfg = {
        .clock_speed_hz = 10 * 1000 * 1000,  // 10 MHz
        .mode = 0,
        .spics_io_num = 15,
        .queue_size = 1,
    };
    spi_bus_add_device(SPI2_HOST, &dev_cfg, &spi);

    // DMA 傳輸(資料自動透過 GDMA 搬運,不佔用 CPU)
    spi_transaction_t t = {
        .length = DMA_CHUNK_SIZE * 8,     // 位元數
        .tx_buffer = tx_buffer,
        .rx_buffer = rx_buffer,
    };
    spi_device_transmit(spi, &t);  // 底層使用 DMA 傳輸

    // 同時,CPU 可以處理其他任務
    printf("SPI DMA 傳輸完成!\n");
}

DMA + FIFO 的搭配

DMA 和 FIFO 是天生一對。DMA 負責資料搬運,FIFO 負責速率匹配和數據緩衝:

  • UART RX:DMA 將 UART FIFO 的資料週期性搬入 Ring Buffer,CPU 從 Ring Buffer 讀取
  • SPI TX:CPU 填充 DMA Buffer,DMA 逐 byte 餵給 SPI FIFO,CPU 完全解放
  • ADC + DMA + 雙 Buffer:DMA 輪流填充兩個 Buffer,CPU 處理已填滿的 Buffer

實務上,DMA 觸發來源通常設定為周邊 FIFO 的「幾乎空」或「幾乎滿」事件,而非每個 byte 都觸發,這樣可以大幅降低 DMA 傳輸次數。

常見陷阱

Cache Coherency(快取一致性)

這是 STM32H7 等高階 MCU 最常見的問題。CPU 的 Cache 中可能有資料的舊副本,而 DMA 直接寫入了 SRAM——CPU 讀到的可能是 Cache 中的舊資料。

解法:使用 DMA 緩衝區所在的記憶體區域配置為「非快取」(Non-Cacheable),或在訪問前執行 Cache 清理/失效操作。

// STM32H7 解法:使用非快取記憶體區段
// 或在 DMA 讀取前失效 Cache
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);
// 在 DMA 寫入前清理 Cache
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);

Buffer Overflow / Underflow

DMA 傳輸速度超過 CPU 處理速度時,Buffer 會被覆寫(Overflow)。反之,CPU 處理太快導致 Buffer 空(Underflow)。

解法:使用 Ping-Pong Buffer + 水位標記(Watermark),或使用 Circular DMA Mode + 讀取/寫入指標追蹤。

DMA 與中斷競爭

DMA 傳輸完成中斷和其他中斷同時發生時,可能導致資料競爭(Race Condition)。

解法:在 DMA 回呼中使用原子操作或關閉中斷來保護共享資料。

總結

DMA 是嵌入式系統中與 Timer、Interrupt 並列的三大核心硬體資源之一。善用 DMA 可以讓你的系統在同樣的 CPU 頻率下處理更多的資料流,這是從「能用」到「高效能」的關鍵技術。

初學者建議從 STM32 HAL 的 ADC + DMA 範例開始,觀察 DMA 如何讓 CPU 從輪詢中解放。進階後再挑戰 Ping-Pong Buffer 和快取一致性問題。

📖 延伸閱讀:FIFO 完全解析 · Modbus 通訊協定 · PID 演算法

標籤: 教學 生產力
最後更新:2026 年 5 月 23 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang