メモリ・ストレージ

DMA

CPUを介さずメモリ転送を行う仕組み。負荷を下げ高速化。

概要

DMA(Direct Memory Access: ダイレクトメモリアクセス)は、CPUを介さずにメモリ間、またはメモリとペリフェラル(UART・SPI・ADC等)間でデータを転送する仕組みです。DMAコントローラ(DMAC)というハードウェアが転送を担当し、CPUはその間に別の処理を継続できます。

CPUによるデータ転送は「ロード(メモリ読み出し)→レジスタに格納→ストア(書き込み)」を繰り返すため、大量データ転送時にCPUリソースを大量消費します。DMAを使うと転送をDMACにオフロードでき、CPUの実効処理能力が大幅に向上します。大容量の音声データ転送、ADCの連続サンプリング、高速シリアル通信(SPI/UART)のバッファリングなど、高スループットが必要な処理で特に効果的です。

ただし、DMAとキャッシュの組み合わせでは、キャッシュコヒーレンシ(データの一貫性)の管理が必要です。ARM Cortex-M7やCortex-AのようにキャッシュとDMAが共存するシステムでは、DMA転送前後のキャッシュ操作(フラッシュ・無効化)が重要な実装ポイントです。

歴史・背景

DMAの概念は1960年代のメインフレーム時代に登場しました。初期のI/Oはプログラム制御I/O(PIO: Programmed I/O)でCPUが全てのデータ転送を行っていましたが、大型コンピュータでのI/O効率化ニーズからDMAが開発されました。

1970〜80年代の8ビット・16ビットマイコン時代(Intel 8080・Z80等)にもDMAコントローラ(Intel 8257・8237等)が外付けで使われました。IBM PCの8237 DMAコントローラはメモリ〜ISAバスデバイス間のDMAを担いました。

ARM Cortex-M系マイコンでは、汎用DMAコントローラ(STM32のDMA1/DMA2等)がメモリ〜メモリ、メモリ〜ペリフェラル間のDMAをサポートし、特定ペリフェラルとDMAチャンネルが紐づく形で設計されています。

近年の高性能SoC(NVIDIA Jetson・Raspberry Pi等)では複数のDMAエンジンが搭載されており、CPU・GPU・NPU・I/Oペリフェラル間の高帯域幅データ転送を効率化しています。また、GPGPU(汎用GPU)計算でのCUDA DMA・ARM AMBA DMAXといった高度なDMAアーキテクチャも普及しています。

技術仕様

DMAチャンネルと転送モード

転送モード説明用途
メモリ→メモリSRAM間・SRAM↔外部SDRAM間高速バッファコピー
ペリフェラル→メモリADC・UART受信→RAMADCサンプリング・シリアル受信
メモリ→ペリフェラルRAM→UART・SPI送信大量データ送信
周期モード(Circular)バッファを循環して転送連続ADC・オーディオ
通常モード(Normal)指定転送数で停止単発転送

STM32 DMAコントローラ仕様(例: STM32F4)

  • DMA1: 7チャンネル(各チャンネルに複数のリクエストを割り当て可能)
  • DMA2: 8チャンネル
  • データ幅: バイト(8bit)・ハーフワード(16bit)・ワード(32bit)
  • 転送数: 最大65535カウント/転送
  • 優先度: Very High / High / Medium / Low(ソフトウェア設定)
  • バースト転送: シングル / 4ビート / 8ビート / 16ビートバースト
  • FIFOバッファ: 4ワード(16バイト)のFIFOを内蔵
  • 割り込み: 転送完了・ハーフ転送・転送エラー・FIFOエラー

転送速度の目安

転送タイプ条件転送速度
SRAM→SRAM(DMA)32bit幅, 168MHz〜672MB/s
UART→SRAM(DMA)115200bps, 8bit〜11.5KB/s
SPI→SRAM(DMA)42MHz, 8bit〜42MB/s
ADC→SRAM(DMA)12bit, 1MHz〜2MB/s

動作原理

DMA転送の基本フロー

1. 設定: CPUがDMACに転送元・転送先・転送数・データ幅を設定
2. 有効化: DMAチャンネルを有効化
3. トリガ: ペリフェラルのDMAリクエスト(DRQ)またはソフトウェアトリガ
4. 転送: DMACがバスを使ってデータをコピー(CPUはバスを一時的に譲る)
5. 完了: 転送完了でDMA完了割り込み(または完了フラグポーリング)
6. 後処理: CPUが転送データを処理
// STM32 HAL DMA設定例(UART受信 → SRAM)
// DMA設定
hdma_usart1_rx.Instance                 = DMA2_Stream2;
hdma_usart1_rx.Init.Channel             = DMA_CHANNEL_4;
hdma_usart1_rx.Init.Direction           = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc           = DMA_PINC_DISABLE;   // ペリフェラルアドレス固定
hdma_usart1_rx.Init.MemInc              = DMA_MINC_ENABLE;    // メモリアドレスインクリメント
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment    = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode                = DMA_CIRCULAR;       // 循環モード
hdma_usart1_rx.Init.Priority            = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);

// DMA受信開始
uint8_t rx_buf[256];
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
// これ以降CPUは他の処理を継続可能

サイクルスチーリング(Cycle Stealing)

DMAがバスを使うとき、CPUは一時的にバスアクセスを中断します(バスサイクルを「盗まれる」)。ただし、通常は数サイクルずつ交互にバスを使うため、CPUはほぼ中断なく動作できます。

バスの使用タイムライン:
CPU:  実行 | 待機 | 実行 | 待機 | 実行 | 待機 |
DMA:  ---  | 転送 | ---  | 転送 | ---  | 転送 |
         ↑バスアービトレーションで交互使用

DMAダブルバッファ(ピンポンバッファ)

連続的なデータストリームを処理する際、バッファ0とバッファ1を交互に使います(ダブルバッファリング)。DMAがバッファ0を埋めている間にCPUがバッファ1のデータを処理し、次にDMAがバッファ1を、CPUがバッファ0を処理…と繰り返します。

// DMAダブルバッファ(STM32 HAL)
uint8_t buf0[ADC_BUF_SIZE];
uint8_t buf1[ADC_BUF_SIZE];

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buf0, ADC_BUF_SIZE);

// DMA Half Transfer Callback (buf0の前半が埋まった)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
    process_data(buf0, ADC_BUF_SIZE / 2);  // 前半を処理
}

// DMA Full Transfer Callback (buf0の後半が埋まった)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    process_data(buf0 + ADC_BUF_SIZE/2, ADC_BUF_SIZE / 2);  // 後半を処理
}

MDMA / BDMA(高機能DMA)

STM32H7等の高性能マイコンには複数のDMAエンジンがあります:

  • DMA1/DMA2: 汎用DMA(APBペリフェラルとSRAM間)
  • MDMA(Master DMA): CPUとは独立してAHB/AXIバスを使う高速DMA
  • BDMA(Basic DMA): 低消費電力ドメインのバックアップSRAMやペリフェラル向け

用途・ユースケース

ADC連続サンプリング

ADCのDMAモードを使うと、CPUへの割り込みなしにADCの変換結果を連続してRAMに蓄積できます。音声録音・振動センサー(FFT解析)・電流モニタリングなどで重要です。

// ADC DMAサンプリング(12bit, 10チャンネル, 循環)
uint16_t adc_buf[10];  // 10チャンネル分のバッファ
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 10);
// 変換完了後 adc_buf[0]〜[9] に各チャンネルの値が自動更新される

SPIディスプレイへの高速転送

LCDディスプレイ(ILI9341・ST7789等)への画像転送では、DMAを使って送信中にCPUが次フレームの描画を並行処理できます。

// SPI DMA送信(フレームバッファ→LCD)
void lcd_draw_frame(const uint8_t *framebuf, size_t len) {
    gpio_write(LCD_CS, 0);   // CS Low
    gpio_write(LCD_DC, 1);   // Data mode
    HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)framebuf, len);
    // DMA転送中にCPUは次フレームを描画可能
}

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    gpio_write(LCD_CS, 1);   // CS High(転送完了)
}

UART/USART高速受信

UARTのDMA受信は、バイトごとの割り込みが不要で、バッファ満杯または半分になった時点で割り込みが発生します。高ボーレート(921600bps以上)でのロバストな受信に必須です。

メモリコピーの高速化

SRAM間の大量データコピー(memcpy相当)をDMAで実施することで、CPUサイクルを節約できます。ただし小さなデータ(数百バイト以下)ではDMA設定オーバーヘッドのほうが大きいため、CPUコピーのほうが速いこともあります。

実装・開発のポイント

キャッシュコヒーレンシ管理

ARM Cortex-M7やCortex-A(キャッシュあり)でDMAを使う場合、キャッシュの整合性管理が最重要です:

// DMAバッファはキャッシュラインサイズ(32B or 64B)でアライメント
#define CACHE_LINE_SIZE 32
uint8_t rx_buf[1024] __attribute__((aligned(CACHE_LINE_SIZE)));

// DMA受信前: DMAが書き込む領域をInvalidate(古いキャッシュを無効化)
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));

// DMA完了後: データを読む前にもInvalidate(念のため)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
    use_received_data(rx_buf, sizeof(rx_buf));
}

// DMA送信前: CPUが書いたデータをDRAMにClean
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));

DMAエラー処理

DMA転送エラー(バスエラー・FIFOエラー等)のコールバックを必ず実装します:

void HAL_DMA_ErrorCallback(DMA_HandleTypeDef *hdma) {
    // エラー発生時: ログ記録・DMAリセット・[ウォッチドッグ](/embedded/glossary/watchdog/)リセット
    error_handler(DMA_ERROR, HAL_DMA_GetError(hdma));
}

DMAチャンネルの競合

複数のペリフェラルが同じDMAストリームを要求している場合(STM32ではストリームとチャンネルの組み合わせが固定)、競合を避けてDMAチャンネルを選択する必要があります。データシートのDMAチャンネル割り当て表を必ず参照します。

ノンキャッシャブルメモリへのDMAバッファ配置

キャッシュ管理を簡素化するため、DMAバッファをノンキャッシャブル領域(MPUでキャッシュ無効化した特定SRAM領域)に配置する方法も有効です:

// GCCでノンキャッシャブルセクションに配置
__attribute__((section(".noncacheable")))
uint8_t dma_rx_buf[4096];
// リンカスクリプトで .noncacheable セクションをノンキャッシャブルSRAMに配置
// MPUでその領域のキャッシュをDisableに設定

他技術との比較

DMA vs CPU転送(PIO: Programmed I/O)

項目DMACPU転送(PIO)
CPU負荷低い(設定のみ)高い(全サイクルを消費)
転送速度高速(バス帯域幅依存)CPU速度依存(命令オーバーヘッドあり)
レイテンシ設定オーバーヘッドあり即座に開始
実装の複雑さ高い(キャッシュ管理等)低い
小データ非効率(オーバーヘッド大)効率的
大データ効率的非効率(CPU独占)

DMA vs 割り込み駆動I/O

割り込み駆動I/O(UART受信割り込みでバイトごとに処理)はDMAより実装が簡単ですが、高速通信では割り込みレートが高くなりCPU負荷が増大します(115200bpsで〜11500回/秒の割り込み)。高ボーレート・大容量転送にはDMAが適しています。

DMA vs FIFO(ハードウェアFIFO)

ペリフェラルが内蔵するFIFO(I2SのTX/RX FIFO等)は小規模なバッファリングを行いますが、容量が小さく(4〜32ワード程度)、大量データにはDMAと組み合わせて使います。

関連用語

参考リンク