Teensy 3.2のHardware Quadrature Decoderを試す
Teensy3.2という、ArduinoIDEにて開発できる32bitマイコンボードがある。
Arduino pro miniとほぼ同じくらいのサイズでありながら、Cortex M4を搭載した96MHz動作のスゴイやつだ。出力ピンも揃っていて使いやすく、Arduinoライクに書けて書き込みも速いので最近はこいつを使う機会が多くなっている。
そのなかで、ハードウェアデコーダ機能によるインクリメンタル型のエンコーダの読み取りについてチャレンジしてみたので、備忘録がてら軽くここに置いておこうと思う。
ただし、このコラムはある程度マイコン開発に慣れた人を対象にしている。
ご了解いただきたい。
Quadrature Decoder mode とは
マイコンのような組み込みデバイスは、その名の通り機械を制御する装置に組み込まれて使われることが多い。機械を動かすもの、いわゆるアクチュエータの状態を読み込み、加工(計算)して、期待通りの動きとなるようその挙動を制御する。
アクチュエータとして世間に大人気なのはモータであるが、このモータの状態を読み込むセンサとして回転エンコーダが一般的に使われている。この回転エンコーダのうち、インクリメンタル型と呼ばれるタイプの値を読み込むのが、直交デコーダ(Quadrature Decoder)である。
直交デコーダはソフトウェア処理として実装することもできるが、インクリメンタル型の性質上、割り込み処理の増大によりマイコンのリソース効率が非常に悪くなる。特に複数のエンコーダchを処理しようとすると、読み飛ばしや挙動の不安定化に至ってお話にならない。(ここでは、500pulse/rot程度のエンコーダのモータ軸直付け程度を想定している)。
さて、Teensyシリーズの開発は、拡張したArduinoIDEを使用することで行われる。この拡張機能は同時にMsTimer2やThreadのような信頼性のある様々なライブラリも導入するのだが、実はこの中に「Encoder Library」というのがあって、これを使うと任意のピンを使ってA相とB相によるインクリメンタル型エンコーダの読み取りを行うことができる。
ところがどっこい。このライブラリはいわゆるソフトウェアデコーダであり、その内実はピン割り込みによるカウントに過ぎない。つまり、回転速度やメインプロセスによっては読み飛ばしが生じる恐れが拭えないのである。
前述の理由から、できることならハードウェアデコーダで済ましたいわけだ。ある程度、能力の高い汎用マイコンにはハードウェアに様々な機能が搭載されていて、32bitにもなればまぁ間違いなくハードウェアデコーダが存在していると考えて良い。
というわけで、Teensyのコアのデータシート(MK20DX256)から該当機能を探してみた。
K20 Sub-Family Reference Manual (FreeScale)
ありました。ハードウェアデコーダ機能 『Quadrature Decoder mode』(QDmodeと呼ぶことにする)
構造としてはPWMやタイマー割り込みに使用するFTM counterを、内部クロックではなく外部ピンの状態に応じてアップ&ダウンカウントを行うもののようだ。
A相B相の立ち上がりをsynchronizerで同期信号に変換し、フィルターを通したうえでFTM counter directionでクロック信号と方向信号を出力、最後にFTM counterで積算するという流れである。
今回はこれを使うために、図中の各変数やら何やらの設定を行う。
ちなみになぜFFの直列二段で同期信号へ変更するのかと言うと、MCU内部クロックの立ち上がりと外部信号の立ち上がりがニアミスすると、内部で保持した値が不定になることがあるのだとか。この状態をメタステーブル状態と呼んで、直列二段のフリップフロップで避けるのが常識らしい。詳しくは↓。
QDの設定
Teensy3.2には3つのタイマー(カウンタ)があり、それぞれFTM0、FTM1、FTM2として存在している。QDmodeはこのうちFTM1とFTM2を使う。ちなみにFTM0は広範囲に使われているので、このマイコンの基幹として位置づけられているようだ。
まず、レジスタについて、 FTMEN = 1 QUADEN = 1 として設定をせよとのこと。
FTMENとQUADENはどこにあるのかというと…
K20 Sub-Family Reference Manual (FreeScale)
https://www.pjrc.com/teensy/K20P64M72SF1RM.pdfFTMx_MODEの1ビット目と、FTMx_QDCTRLの1ビット目である。なお、xはFTMの番号である。
以上のことを踏まえると、
FTM1_MODE |= 0;
FTM1_MODE |= 1<<2; //WPDIS
FTM1_MODE |= 1<<0; //FTMEN
FTM1_QDCTRL = 0;
FTM1_QDCTRL |= 1<<0; //QUADEN
となる。ちなみにWPDISは書き込み制限の解除らしい。Teensyならやらなくても可能。
ブロックダイアグラムを見ると、synchronizerの先にFilterが存在する。これはシステムクロックを4分周した周波数より高周波な入力をフィルターするものらしい(P831)。
K20 Sub-Family Reference Manual (FreeScale)
https://www.pjrc.com/teensy/K20P64M72SF1RM.pdf使わないなら以下のようになる。
//FTM1_QDCTRL &= ~(1<<7) ; //PHAFLTREN
//FTM1_QDCTRL &= ~(1<<6) ; //PHBFLTREN
QUADENのときに既にゼロにしたから実を言うと書く必要が無い。CH(n)FVAL[3:0]についても同様。
さらに読み進めると、PHAPOLとPHBPOLを設定しろという。
これは論理の逆転を行うもので、まぁつまり両方ゼロで良いので特に書く必要はない。
(レジスタの値がゼロであることが確実でない場合は、もちろん処理したほうが良い)
QDmodeのための設定はだいたいこのくらいで終わりである。
ピンの設定
案外忘れがちなのが、ピンの設定である。FTMの設定がされても、入力ピンが設定されていないとウントモスントモ云わないので注意が必要である。
K20 Sub-Family Reference Manual (FreeScale)
https://www.pjrc.com/teensy/K20P64M72SF1RM.pdfFTM1_QD_PHAとPHBはALT7に登録されている。従って、PTA12とPTA13のピンをALT7にすれば良い。
K20 Sub-Family Reference Manual (FreeScale)
https://www.pjrc.com/teensy/K20P64M72SF1RM.pdfALTの設定はPORTx_PCRnの8~10ビットで決定される。従って、FTM1の場合は下のように書ける。
PORTA_PCR12 |= B111 << 8; //pin set ALT7
PORTA_PCR13 |= B111 << 8; //pin set ALT7
FTMの設定と桁溢れフラグ
値が内部でどのようにカウントされているのかを表す以下のような図がある。
K20 Sub-Family Reference Manual (FreeScale)
https://www.pjrc.com/teensy/K20P64M72SF1RM.pdfFTM counterの中で、CNTINからMODまでの領域でアップ、ダウンカウントを行っている。このCNTINとMODも設定が必要である。CNTIN=0とすれば、MODの値が最大カウント値になる。MODの取りうる値は16bitの範囲内だ。
FTM1_MOD = 5000 - 1;
FTM1_CNTIN = 0;
もうひとつ重要なのはTOFとTOFDIRである。カウント値が飛ぶ際にTOFが1に、それがアップカウントの場合はTOFDIRが1、ダウンカウントの場合はTOFDIRが0となるようだ。
TOFDIRはFTMx_QDCTRLの1ビット目、TOFはFTMx_SCの7ビット目にある。
K20 Sub-Family Reference Manual (FreeScale)
https://www.pjrc.com/teensy/K20P64M72SF1RM.pdf値はFTMx_CNTの中にあるので、カウント値の取得はTOFを監視しながら以下のようになる。
int ct = 0 ;
int over = 0 ;
int Count = 0 ;
int MAX_COUNT_VALUE = 5000 ;
void setup(){
//略
}
void loop() {
ct = FTM1_CNT;
if(FTM1_SC & 1<<7){ //TOF
FTM1_SC &= ~(1<<7); //Clear TOF
if(FTM1_QDCTRL & 1<<1) over += MAX_COUNT_VALUE; //TOFDIR
else over -= MAX_COUNT_VALUE;
}
Count = ct + over;
Serial.println( Count );
delay(50);
}
上の例はポーリング処理の場合である。これが正常に動くのは「ポーリング周期がエンコーダパルスに対して十分に早い」かつ「正転逆転の動きが十分に緩慢である」という条件を満たす場合のみなので注意されたい。
例えば、ポーリング周期のあいだに、桁上りと桁下がりが瞬時に行われたとしよう。具体的にはMODが4999だったとして、4999→0→4999のときである。このとき、カウント値(ct + over)は4999であるべきだが、QDは桁上りと下がりを経験したためTOFは1だしTOFDIRは0である。従って、上記のプログラムではoverは-5000され、カウント値(ct + over)は-1となってしまう。
以上のことから、ポーリングである程度安定した処理とするなら、実はTOFとTOFDIRを使わない次のようなやり方が良いように思う。
int ct = 0 ;
int over = 0 ;
int Count = 0 ;
inr tmpCount = 0 ;
int MAX_COUNT_VALUE = 5000 ;
void setup(){
//略
}
void loop() {
tmpCount = Count;
ct = FTM1_CNT;
Count = ct + over;
if ( (Count - tmpCount) > MAX_COUNT_VALUE/2 ){
over -= MAX_COUNT_VALUE;
else if ( (Count - tmpCount) < -MAX_COUNT_VALUE/2 ){
over += MAX_COUNT_VALUE;
}
Count = ct + over;
Serial.println( Count );
delay(50);
}
要は、Countの値は必ず連続的であることを用いた桁あふれ処理といえる。ただし、これもポーリングの間にMOD/2を超えるパルスが入力されないことが前提ではある。だからMODを最大値の0xFFFFに設定しておけば大体のシステムには対応できると思われる。
あとは上のプログラムをうまく組み合わせれば動く。以上!
/* メモ */
ポーリングを使わずに割り込み処理でやりたい勢はTimer Overflow Interrupt(36.6.1 P899)を使うと良い。やり方は知らないので頑張ってほしい。既にハードウェアQDを試した先人たちのライブラリは割り込みを使ってるので、それが参考になるかも。https://github.com/michaeljball/QuadDecode (github)
割り込みは便利だけど、前述のように瞬間的に桁上りと桁下がりを行った場合には割り込みが多重になったりするのかなぁなんて思う。先人のライブラリはフィルターを利用して4クロック以下の信号を無視するようにしているので、瞬間的な信号の問題をなくしているみたい。でもそこまでするならポーリングで十分なのでは…?
/* メモ */
ハードウェアデコーダーなのに読み取り処理速度遅くね???って感じな気がする。ソフトウェアによるライブラリと最大読み取り可能回転速度が同じような気がする。
フィルターも無効化してるし、怪しいのはsynchronizerか?とはいえ、システムバスクロックの48MHzを同期に用いていて、しかも一回転2000パルスのエンコーダーを用いていたとしても、秒速2万4千回転は許容できるハズなんだ。電気的なアレコレだとしたら厄介だなぁ。
/* メモ */
オーバーフロー割り込みハンドラ。ポーリングを嫌う場合はコチラ。
これを実行するにはFTMx_SCのTOIEをEnable、FTMx_CnSCのCHIEをEnable(?)。
より確実にするなら。
FTMx_SCのCPWMS=0、MSnB=0、MSnA=0。
FTMx_COMBINEのDECAPENx=0、CONBINEx=0。
フィルターつけるなら。
FTMx_QDCTRLのPHAFLTREN=1、PHBFLTREN=1。FTMx_FILTERのCHnFVAL=(4分周カウント値)
/* メモ */
ライブラリのソフトウェアでは読み取りミスが生じているとの報告がちょくちょくある。徐々に原点がズレてしまうそうだ。それに対して、ハードウェアでは同様の問題が生じていない模様。ハードウェアを開拓する意義があったことに一安心。