春雨日記 about me tags

GPIOのODRにデータを入れる事でデータを送信していましたが,DMAを用いて効率化してみました.

最近研究でSTM32ばっかり触ってるので微妙に食傷気味ですが,液晶をパラレル駆動したかったのでDMAと対峙する事にしました.笑

はじめに

使用するボード: WeAct製STM32F401CCU6

出力信号: 16ビットパラレル出力+クロック出力

F407等の高級機だと実はFSMCで何の苦労も無く上記の信号を出力できますが,F401などのローエンド機ではそんなものありません.

出力はGPIOA[0-7]とGPIOB[0-7]で2本使い,16ビットパラレル転送としています.

DMAのストリームを複数使用する事でこのような2つのペリフェラルを使った同期転送も可能です.

また,タイマをトリガとして使用している事からクロックの同時出力も可能です.

これらをCubeMXで設定する方法がわからなかった為,初期化以外の設定をほぼレジスタで直接行います.

※今回の記事は,主にこのフォーラム投稿に倣っています.その中でも疑問に思った点がいくつかあったので,翻訳しつつ注釈を入れ,クロック出力を付加している感じです.

CubeMX設定

必要なのは

  • 出力のGPIOポート設定
  • タイマの初期設定

以上です.

データ用のGPIOは必要なピンをOutputにしてSpeedをVery Highにするぐらいなので省略して,タイマの設定を以下に示します.

クロック用PWM出力にCH1を,DMAのトリガソースにCH2,CH3を使用しています.

画像は無いですが,クロック出力ピンはGPIO settingsからPull-UPにして下さい.これをしないと出力後の状態が不定となります1

立ち上がり・立ち下がりエッジを変更する際は,PWM modeを変更して下さい.

ピンの表示はこんな感じ.

PC13は動作確認LEDで使用しています.特に関係無いです.

DMAについて

DMAとは,Direct Memory Accessの略で,CPUとは別にDMAコントローラがメモリ上のデータを操作する仕組みです.

CPU時間を消費せずにデータのやり取りができる事から,センサの値を黙々と転送したり,画像等の大きなデータを後ろで勝手に送って貰うといった処理が可能となります.

大まかに言ってDMAで必要となるのは転送元,転送先,転送量,そしてトリガソースです.

今回は転送元がデータ配列,転送先がGPIOのODRレジスタ,転送量は配列のサイズと明確ですが,トリガを何にするのかという問題が発生します.

たとえばDMAを用いたADCからメモリへの読み出しの場合,ADCの変換完了割込みが用いられますが,配列対GPIOとなると,トリガソースが不在です.

そこでタイマを使用します.タイマを利用することで一定の速度を保つ事ができるという利点もあり,液晶の駆動にはこの特性が特に適していると言えます.

STM32F4シリーズのDMAはDMA1とDMA2の2つがあり,それぞれでトリガに設定できるソースが異なります2

中でも,「ストリーム」と「チャネル」という言葉が登場しますが,これらはデータの流れとトリガを表しています.

(注釈2より引用)

たとえば,TIM1のオーバーフロー割込み(TIM1_UP)を利用する場合,DMA2 Stream5 Channel6を使用する必要がある訳です.

今回はトリガソースをTIM1_CH2とTIM1_CH3としているため,DMA2 Stream2 Channel6とDMA2 Stream6 Channel6を使用します.

異なるストリームに同じタイミングで発生するトリガソースを設定する事で,複数のストリームを同時に動作させる事ができます.

プログラム

https://git.haru3.me/haru/stm32f401ccu6-dma-gpio/src/branch/master/Core/Src/main.c

上記のリンクで全文読めます.ここでは要点のみ触れますので,参考にする際は必ず参照して下さい(プロジェクトごと上がっています).

DMAストリーム初期化

1
2
3
4
5
6
7
8
9
  //TIM1_CH3 @ DMA2/Stream6/CH6
  DMA2_Stream6->NDTR = 16;
  DMA2_Stream6->M0AR = (uint32_t)data;
  DMA2_Stream6->PAR = (uint32_t)&GPIOA->ODR;
  DMA2_Stream6->CR = (6u << DMA_SxCR_CHSEL_Pos) | //CH6
		  	  	  	  	  DMA_SxCR_MINC			| //Memory Address Increment
						  DMA_SxCR_DIR_0		| //Memory to Peripheral
						  DMA_IT_TC				| //Transmission Complete Interrupt
						  DMA_SxCR_EN;			  //Stream Enable
  • NDTRは転送する回数
  • M0ARは転送元
  • PARは転送先
  • CRはコンフィグレーションレジスタ

となっています.

アドレスは内部でuint32_tとなっっているため,キャストしておくと警告が出なくなります.

コンフィグレーションの中身はアプリケーションノートとかHALライブラリの動作を追った方がわかりやすいと思いますが,一応コメントで意味を書いておきました.

DMA_IT_TCで転送完了割込みを有効化しています.

タイマ初期化

1
2
3
4
5
6
7
8
9
  TIM1->PSC = 0;
  TIM1->ARR = 83;												//1MHz for DMA clock
  TIM1->DIER =  TIM_DIER_CC2DE | TIM_DIER_CC3DE;				//Enable interrupts. (add TIM_DIER_UDE for TIMx_UP.)
  TIM1->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E;	//Capture compare enable. used for PWM and OC.
  TIM1->CCR1 = 41;												//About half for clock output timing.
  TIM1->CCR2 = 0;												//Start immediately for DMA transfer when timer starts.
  TIM1->CCR3 = 0;												//same
  TIM1->CNT = TIM1->ARR - 1;									//Workaround to cancel incorrect edge on beginning.
  TIM1->BDTR = TIM_BDTR_MOE;									//Master Output Enable
  • PSCは分周比
  • ARRはカウンタ(TIMx->CNT)の最大値

84MHzのメインクロックを分周無しで84カウント(※0-83なので84カウント)となり,1MHzのクロックとなります.

  • DIERは割込み/DMAトリガ有効化レジスタ

ここではCC2とCC3の割込みを有効化しています.タイマのオーバーフロー割込みを使用する際はTIM_DIER_UDEを使用します.

  • CCERはCapture Compare有効化レジスタ(※Capture CompareとはTIMx->CNTと設定値の大小を比較して色々する機能)
  • CCR1,CCR2,CCR3は上記のCapture Compareの設定値
  • CNTは現在のカウント(※ワークアラウンドが入っています)
  • BDTRはBreak/デッドタイムを設定するレジスタ

となっています.

割込みハンドラ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//HISR(Stream4~Stream7)
void DMA2_Stream6_IRQHandler(){
	if(DMA2->HISR & DMA_HISR_TCIF6){
		DMA2->HIFCR |= DMA_HIFCR_CTCIF6;
		TIM1->BDTR &= ~(TIM_BDTR_MOE);
		TIM1->CCER &= ~(TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E);
		TIM1->CR1 &= ~(TIM_CR1_CEN);
		GPIOA->BSRR = 0xff;
		GPIOB->BSRR = 0xff;
	}
}

この関数は

1
  __NVIC_EnableIRQ(DMA2_Stream6_IRQn); //Enable Interrupt

と対応しており,規則に従った名前の関数を作ると自動的に呼ばれるようです.そのためCubeMXで割込みコードを生成すると場合によっては干渉するので気をつけて下さい.

割込みのレジスタはLISRとHISR(lowerとhigher)に分かれており,ストリームによって受け取れるレジスタが異なります.

ここではStream6でHISRとなっています.

DMA_HISR_TCIF6はストリーム6の転送完了割込みを示しており,このビットが立っていた際は転送完了を示します.

受け取ったフラグは次回の為にリセットする必要があるため,HIFCR(もしくはLIFCR)でクリアさせます.

TIM1の3つはタイマを停止させています.少なくともCCERのCC1Eを取り除かなければクロックが出っぱなしとなり,正常に通信できなくなります.

なお,DMA転送の大まかな流れはアプリケーションノートに記載があります3

動作確認

ロジックアナライザ(ADALM2000)を用いて出力信号を確認しました.

  • CH0-7はA0-A7
  • CH8-14はB0-B6
  • CH15がクロック

となっています.(1本足りない泣)

きちんと1MHzで同時に2ストリーム流れており,クロックも正しく出ている事がわかります.

同時と言っても順番に処理しているので微妙に遅れがありますが,立ち下がりエッジには余裕で間に合っているため問題ではありません.

おわりに

液晶を動かしたいという事で,気軽に調べ始めた結果かなり深い所まで来てしまいました.

今回ロジックアナライザとして使用したADALM2000,かなり多機能でいつも手元にある気がします.アクティブラーニングキットという名前は伊達では無いな…

どうでもいい蛇足: この記事に限った話ではないですが,私自身完全に理解しているとは言えないので,自己責任でお願いします.

おまけ: GPIOのODRレジスタはバイト単位でアクセス可能

これ知ってました?

たとえば,GPIOAのA0~A7を使用してパラレルデータを送信する時,これまでは

1
GPIOA->ODR = (GPIOA->ODR & 0xff00) | (uint8_t)data;

という風にちゃんと上位8ビットを逃がして書き込むという処理をしていたのですが,

1
*(uint8_t*)(&GPIOA->ODR) = (uint8_t)data;

こうすることですんなり下位8ビットのみ書き換える事ができました.

データシートを見る限りビット単位でアクセス出来そうな気もしなく無いですが,どうやればいいんだろう..?

こういうプログラムを書くと

こうなるわけです.(A10以降は省略)

パラレル通信に限らず,大量のGPIO操作を行う際には便利そうです.


  1. https://community.st.com/s/question/0D50X00009Xkf0vSAB/haltimpwmstop-leaves-output-pin-in-undefined-state ↩︎

  2. AN4031アプリケーションノート p8,9より ↩︎

  3. AN4031アプリケーションノート p14,15より ↩︎