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ストリーム初期化
|
|
- NDTRは転送する回数
- M0ARは転送元
- PARは転送先
- CRはコンフィグレーションレジスタ
となっています.
アドレスは内部でuint32_tとなっっているため,キャストしておくと警告が出なくなります.
コンフィグレーションの中身はアプリケーションノートとかHALライブラリの動作を追った方がわかりやすいと思いますが,一応コメントで意味を書いておきました.
DMA_IT_TC
で転送完了割込みを有効化しています.
タイマ初期化
|
|
- 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/デッドタイムを設定するレジスタ
となっています.
割込みハンドラ
|
|
この関数は
|
|
と対応しており,規則に従った名前の関数を作ると自動的に呼ばれるようです.そのため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を使用してパラレルデータを送信する時,これまでは
|
|
という風にちゃんと上位8ビットを逃がして書き込むという処理をしていたのですが,
|
|
こうすることですんなり下位8ビットのみ書き換える事ができました.
データシートを見る限りビット単位でアクセス出来そうな気もしなく無いですが,どうやればいいんだろう..?
こういうプログラムを書くと
こうなるわけです.(A10以降は省略)
パラレル通信に限らず,大量のGPIO操作を行う際には便利そうです.
-
https://community.st.com/s/question/0D50X00009Xkf0vSAB/haltimpwmstop-leaves-output-pin-in-undefined-state ↩︎
-
AN4031アプリケーションノート p8,9より ↩︎
-
AN4031アプリケーションノート p14,15より ↩︎