はじめに: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を設定します。
設定手順
- 「Pinout & Configuration」→「USART2」を開き、「Mode」を Asynchronous に設定済みであることを確認
- 「DMA Settings」タブで「Add」をクリック
- 追加された行で「Select」プルダウンから USART2_RX を選択
- 「Mode」を Circular(連続受信)または Normal(1回で停止)に設定
- 「Data Width」を「Byte」に(UARTは1バイト単位)
- 「NVIC Settings」タブで「USART2 global interrupt」と「DMA1 Stream/Channel x global interrupt」を Enable
- 「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;
}
}
従来は「タイムアウト付き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 モード + 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 を使った高速サンプリング装置・通信ゲートウェイの試作・量産設計、既存基板の高速化や移行も 組込み制御・マイコン開発 として対応しています。
STM32 DMA・高速サンプリング装置の受託開発
ADC + DMA連続サンプリング、UART/SPI/I2Cの高速ノンブロッキング転送、産業用通信ゲートウェイなど、DMA活用が必須となる設計をテクノスフィアが請け負います。試作のPoCから量産・現場導入まで一気通貫。