概要
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受信→RAM | ADCサンプリング・シリアル受信 |
| メモリ→ペリフェラル | 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)
| 項目 | DMA | CPU転送(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と組み合わせて使います。