DMA(Direct Memory Access)【STM32のDMA詳細】

DMAは初心者・入門者には必須のスキルではありませんが、DMAを使用するとCPUの負担が減るため、CPU能力を他の処理に回せ、省電力に寄与できますので実務では求められることもあるでしょう。この章ではADコンバータの複数チャネルデータをDMAでメモリに転送する例やUSART送受信にDMAを使用した例で解説しています。

STM32のDMA転送

めかのとろ

DMA(Direct Memory Access)とは直接メモリアクセスと呼ばれるもので通常、データはCPUを介してメモリに転送するのに対して、DMA転送を行うとCPUを介さずにADコンバータ、シリアル通信などペリフェラルのレジスタデータをメモリに転送します。

めかのとろ

CPUの代わりになるものがDMAコントローラと呼ばれるものでSTM32マイコンでは最高で2個内蔵されています。STM32F10xではDMA1とDMA2の2個のコントローラがありAHBに接続されています。
各DMAにはそれぞれ複数のチャネルがありペリフェラルごとにあらかじめ指定されています。

ペリフェラルからDMA1に発せられるDMA要求チャネル
ペリフェラルからDMA2に発せられるDMA要求チャネル

複数チャネルADCのDMAによるデータ転送

めかのとろ

ADC1のCH1とCH2の変換値取得にDMAを使用する例をあげて解説します。

ADCのDMAによるデータ転送

 ① 使用するDMAにクロック供給 

めかのとろ

ADC1はDMA要求がDMA1のCH1に発せられるので使用するDMA1にクロックを供給します。DMAはAHBに接続していますのでAHBに供給します。

設定例: RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA1にクロック供給

めかのとろ

第1引数は下表に示すAHBに接続するペリフェラルを指定します。

AHBに接続するペリフェラル用クロック供給関数へのマクロ
めかのとろ

クロック供給関数RCC_AHBPeriphClockCmdの第2引数がENABLEで供給開始、DISABLEで供給停止となります。

 ② DMA1_CH1初期設定 

めかのとろ

ADC1はDMA1のチャネル1を使用しているのでDMA1_CH1の初期設定を実行します。はじめのDMA_Deinit関数リセット直後の初期状態に戻すもので一応実行しておきます。関数の引数には設定対象のDMAのチャネル(DMAx_Channely)を指定(x:1-2, y:1-7)します。

DMA1の初期化関数設定例: DMA_Init(DMA1_Channel1, &DMA_InitStructure);

めかのとろ

初期化はDMA_Init関数を実行します。関数の第1引数にはDMA_Dinit関数と同じものを指定し、第2引数は構造体メンバになっていて以下に示します。

めかのとろ

変換データを格納するペリフェラル側のデータレジスタ(ADC_DR)のアドレスを指定します。
これはペリフェラルライブラリstm32f10x.h内で定義されているADC1のアドレスのレジスタ名(DR)で指定しておきます。実際のアドレスはレファレンスマニュアルに記載しているもの((uint32_t)0x4001244C)で直接指定してもよいです。

めかのとろ

変換データを格納するメモリ側のアドレスを指定します。この例では配列変数ADCConvertedValue[2]のアドレスとして&ADCConvertedValueと指定しています。

めかのとろ

データの転送方向を指定します。ACコンバータの変換値レジスタからメモリの場合はDMA_DIR_PeripheralSRTを指定します。

めかのとろ

DMAチャネルの優先度はDMA_Priorith_High(高)を指定しておきます。

めかのとろ

転送するデータ数を指定します。2チャネル分であれば2となります。

めかのとろ

DMA_PeripheralIncDMA_MemoryIncはデータ転送時に転送ごとにメモリのアドレスを自動的に増やしてくれます

めかのとろ

DMA_PeripheralDataSizeとDMA_MemoryDataSizeはそれぞれペリフェラルとメモリのデータビット長を指定します。ADCの扱うデータ長は16ビットなのでHalfWordを指定しています。

めかのとろ

サーキュラモードを指定すると指定チャネル数のデータが指定メモリのアドレスへ自動転送を繰り返します。

めかのとろ

ペリフェラルとメモリ間の転送でなく、メモリ上のあるアドレスからメモリ上の別アドレスへ転送する場合に有効にします。今回は使用しません。

めかのとろ

各メンバの指定が終了しましたので、初期化関数DMA_Init(DMA1_Channel1, &DMA_InitStructure)を実行します。

 ③ DMA1の有効化 

めかのとろ

これまででDMAの初期化ができましたのでここでDMA_Cmd関数を実行してDMAを有効化します。

DMA有効化関数実行例: DMA_Cmd(DMA1_Channel1, ENABLE);

めかのとろ

関数の第1引数には設定対象のDMAのチャネル(DMAx_Channely))を指定(x:1-2, y:1-7)し、第2引数はENABLEで有効、DISABLEで無効となります。

 ④ DMA1要求の有効化 

めかのとろ

ここまでで、DMAの初期設定は完了し、有効化したのでスタンバイ状態です。実際にデータの転送を始めるには各ペリフェラルからDMA転送開始させるためのDMA要求の有効化を行う必要があります。サンプル例ではADコンバータのADC1のチャネル1とチャネル2の変換と同時に転送を開始続けるためにADC_DMACmd関数を実行します。

DMA要求の有効化関数の実行例:ADC_DMACmd(ADC1, ENABLE);

めかのとろ

関数の第1引数には設定対象のADコンバータ(ADC1-3:マイコンによります)を指定し、第2引数はENABLEで有効、DISABLEで無効となります。

めかのとろ

これでADコンバータでの2チャンネル分の変換と同時に指定したメモリへ変換値が自動的に繰り返し転送されます。

シリアル通信のDMA転送

文字列受信DMA転送

めかのとろ

DMAをつかったUSART通信の受信プログラムは比較的に単純です。
ここでは受信データ6文字分をDMAによりシリアル通信受信レジスタからメモリに自動転送する例を解説します

シリアル通信DMA受信

 ① データ格納変数など準備 

めかのとろ

シリアルUSART通信の受信においてここではDMAで6文字分転送する設定(6文字の文字列)にするため、変数など準備します。ここでは配列RxData[]に受信文字を格納します。

 ② DMA1(USART3_RX)にクロック供給 

めかのとろ

USART3受信はDMA要求がDMA1のCH3に発せられるので使用するDMA1にクロックを供給します。DMAはAHBに接続していますのでAHBに供給します。

 ③ DMA1_CH3(USART3_RX)のIRQ割り込み設定

めかのとろ

今回はDMAで受信データがメモリに転送完了したときに割り込みを使用するため優先度を設定します(優先度0)

 ④ DMA1_CH3初期設定 

めかのとろ

DMAの初期設定をします。転送先のメモリRxData[]のアドレス転送数(RX_BUFFER)転送データは1文字8bitなのでDataSizeはByteとしています。受信はサーキュラモードとしています。こうするとデータを受信してバッファが一杯になったところで最初にもどるリングバッファとなります。
パラメータを構造体メンバに設定してからADCの時と同様にDMA_Init関数を実行します。

めかのとろ

DMAの転送が完了した時点で割り込み処理を行いたいために転送完了割り込みをDMA_ITConfig関数で有効化します。

DMA転送完了割り込み有効化実行例:DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE);

めかのとろ

第1引数には設定対象のDMAのチャネル(DMAx_Channely))を指定(x:1-2, y:1-7)し、第2引数は割り込み発生要因、第3引数はENABLEで割り込み有効、DISABLEで割り込み無効となります。

 ⑤ DMA1の有効化

めかのとろ

設定終了後、DMA_Cmd関数を実行してDMAを有効化します。

 ⑥ USART3の設定およびDMA1(USART3_RX)要求の有効化

めかのとろ

USART3の初期設定をしてから、DMA転送開始させるためのDMA要求の有効化します。USARTのDMA要求はUSART_DMACmd関数で実行します。関数の第1引数には設定対象のUSARTを指定し、第2引数には設定対象のDMA要求を、第3引数はENABLEで有効、DISABLEで無効となります。

めかのとろ

これより、シリアルUSART3で受信が発生するたびにデータが自動でメモリRxData[]に転送されます。

 ⑦ 通常アプリ内の処理 

めかのとろ

受信割り込みが発生してから所望の文字列を取得したりする処理はアプリケーション内に作成します。
この例では割り込みが発生したときだけ判別フラグrx_flagが1になるので、判別に応じて処理を行います。

 ⑧ DMA割り込みハンドラ 

めかのとろ

DMAで転送が完了すると割り込みが発生し、割り込みハンドラDMA1 Channel3 USART3_RXが起動します。この割り込みハンドラが起動したときに、割り込み要因が転送完了であるかどうかを判別するために、DMA_GetITStatus関数を実行して確認します。

DMA割り込み有効化関数実行例: DMA_GetITStatus(DMA1_IT_TC3);

めかのとろ

ここでも割り込みフラグはDMA_ClearITPengingBit関数で割り込みビットフラグをクリア(0にする)しておきます。割り込み処理としては、割り込み判別フラグrx_flagを1に設定すると、アプリケーション内で判別フラグが1のときだけ受信処理を実行できます。

DMA割り込みフラグ(ペンディングビット)クリア関数実行例: DMA_ClearITPendingBit(DMA1_IT_TC3);

めかのとろ

もちろん、厳密に割り込み時点に処理をしたい場合は割り込みハンドラ内に処理を記述しますが、あまり大きく重い処理はしないように注意します。

文字列送信DMA転送

めかのとろ

DMAを使用した文字列送信は割り込みを使用していない基本的なものを例にあげて解説します。
シンプルな送信プログラムですが、DMAの働きがよくわかります。

めかのとろ

まず転送する文字列を格納する配列TxData[]を準備します。ここでは文字数(配列数)つまりDMAに指定するBuffer_Sizeは送信の都度文字列に応じて指定します。受信の場合と異なるところは変換モードが連続のサーキュラモードではなくノーマルモードを指定しているところです。

めかのとろ

この場合は送信ごとに転送するデータ数を管理するレジスタDMA_CNDTRxの値として転送分の文字数(DMA_BufferSize)を指定する必要があります。このレジスタは転送がはじまるとインクリメント(増加)していき、指定値に達すると0に戻ります。サーキュラモードの場合はこれを繰り返すのですが、ノーマルモードでは何もしないとレジスタ値は0のままです。

めかのとろ

そこでサンプルプログラムでは送信のたびにDMAを初期化して転送サイズを指定しています。慣れてくるとこのレジスタ値だけを指定すればよいので、DMA1_Channel2 -> CNDTR= BufferSizeのようにレジスタに直接設定することもできます。

めかのとろ

レジスタを直接操作すると無駄がなくなるので、プログラムはより軽く速く効率がよくなります。ただし、直感的に理解しにくくはなりますが。

シリアル通信DMA送信

 ① 設定パラメータ、関数等の宣言 

めかのとろ

設定パラメータ等を宣言します。通信プログラムでは送受信を処理する部分は関数にまとめておいてから、アプリ内で任意のタイミングで使うことが多いため、作成する関数の宣言をしておきます。

 ②③ 設定パラメータ、関数等の宣言 

めかのとろ

使用するペリフェラルにクロックを供給し、GPIOをはじめ、初期化しておきます。

 ④ アプリケーション内の通常処理 

めかのとろ

アプリケーションプログラム本体で文字列送信を実行しています。サンプルでは単に登録した文字を無限に表示するものですが、DMAとシリアル送信が機能している動作が確認できます。

 ⑤ よく使う処理の関数化 

めかのとろ

今回作成した送信関数および送信関数内で使用するDMA初期化部も関数化しています。送信関数を実行し、DMA転送を開始するたびにDMA_Cmd関数を実行しています。DMA_GetFlagStatus関数を利用してDMAによる転送が完了するまで待機しています。

めかのとろ

ここまでで、DMAによる転送を行ったシリアル送信の解説をしてきましたが、ここでも転送完了で待機しているためこのままではあまり実用的ではありません。DMA転送を使用する場合は、CPUの負担を減らさなければあまり意味がないため、もうひと工夫必要です。

コラム

実際の教材を使用したアプリケーションプログラムにおいては実用的なノウハウがつまっていますので、ぜひご覧いただき、さらに発展させてみてください。