STM32いじってみた(10) DMA編(その2)

実例と実装編です。まずは設定の基本ルーチンから示します。

設定ルーチン

とりあえずマニュアルによると DMAC に関してはこうしろと書いてあります。

更に必要なのが、

  • DMA へのクロック供給(重要)
  • ペリフェラルの初期化
  • ペリフェラルの DMA 使用設定
  • (必要ならば)DMA 割り込み登録

です。そして大事なのは、DMA チャネルです。チャネルによって扱えるペリフェラルの種類が異なりますので、ユーザーマニュアルでよ〜く確認してください。一部を抜粋したのが以下の画像です。

以上を注意すれば、実はこれだけで動作し始めます。スゴイデスネ。以下くだらない実例を見ていきましょう。

実例1: ADCでリアルタイムにスキャン(データ1つ)

一番基本です。ソースコードを示しますね。

uint16_t adc_dat; // ADCのデータを格納するための変数
void dma_adc_init(void) { // using DMA1_1
    adc_init(); // 何らかの方法で通常通りADC1を初期化

    RCC->AHBENR |= RCC_AHBENR_DMA1EN; // supply clock to DMA1
    DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; // peripheral addresss
    DMA1_Channel1->CMAR = (uint32_t)&adc_dat; // memory address
    DMA1_Channel1->CNDTR = 1; // 1 data
    DMA1_Channel1->CCR |= DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_CIRC;
    // Peripheral: 32bit, Memory: 16bit, Read from Peripheral, circular mode.

    DMA1_Channel1->CCR |= DMA_CCR_EN; // Channel enabled

    ADC1->CFGR |= ADC_CFGR_DMAEN | ADC_CFGR_DMACFG; // enable DMA as Circular Mode.
    ADC1->CFGR |= ADC_CFGR_CONT; // continuous single convert mode
    ADC1->SQR1 &= ~ADC_SQR1_SQ1_Msk; // delete the channel information
    ADC1->SQR1 |= (1 << ADC_SQR1_SQ1_Pos); // channel number of ADC1
    ADC1->CR |= ADC_CR_ADSTART; // start a/d convert
}

この関数を実行するだけで、リアルタイムに ADC1 の CH1 でスキャンが行われます。使用例ですが、例えば main 関数中に

while(1) {
    ms_wait(1000); // wait for 1sec
    uart_printf("%d\n", adc_dat);
}

とすればその時のデータが表示されるというわけですね。

さて、初期化方法について軽く説明します。まず最初に対象のペリフェラルを初期化してください。いつも使う通りでいいです。追加の設定は後で行っていきましょう。
次に DMA への設定と有効化です。

RCC->AHBENR |= RCC_AHBENR_DMA1EN; // supply clock to DMA1
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; // peripheral addresss
DMA1_Channel1->CMAR = (uint32_t)&adc_dat; // memory address
DMA1_Channel1->CNDTR = 1; // 1 data
DMA1_Channel1->CCR |= DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_CIRC;
// Peripheral: 32bit, Memory: 16bit, Read from Peripheral, circular mode.

DMA1_Channel1->CCR |= DMA_CCR_EN; // Channel enabled

このブロックでは DMA の有効化 から始まっています。これをやらないと全く設定が反映されませんからね。すっごく大事ですね(ハマって苦労した顔)。
その次に CPAR でペリフェラルレジスタのアドレスを指定します。この場合は ADC1 のデータレジスタですね。続いて CMAR はメモリのアドレス(RAM上の変数アドレス)を指定します。デフォルトでペリフェラルから読みメモリに書き出すという動作になるのですが、これは別のレジスタのビットを変更すれば逆向きに設定できます。CNDTR はやり取りするデータ数です。今回は ADC データ1つなので 1 としました。

続いてキモとなる CCR です。PSIZE ビットと MSIZE ビットは 0ビットめを1にすると1データのビット長は 16bit だ言うことになります。CIRC はサーキュラーモードと言われるもので、このビットを立てると連続的に DMA 処理をしています。最後に EN を立てて設定完了です。

最後に ADC1 への DMA 用追加設定です。ポイントは以下。

ADC1->CFGR |= ADC_CFGR_DMAEN | ADC_CFGR_DMACFG; // enable DMA as Circular Mode.
ADC1->CFGR |= ADC_CFGR_CONT; // continuous single convert mode

コメントのとおりなのですが、Continuous モードにするのをお忘れなく(連続変換をするのでそれはそう)。

実例2: ADCでリアルタイムにスキャン(データ2つ)

2つバージョンです。ソースコードは以下。

uint16_t adc_dat[2];
void dma_adc_init(void) { // using DMA1_1
    adc_init();
    RCC->AHBENR |= RCC_AHBENR_DMA1EN; // supply clock to DMA1
    DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; // peripheral addresss
    DMA1_Channel1->CMAR = (uint32_t)dac_dat; // memory address
    DMA1_Channel1->CNDTR = 2; // 2 data *変更*
    DMA1_Channel1->CCR |= DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_CIRC;
    // Peripheral: 32bit, Memory: 16bit, Read from Peripheral, circular mode.

    DMA1_Channel1->CCR |= DMA_CCR_EN | DMA_CCR_MINC; *変更*

    ADC1->CFGR |= ADC_CFGR_DMAEN | ADC_CFGR_DMACFG; // enable DMA as Circular Mode.
    ADC1->CFGR |= ADC_CFGR_CONT; // continuous single convert mode
    ADC1->SQR1 &= ~ADC_SQR1_L_Msk; // delete number information *変更*
    ADC1->SQR1 |= (1 << ADC_SQR1_L_Pos); // 2data *変更*
    ADC1->SQR1 &= ~ADC_SQR1_SQ1_Msk; // delete the channel information
    ADC1->SQR1 &= ~ADC_SQR1_SQ2_Msk; // delete the channel information *変更*
    ADC1->SQR1 |= (1 << ADC_SQR1_SQ1_Pos); // 1st channel number of ADC1
    ADC1->SQR1 |= (2 << ADC_SQR1_SQ2_Pos); // 2nd channel number of ADC1 *変更*
    ADC1->CR |= ADC_CR_ADSTART; // start a/d convert
}

変更点は *変更* と示しておきました。2つ用に色々変えただけです。中でもポイントはこちら。

DMA1_Channel1->CCR |= DMA_CCR_EN | DMA_CCR_MINC; *変更*

MINC はメモリインクリメントの意味で、1を立てると書き込み先変数アドレスをインクリメントしてくれます。これで一番上で uint16_t adc_dat[2]; とした1つめには ADC1_CH1 のデータ、2つめには ADC1_CH2 のデータが入るということです。

実例3: USART で無限に文字列を吐き続ける

メモリ読み書き方向の異なる実例として1つ。あと、なんとなく割り込みを入れてみました。

void dma_usart_init(uint32_t baudrate) {
    usart_init(baudrate);

    RCC->AHBENR |= RCC_AHBENR_DMA1EN; // supply clock to DMA1
    DMA1_Channel4->CMAR = (uint32_t)"UNKO "; // memory address
    DMA1_Channel4->CPAR = (uint32_t)&USART1->TDR; // peripheral addresss
    DMA1_Channel4->CNDTR = 5; // 5 data
    DMA1_Channel4->CCR |= DMA_CCR_CIRC | DMA_CCR_DIR | DMA_CCR_MINC;
    // Peripheral: 32bit, Memory: 16bit, Read from Memory, circular mode.

    USART1->CR3 |= USART_CR3_DMAT; // enable DMA transmitter
    USART1->ICR |= USART_ICR_TCCF;
    DMA1_Channel4->CCR |= DMA_CCR_EN | DMA_CCR_TCIE;
    NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}

DMA 設定のポイントは以下。

DMA1_Channel4->CCR |= DMA_CCR_CIRC | DMA_CCR_DIR | DMA_CCR_MINC;

真ん中の DMA_CCR_DIR がポイントです。これを立てると、上で述べた方向が逆になります。つまりメモリからデータを読んでペリフェラルにデータを書き込むという逆動作になります。

DMA 転送完了時に割り込みを設定していますが、それが以下の部分。コード末尾です。

DMA1_Channel4->CCR |= DMA_CCR_EN | DMA_CCR_TCIE;
NVIC_EnableIRQ(DMA1_Channel4_IRQn);

DMA_CCR_TCIE をオンにすると DMA から割り込みが入ります。そのために NVIC_EnableIRQ をしています。さてその割り込み関数ですが、適当にチカチカされておいています。当然ピンの出力設定は別途してくださいね!!

void DMA1_Channel4_IRQHandler(void) {
    if( DMA1->ISR & DMA_ISR_TCIF4_Msk ) {
        GPIOB->ODR ^= (1 << 7); // toggle output on PB7
        DMA1->IFCR |= DMA_IFCR_CTCIF4;
    }
}

久しぶりの STM32 ペリフェラル設定ネタ投下でした。DMA は便利ですが一見設定が面倒臭そうだと思われる人も多いと思います。しかし、この記事によって実は割と設定は簡単と分かっていただければ嬉しいなあと思っています。