小ネタ:PSoC で Quadrature Decoder (QuadDec) を使うときの注意点

PSoC でロータリーエンコーダを読むときには Quadrature Decoder (QuadDec) を使いますが、このモジュールの使い方でちょっとじたばたしたので忘れないうちにメモを残しときます。

どこでつまづいたかというと、出力の値を循環させるところです。今使っている用途ではロータリーエンコーダから読み取る値には正の値の範囲があって (0 <= n < max みたいな)、範囲を超えたら値を循環させたいのです。ロータリーエンコーダのカウンタ値はデフォルトでは 0 から始まるのでエンコーダを増加の方向に回した場合は何の問題もなく以下で済むんですが

uint8_t value = QuadDec_GetCounter() % range;
C

エンコーダは左に回すこともあるわけで、QuadDec_GetCounter() の値が負になったりすることもあるわけです。その場合の処理を考えておく必要があるという話です。そもそもモジュールに問題があるという話ではなく整数の扱いは時々意外と難しくなるという話なんですけれども。上の関数で例えば range= 7 の場合、値は以下のようになるわけです。

入力が負になったら出力も負の値です。0 から 6 までの循環値が欲しいので、これをこのまま使うとモジュールが誤動作します。そもそも modulo の値って必ず正の値にならないとおかしいんじゃないの?と最初思ったのですが、modulo は割り算の余りですから、例えば – 9 % 7 の場合、-9 / 7 = -1、-1 * 7 = -7 これにあまりを足せば元の数になるわけだから -9 / 7 = -1 あまり -2 と負の値になるわけです。なるほど。面倒です。

どうしましょう?色々な解決方法を試してみました

その1:ダメ:カウンタ値が負になったら range を足して強制セット

QuadDec_SetCounter() という API が用意されているから、カウンタの値が -1 になったら 7 を足してカウンタ値を更新、無理やり正の値にすればよいのでは?これはまあ、理屈通りに動けば簡単な解決法ですけれども、どうもだめなようです。ロータリーエンコーダが動いている最中にはハードウェア側からも状態の変更がかかるので、同時にカウンタをセットすると値が変わったり変わらなかったりで動作に全く信頼性がありません。これはだめでした。カウンタのセットは、ロータリーエンコーダが動いていない時にやらないといけないようです。人が動かすものなので基本無理?今やっているプロジェクトでは、エンコーダにスイッチが付いているので、スイッチを押した瞬間だけはエンコーダを回すことは基本無理なので例外的にセットして大丈夫です。PSoC のハードウェアはこうやって API を介して制御するので開発が手軽な反面融通が利かないところもあるような気がします。

その2:まあまあ:カウンタ値を負にならない大きな値にとる

カウンタ値の範囲は設定で 8bit/16bit/32bit が選べます。8bit の設定ではエンコーダ 256 クリックで一周してしまいますから駄目ですが、16bit なら初期値を 16384 の近くにしておけば一万6千クリック回さない限り回転しません。32bit の中点にしたら手で一周させるのは実質不可能でしょう(計算してませんが数年を必要とするか腱鞘炎になるか)。そうしておいて単純な計算式で循環値を得ると。これがまあ手っ取り早い解決法でありました。ただ、この方法はあっという間に一周してしまう 8bit 値では使えないし、16 bit でも暇な人がいれば回せてしまうでしょう。確実性という点で難があります。

その3:はまったら最強:循環周期 (range) を 2 のべき乗にする

この方法は例えば 16 = 2^4 ある MIDI チャネルの選択などで使えます。この問題について深く考えるとそれだけでひと記事になってしまいそうなぐらい深そうなんですけれども、整数の変数の実装を逆手にとります。例えば、符号付き 8 ビット整数の -1 は、符号なし 8 ビット整数では 255、-2 は 254 と、とれる値の範囲の「上半分」が負の値に割り当てられています。つまりもともとの値自体が循環しているのですが、この周期が range が 2 のべき乗の場合、必ず range の整数倍になります。つまり周期が同期しています。この場合処理は簡単で

value = (uint16_t) QuadDec_GetCounter() % range
C

と、カウンタの値を符号なし整数にキャストするだけで済みます。この方法はすごく簡単でいいですけど、周期が同期していないとだめです。例えば range が 7 だった場合、

(uint16_t) -1 % 7 = 65535 % 7 = 1
C

となってしまいますが、期待している値は 6 です。

その4:(考えるのが)面倒だが確実:正負に分けて違う計算式を充てる

この方法なら小細工はいりません。単純に余剰の定義にのっとって計算しているので、上に挙げてきた方法のように「ただし~に限る」の条件はありません。関数として一度組んでしまえば無条件に使えるので、最終的には結局この方法が一番楽でした。

int16_t counter = QuadDec_GetCounter();
uint8_t value = counter >= 0 
    ? counter % range
    : (counter + 1) % range + range - 1;
C

これ以上単純な計算式が思いつかなかったのでこうなりましたが、もっと洗練された方法をご存じの方がいらしたら教えていただけると嬉しいです。

Comments

No comments yet. Why don’t you start the discussion?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.