目次
  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通信 実装ガイド をご覧ください。FreeRTOSと組み合わせる場合は STM32 + FreeRTOS 入門 も参考になります。

弊社での活用実績

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

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処理を進めるため、リアルタイムで周波数スペクトルを更新できます。類似の組込み開発実績 を多数ご紹介しています。

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 を使った高速サンプリング装置・通信ゲートウェイの試作・量産設計、既存基板の高速化や移行も 組込み制御・マイコン開発 として対応しています。

/この内容の受託開発・PoC相談はこちら\

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

ADC + DMA連続サンプリング、UART/SPI/I2Cの高速ノンブロッキング転送、産業用通信ゲートウェイなど、DMA活用が必須となる設計をテクノスフィアが請け負います。試作のPoCから量産・現場導入まで一気通貫。

次に読む|STM32 シリーズ記事