STM32でDMA転送を使ってみる(USART編)

出典: Wikimura

ポーリングに比べれば、割り込みを使えば効率的にデータ転送ができると言われています。 DMAを使えば、CPUはデータ転送に煩わされることなく自分の仕事に集中できるようになり、割り込みに比べて更に効率的になります。

割り込みの場合レジスタ退避などのオーバーヘッドが伴うため、1MHzで割り込みなんて現実的ではありません。一方DMAは、STM32のバスアービタが許す限りの転送速度が使えます。システムクロック72MHzの場合、その半分の36MHzがDMA転送に割けるそうです。

CANSAT(実は今回参加するのはOpenClass)では、GPSデータの受信、センサデータの受信、DAC操作など、データのやり取りは結構あります。中でもDACはFSK変調波を生成するのに使うため、100kHz以上で動かしたいと考えています。こうなるとDMAしか考えられません。

ここでは、練習としてUSARTの送信をDMA転送に挑戦します。


目次

DMAのAPI

DMA関数

STM32 Standard Peripherals LibraryのDMAの項を見ると、関数はあまり多くないことが分かりました。以下に出てくる保留フラグ(Pending flag)とは、イベントが発生したことを表すフラグのようです。割り込み保留フラグとは別に、イベントが発生するとセットされるそうです。ポーリングでチェックするためのフラグのようです。

DMA_ClearFlag

DMAyチャネルxの保留フラグをクリアする。

DMA_ClearITPendingBit

DMAyチャネルxの割り込み保留ビットをクリアする。

DMA_Cmd

DMAyチャネルxを有効化・無効化する。 有効化すると転送が開始される。

DMA_DeInit

DMAyチャネルxのレジスタをリセット値に戻す。

DMA_GetCurrDataCounter

DMAyチャネルxの残り転送データ数を返す。

DMA_GetFlagStatus

DMAyチャネルxの指定したフラグがセットされているかを返す。→保留フラグの方

DMA_GetITStatus

DMAyチャネルxの割り込みが起きたかをチェックする。→割り込み保留ビットの方

DMA_Init

DMAyチャネルxを指定されたDMA_InitStruct型のパラメータで初期化する。

DMA_ITConfig

DMAyチャネルxの割り込みを許可・禁止する。

DMA_StructInit

DMA_InitStructのメンバをデフォルト値で埋める。

初期化構造体

DMA_BufferSize

転送するデータ数(バイト数ではない)。 転送元と転送先のデータサイズは別々に指定できるのでどちらか不明。おそらく転送元でのデータ数かと思われる。NDT(Number of Data to Transfer)レジスタのことだと思われる。

NDTレジスタの説明では、[Number of data to be transferred]と書かれていたので、転送されるデータ数...つまり転送元で決まると考えた。サイズが異なるときの振る舞いについては書かれていない。一応「サイズが異なる場合はデータのパッキング・アンパッキングが自動で行われる」とあったが、MSB or LSBのどちらから転送されるのだろう?

DMA_DIR

データ転送方向。ペリフェラルが転送先か転送元かを決める。とり得る値は以下の2つ。

  • DMA_DIR_PeripheralDST: ペリフェラルが転送先
  • DMA_DIR_PeripheralSRC: ペリフェラルが転送元

DMA_M2M

メモリ間転送モードにする。通常はペリフェラル-メモリ間の転送を目的としているらしい。

  • DMA_M2M_Disable: メモリ間転送無効(ペリフェラル-メモリ間転送モード)
  • DMA_M2M_Enable: メモリ間転送モード

DMA_MemoryBaseAddr

メモリ側の基底アドレス。

DMA_MemoryDataSize

メモリ側のデータサイズ。とり得る値は以下の3つ。

  • DMA_MemoryDataSize_Byte: 8ビット
  • DMA_MemoryDataSize_HalfWord: 16ビット
  • DMA_MemoryDataSize_Word: 32ビット

DMA_MemoryInc

メモリ側のアドレスを転送毎にインクリメントするかどうかを決める。とり得る値は以下の2つ。

  • DMA_MemoryInc_Disable: メモリインクリメントモード無効
  • DMA_MemoryInc_Enable: メモリインクリメントモード有効

DMA_Mode

DMAの動作モードを決める。通常モードかサーキュラーモード(転送が終わったらアドレスが最初に戻って、転送が自動で再開する)が選択できる。とり得る値は以下の2つ

  • DMA_Mode_Circular: サーキュラーモード
  • DMA_Mode_Normal: 通常モード

DMA_PeripheralBaseAddr

ペリフェラル側の基底アドレス。

DMA_PeripheralDataSize

ペリフェラル側のデータサイズ。とり得る値は以下の3つ。

  • DMA_PeripheralDataSize_Byte: 8ビット
  • DMA_PeripheralDataSize_HalfWord: 16ビット
  • DMA_PeripheralDataSize_Word: 32ビット

DMA_PeripheralInc

ペリフェラル側のアドレスを転送毎にインクリメントするかどうかを決める。とり得る値は以下の2つ。

  • DMA_PeripheralInc_Disable: ペリフェラルインクリメントモード無効
  • DMA_PeripheralInc_Enable: ペリフェラルインクリメントモード有効

DMA_Priority

DMA転送のソフトウェア優先度を決定する。これが同じチャネルがかちあった場合、ハードウェア優先度(チャネル番号の小さい方が高優先度)が使われる。とり得る値は以下の4つ。

  • DMA_Priority_VeryHigh: 最優先
  • DMA_Priority_High: 高優先度
  • DMA_Priority_Medium: 中優先度
  • DMA_Priority_Low: 低優先度


DMAクロック有効化

DMAコントローラもクロック有効化が必要です。DMAコントローラはAHBに属するので、以下の関数を使います。

void RCC_AHBPeriphClockCmd( uint32_t  RCC_AHBPeriph, FunctionalState  NewState ) 

DMA1のクロック有効化:
RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE);

DMA2のクロック有効化:
RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA2, ENABLE);

DMA1および2のクロック有効化:
RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1 | RCC_AHBPeriph_DMA2, ENABLE);


USART2の場合

DMAは2基搭載されており、それぞれチャネル1~7があります。各チャネルは固有のDMA要求に対応付けられています。

USART系のDMA要求は以下のようにチャネルに割り振られています。

  • USART3_TX: 2
  • USART3_RX: 3
  • USART1_TX: 4
  • USART1_RX: 5
  • USART2_RX: 6
  • USART2_TX: 7

USART2の送信をDMA転送したい場合、チャネル7を設定します。 DMA要求とチャネルの関係はリファレンスマニュアルのDMA解説に載っています。


DMA1_Configuration

DMA1を設定する関数を作りました。 DMA1をUSART2のDMA要求で動くように設定しました。 外部で定義した文字列msgの先頭アドレスを送信元に設定しています。

USART2に対応付けられた、DMAのチャネル7を以下のように初期化しています。

  • ペリフェラル基底アドレス: USART2のDR
  • メモリ基底アドレス: msg(文字列)の先頭アドレス
  • 転送方向: メモリ→ペリフェラル
  • 転送データ数: 文字数(Null文字は?)
  • ペリフェラルインクリメントモード: 無効
  • メモリインクリメントモード: 有効
  • ペリフェラルデータサイズ: 8bit
  • メモリデータサイズ: 8bit
  • 転送モード: 通常モード
  • 優先度: 最優先
  • メモリ間転送: 無効
void DMA1_Configuration(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    // DMAのUSART2 TXに対応するのはチャネル7
    // 既に設定がある場合はあらかじめリセット値に戻すと安全?
    DMA_DeInit( DMA1_Channel7);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)msg;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize = sizeof(msg)/sizeof(uint8_t);
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init( DMA1_Channel7, &DMA_InitStructure);
}

USART側の設定

リファレンスマニュアルによれば、USART送信をDMAで使えるようにするためには以下のようにするそうです。

  1. データレジスタUSART_DRをDMA転送先となるよう設定する(サイズは8bit)
  2. メモリ上のDMA転送元を設定する(8bitでなくても可)
  3. バイト数、優先度、割り込みの設定等をする
  4. DMAチャネルをアクティブにする

アクティブになった瞬間に動作を始めるようです。

ただし、送信割り込みを許可(TXEIEをセット)してはいけないとのことです。

USARTのDMA設定関数

STM32 Standard Peripheral LibraryのUSARTを調べたところ、DMAを有効化する関数がありました。

USART_DMACmd

USARTxのDMA転送要求(USART_DMAReq)を有効化・無効化(NewState)にする。

USART_DMAReqは以下の2つのどちらか、またはその組み合わせ(ORをとる)が指定できる。

  • USART_DMAReq_Tx: DMA送信要求
  • USART_DMAReq_Rx: DMA受信要求

NewStateは有効化・無効化を指定します。

  • ENABLE: 有効化
  • DISABLE: 無効化
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState)
行われている処理:
  有効化: USARTx->CR3 |= USART_DMAReq;
  無効化: USARTx->CR3 &= (uint16_t)~USART_DMAReq;

USART2_Configuration

Olimex社のSTM32-P103ボードを使うため、RS-232コネクタ(メス)の付いているUSART2を使います。USART2の設定関数を以下のように作りました。

  • ボーレート: 9600 bps
  • データビット: 8
  • ストップビット: 1
  • パリティ: 無し
  • ハードウェアフロー制御: 無し
  • 使用ピン: Txのみ
  • DMA要求: 有効
void USART2_Configuration( void)
{
    USART_InitTypeDef USART_InitStructure;

    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx;

    // USARTxに設定を反映
    USART_Init(USART2, &USART_InitStructure);
    // DMA送信有効化
    USART_DMACmd( USART2, USART_DMAReq_Tx, ENABLE);
    // USART有効化
    USART_Cmd( USART2, ENABLE);
}


Eclipseプロジェクト

Olimex社のSTM32-P103、STM32-H103ボードで動作するプログラムを作りました。

コメント

  • 関係ないことですが、STM32 Primer2がStrawberry Linuxで販売されるようになったんですね...Digikeyで8000円位で買ったのに...
  • JTAGデバッガでテストしていると、最初のブレイクポイントに到達する前にすでにメッセージを送信されてしまいます...勝手に1回実行されているのでしょうか?
    • OpenOCDを再起動したらいつの間にか治っていました。良く分かりません。再現性が無いようです。

参考文献

個人用ツール