# STM32 DMA 完全ガイド｜UART・SPI・ADC との連携でCPU負荷を最小化｜株式会社テクノスフィア

> STM32 の DMA を UART/SPI/ADC と組み合わせ CPU 負荷を最小化する実装ガイド。CubeMX 設定・Normal/Circular・ダブルバッファ・IDLE 割り込みでの可変長受信まで網羅。

URL: https://technosphere.co.jp/blog/stm32-dma-complete-guide

** 目次
1. はじめに：DMAを使うべき場面
2. DMAコントローラの基礎
3. STM32CubeMXでのDMA設定
4. UART + DMA（送信・連続受信）
5. SPI + DMA（高速転送）
6. ADC + DMA（連続サンプリング）
7. Circular モードとダブルバッファ
8. よくあるトラブルと対策
9. まとめ

## はじめに：DMAを使うべき場面

 STM32の**DMA（Direct Memory Access）**は、CPUを介さずにペリフェラル⇔メモリ間でデータ転送を行うハードウェアです。CPUは転送中に別の処理を進められるため、システム全体のスループットが大幅に向上します。

具体的には次のような場面でDMAは必須級です。
- **高速UART受信：**460800bpsで連続受信しながらCPUは制御処理を続けたい
- **ADCサンプリング：**1MSpsで連続A/D変換結果を取得しFFT解析へ流したい
- **SPI大容量転送：**SDカード・SPIフラッシュ・LCDフレームバッファ更新
- **メモリ to メモリコピー：**大きな構造体・画像バッファのコピー

本記事の前提として、UART/I2C/SPIの基本実装は [STM32マイコンのUART・I2C・SPI・CAN通信 実装ガイド](https://technosphere.co.jp/blog/stm32-uart-i2c-can) をご覧ください。FreeRTOSと組み合わせる場合は [STM32 + FreeRTOS 入門](https://technosphere.co.jp/blog/stm32-freertos-intro) も参考になります。  ** 弊社での活用実績

弊社では振動データロガー（ADC + DMA で 100kSps 連続取得）、産業用通信ゲートウェイ（UART + DMA + IDLE割込みで可変長フレーム受信）、カメラミドルウェア（SPI/DCMI + DMA でフレーム転送）など、DMAをフル活用した [組込み開発実績](https://technosphere.co.jp/embedded-achievements) があります。

## DMAコントローラの基礎

STM32のDMAコントローラは、シリーズごとに「チャネル方式」と「ストリーム方式」の2系統があります。設定の考え方は共通です。

### チャネル方式 / ストリーム方式
- **STM32F1/F3/G0/G4/L0/L4/L5/U5：**DMA1/DMA2にそれぞれ複数の「チャネル」。**DMAMUX**でペリフェラル↔チャネルを自由に対応付け可能（新しい系列）
- **STM32F2/F4/F7/H7：**DMA1/DMA2にそれぞれ8本の「ストリーム」、各ストリームに8チャネル。ペリフェラル⇔ストリーム⇔チャネルの組み合わせがリファレンスマニュアルで固定

### DMA設定の主な要素
| 項目 | 意味 | 典型的な選択 |
| **Direction** | 転送方向 | Periph to Mem / Mem to Periph / Mem to Mem |
| **Mode** | 転送モード | Normal（1回で停止）/ Circular（リング状に永続） |
| **Data Width** | 転送単位 | Byte / Half-Word / Word（ペリフェラル仕様に合わせる） |
| **Increment Address** | アドレス自動加算 | メモリ側は Enable、ペリフェラル側は通常 Disable |
| **Priority** | 転送優先度 | Low / Medium / High / Very High（複数DMAが競合した時の調停） |
| **FIFO**（F4/F7/H7） | 内蔵FIFO使用 | 大量転送やバースト時に Enable で効率化 |

## STM32CubeMXでのDMA設定

CubeMXでDMAを設定する手順は、ペリフェラルの「DMA Settings」タブで追加するだけです。例としてUSART2の受信DMAを設定します。

### 設定手順
1. 「Pinout & Configuration」→「USART2」を開き、「Mode」を Asynchronous に設定済みであることを確認
2. 「DMA Settings」タブで「Add」をクリック
3. 追加された行で「Select」プルダウンから **USART2_RX** を選択
4. 「Mode」を **Circular**（連続受信）または **Normal**（1回で停止）に設定
5. 「Data Width」を「Byte」に（UARTは1バイト単位）
6. 「NVIC Settings」タブで「USART2 global interrupt」と「DMA1 Stream/Channel x global interrupt」を Enable
7. 「Generate Code」で初期化コードを自動生成
  ** よくあるミス

DMA設定だけしてNVIC（割り込み）の有効化を忘れると、転送完了コールバックが呼ばれません。必ずペリフェラル側とDMA側**両方の割り込み**を有効にしてください。

### 生成される初期化コード（抜粋）

```
/* main.c の MX_DMA_Init() */
static void MX_DMA_Init(void)
{
    __HAL_RCC_DMA1_CLK_ENABLE();

    HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 5, 0);  // 優先度は5以上推奨（FreeRTOS併用時）
    HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
}

/* USART2_Init() 内に追加される DMA 連結 */
hdma_usart2_rx.Instance = DMA1_Stream5;
hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
HAL_DMA_Init(&hdma_usart2_rx);

__HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx);
```

## UART + DMA（送信・連続受信）

### 送信 DMA（バーストデータの非同期送信）

大量データを送信する場合、ポーリング送信ではCPUがブロックされます。DMA送信に切り替えるとCPUは他の処理に集中できます。

```
/* 送信DMA：最大512バイトのバッファにログを作り、実長(strlen)を非同期送信 */
uint8_t log_buf[512];
sprintf((char*)log_buf, "tick=%lu, temp=%.2f, ...\r\n", HAL_GetTick(), temp);

HAL_UART_Transmit_DMA(&huart2, log_buf, strlen((char*)log_buf));
// この時点でCPUは別処理を実行可能

/* 送信完了コールバック */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART2) {
        // 送信完了処理（次のバッファを送信、フラグを立てるなど）
    }
}
```

### 受信 DMA + IDLE 割込み（可変長フレーム受信の決定版）

産業機器との通信では「受信フレーム長が不定」というケースが頻出します。DMA Circular + IDLE 割り込みを組み合わせると、効率的に可変長フレームを取得できます。

```
#define RX_BUF_SIZE 256
uint8_t rx_buf[RX_BUF_SIZE];
volatile uint16_t rx_old_pos = 0;

void StartReceiveDmaIdle(void)
{
    /* IDLE割り込みを有効化 */
    __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
    /* DMA Circular モードで連続受信開始 */
    HAL_UART_Receive_DMA(&huart2, rx_buf, RX_BUF_SIZE);
}

/* USART2 割込みハンドラ（stm32xxxx_it.c） */
void USART2_IRQHandler(void)
{
    /* IDLE フラグが立っていれば受信終了とみなす */
    if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart2);
        ProcessReceivedData();
    }
    HAL_UART_IRQHandler(&huart2);
}

void ProcessReceivedData(void)
{
    /* DMA残カウンタから現在の書き込み位置を計算 */
    uint16_t pos = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart2.hdmarx);

    if (pos != rx_old_pos) {
        if (pos > rx_old_pos) {
            /* 通常領域 */
            HandleFrame(&rx_buf[rx_old_pos], pos - rx_old_pos);
        } else {
            /* リング末尾 → 先頭にラップしたケース */
            HandleFrame(&rx_buf[rx_old_pos], RX_BUF_SIZE - rx_old_pos);
            HandleFrame(&rx_buf[0], pos);
        }
        rx_old_pos = pos;
    }
}
```

   ** IDLE割込みのメリット

従来は「タイムアウト付きreceive」「1バイトずつ受信割り込み」のどちらかで対応していましたが、前者はCPUを浪費し、後者は高速通信で取りこぼします。**DMA Circular + IDLE** は両者の欠点を解消し、115200bps以上の高速通信でも確実に取りこぼし無くフレームを切り出せます。

## SPI + DMA（高速転送）

SPIは数十Mbpsの高速通信が可能ですが、CPUのSPI送受信ループでは速度が頭打ちになります。DMAを使うことで本来のバススピードを引き出せます。SPIフラッシュ、LCD、SD カード、ADCチップなどで特に効果的です。

### SPI送受信 DMA

```
/* SPI 全二重 DMA 転送 */
uint8_t spi_tx_buf[1024];
uint8_t spi_rx_buf[1024];

HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);   // CS Low
HAL_SPI_TransmitReceive_DMA(&hspi1, spi_tx_buf, spi_rx_buf, 1024);
// CPUは別処理を進められる

/* 転送完了コールバック */
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi->Instance == SPI1) {
        HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);  // CS High
        // 受信データ処理
    }
}
```

### LCDフレームバッファ更新の例

320×240×16bit のLCDフレーム全更新（150KB）をCPUポーリングで書き込むと数十ms以上かかります。SPI + DMAなら数msに短縮可能です。

```
#define LCD_W 320
#define LCD_H 240
uint16_t lcd_framebuffer[LCD_W * LCD_H];  // 150KB

void Lcd_UpdateFrame(void)
{
    Lcd_SetWindow(0, 0, LCD_W - 1, LCD_H - 1);
    Lcd_DcPin(GPIO_PIN_SET);   // Data モード
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);

    /* 16bit幅、150KB相当を一気にDMA送信 */
    HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)lcd_framebuffer, LCD_W * LCD_H * 2);
}
```

## ADC + DMA（連続サンプリング）

ADCとDMAの組み合わせは、振動計測・音声入力・電流計測などサンプリングが必要な全てのアプリで定番です。CubeMXで ADC を Continuous Conversion + DMA + Circular に設定するのが基本です。

### 単一チャネル連続変換

```
#define ADC_BUF_SIZE 1024
uint16_t adc_buf[ADC_BUF_SIZE];

/* 起動時 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);

/* バッファ半分埋まった時 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        // 前半 512 サンプルを処理（後半はDMAが書き込み中なので安全）
        ProcessSamples(&adc_buf[0], ADC_BUF_SIZE / 2);
    }
}

/* バッファ全部埋まった時 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        // 後半 512 サンプルを処理
        ProcessSamples(&adc_buf[ADC_BUF_SIZE / 2], ADC_BUF_SIZE / 2);
    }
}
```

### 複数チャネル（Scan モード）の取得

ADC1の chs.0,1,4,8 を順次サンプリングしてDMAに格納するパターンです。CubeMXで Rank を設定し、Scan Conversion Mode を Enable にします。

```
#define N_CHANNELS 4
#define N_SAMPLES  100

uint16_t adc_buf[N_SAMPLES * N_CHANNELS];  // ch0, ch1, ch4, ch8 の順に格納

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, N_SAMPLES * N_CHANNELS);

/* 100サンプル × 4ch 取得完了 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /* 各チャネル平均値を計算 */
    uint32_t sum[N_CHANNELS] = {0};
    for (int i = 0; i < N_SAMPLES; i++) {
        for (int ch = 0; ch < N_CHANNELS; ch++) {
            sum[ch] += adc_buf[i * N_CHANNELS + ch];
        }
    }
    for (int ch = 0; ch < N_CHANNELS; ch++) {
        avg[ch] = sum[ch] / N_SAMPLES;
    }
}
```

   ** 産業現場での活用例

弊社では振動データロガー案件で、ADC1+ADC2をデュアルモードで動作させ、合計 250kSps の連続取得をDMA Circular + ダブルバッファで実現しました。CPUは取得と並行してFFT処理を進めるため、リアルタイムで周波数スペクトルを更新できます。[類似の組込み開発実績](https://technosphere.co.jp/embedded-achievements) を多数ご紹介しています。

## Circular モードとダブルバッファ

### Normal モード vs Circular モード
- **Normal モード：**指定サイズ分の転送が終わるとDMA停止。次回も使うには再度 HAL_XXX_Start を呼ぶ必要がある
- **Circular モード：**転送終了時に自動的にバッファ先頭へラップして継続。連続サンプリング・連続受信に最適

### ダブルバッファ方式（DBM）

STM32F4/F7/H7のDMAは「Double Buffer Mode」をサポートし、2つのバッファを自動切り替えできます。一方をDMAが埋めている間、CPUはもう一方を安全に処理できます。Circular モードよりも明確な切り替え境界が得られます。

```
uint16_t buf_a[1024];
uint16_t buf_b[1024];

/* HALではダブルバッファ用関数を使用 */
HAL_DMAEx_MultiBufferStart_IT(
    &hdma_adc1,
    (uint32_t)&hadc1.Instance->DR,
    (uint32_t)buf_a,
    (uint32_t)buf_b,
    1024
);

/* M0完了：buf_a が満杯になった */
void HAL_DMAEx_M0CpltCallback(DMA_HandleTypeDef *hdma) {
    Process(buf_a, 1024);  // この間DMAはbuf_bを埋めている
}

/* M1完了：buf_b が満杯になった */
void HAL_DMAEx_M1CpltCallback(DMA_HandleTypeDef *hdma) {
    Process(buf_b, 1024);
}
```

   ** Circular の Half/Full コールバックでも代替可能

シンプルにはCircular モード + HalfCpltCallback + CpltCallback の組み合わせで「擬似ダブルバッファ」が実現できます（前半を処理中、後半にDMA書き込み）。ダブルバッファモードはより明確な境界・連続性の保証が必要な高速転送向けです。

## よくあるトラブルと対策
| 症状 | 原因 | 対策 |
|  DMA転送が一度きりで止まる |  Normal モードのまま使用 |  連続転送が必要なら Mode を Circular に変更。または完了コールバックで再起動 |
|  受信データが化ける / 一部欠落 |  Data Width 不一致 / バッファ境界 |  ペリフェラル側とメモリ側のData Widthを揃える。`__DSB();` でメモリバリアを入れる |
|  DMA完了コールバックが呼ばれない |  NVIC割り込みが無効 / 優先度設定ミス |  CubeMXのNVIC タブで該当DMAストリーム/チャネル割り込みを Enable。優先度は FreeRTOS併用時5以上 |
|  STM32H7で受信データが意図通り読めない |  D-Cache とDMAバッファの不整合 |  DMAバッファ領域を non-cacheable に配置するか、`SCB_InvalidateDCache_by_Addr()` でキャッシュ無効化 |
|  ADC値が常に0 / 同じ値ばかり |  ADC + DMA で uint32_t* キャスト忘れ |  `HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, size)` のキャストを忘れない |
|  DMAストリーム競合エラー |  F4/F7系で複数ペリフェラルが同一ストリームを取り合う |  リファレンスマニュアルのDMAストリーム表を確認し、空いている組み合わせを選ぶ |
|  SPI DMA送信で末尾1バイトが化ける |  送信完了コールバックでCSをHighにする前にSPI内部FIFOにデータ残存 |  `__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_BSY)` がClearになるまで待ってからCSをHighに |       ** デバッグの定石
- **DMAレジスタを見る：**STM32CubeIDEのSFRビューで `DMA1_Stream5->NDTR`（残転送数）を確認すれば、転送が進んでいるかが一目でわかる
- **ロジックアナライザでバス信号を確認：**SPI/UARTの実信号がそもそも出ているかが切り分けの第一歩
- **D-Cache 問題（H7）はバッファ配置を疑う：**リンカスクリプトで .dma_buf セクションを non-cacheable な領域に配置するのが本道

## まとめ

###  この記事のまとめ
- DMAはCPUを介さずペリフェラル⇔メモリ転送を行うハードウェア。CPUを別処理に集中させられる
- UART + DMA Circular + IDLE割り込みは可変長フレーム受信の決定版パターン
- ADC + DMA Circular はサンプリングデータ取得の定番。Half/FullコールバックでCPUと並行処理
- SPI + DMA はLCDフレーム更新やSDカード書き込みで効果絶大
- STM32H7ではD-Cacheとの整合に注意。DMAバッファをnon-cacheable領域に配置するのが安全
- NVIC優先度はFreeRTOS併用時5以上、ペリフェラル/DMA両方を有効化することを忘れずに

STM32 DMA を使った高速通信・サンプリングシステムの開発でお困りのことがあれば、弊社にお気軽にご相談ください。設計から実装・量産対応まで、20年超のマイコン開発実績で一貫してサポートします。

STM32 DMA を使った高速サンプリング装置・通信ゲートウェイの試作・量産設計、既存基板の高速化や移行も [組込み制御・マイコン開発](https://technosphere.co.jp/embedded) として対応しています。   ／この内容の受託開発・PoC相談はこちら＼

### STM32 DMA・高速サンプリング装置の受託開発

ADC + DMA連続サンプリング、UART/SPI/I2Cの高速ノンブロッキング転送、産業用通信ゲートウェイなど、DMA活用が必須となる設計をテクノスフィアが請け負います。試作のPoCから量産・現場導入まで一気通貫。  [組込み開発サービスを見る](https://technosphere.co.jp/embedded) [高速通信・サンプリング相談](https://technosphere.co.jp/contact)     # STM32 # DMA # UART # SPI # ADC # Circular # ダブルバッファ # マイコン開発

##   次に読む｜STM32 シリーズ記事
-  [STM32 シリーズ STM32マイコンのUART・I2C・SPI・CAN通信 実装ガイド HALライブラリで通信ペリフェラルを使いこなす定番ガイド](https://technosphere.co.jp/blog/stm32-uart-i2c-can)
-  [STM32 シリーズ STM32 + FreeRTOS 入門 タスク・キュー・セマフォを使ったRTOS実装ガイド](https://technosphere.co.jp/blog/stm32-freertos-intro)
-  [開発実績 組込み制御・マイコン開発 実績紹介 DMA活用案件を含む20年超の事例](https://technosphere.co.jp/embedded-achievements)
-  [STM32 シリーズ STM32 低消費電力モード完全ガイド バッテリー駆動を年単位で実現する設計ノウハウ](https://technosphere.co.jp/blog/stm32-low-power-battery)
    [技術コラム一覧に戻る](https://technosphere.co.jp/blog/)
