2016年12月28日水曜日

Arduinoでロータリーエンコーダー

[追記]「この書き方だと、メインルーチンの方で値を読み込もうとしている間にインタラプトが入って値が桁上り時などにとんでもない値に変わってしまう可能性がある」とのご指摘をいただきました。

Z80アセンブラやCP/M上でBDS-Cなんていじってた当時はそういうのに気遣うのは当たり前だったんですが、すっかり高級言語で安逸な暮らしをしておりました……いかんですね。

記事はこちらです。

ご指摘感謝します[追記終わり:2022年3月11日]

写真:まさにバラック配線w

「ロータリエンコーダーなんて懐かしいブツのためにライブラリ使うこともないわなー」

…と考えていたこともありました。さがすより作った方が速いわw …なんて。

いえ、結局自分で作ったんですけどね…ちゃんと動くまで2時間ぐらいかかりました。

■原理■

あちこちで書かれているので簡単にまとめると、ロータリーエンコーダは位相の90度ずれた2つのスイッチからの信号を元にして、どっち向きにどのぐらい回転したかを検出するデバイスです。まぁ回転とは限らないのですが、ほとんど回転物で使われます。

2つのスイッチからの信号を2ビットのデータとみなした場合、正回転だと値 0▶1▶3▶2 と変化します。逆回転だと 2▶3▶1▶0 ですね。0123あるいは3210と変化してくれればまだ簡単なのですが、これは一種のグレイコードというもので状態の変化(この場合は回転)で一度に1ビットずつしか変わらないようにしているものです。1ビットしか変わらないので、チャタリングが起こっても値がどったんばったんすることがないというスグレモノ。

なので、今回はこれを読んだ値>順番値に変換します
int readEncoder() {
  const byte gray[] = {0, 1, 3, 2};
  return gray[(digitalRead(EncoderA) << 1) | digitalRead(EncoderB)];
}
これでめでたく0,1,2,3の値が返ってきます。

あとは、両方のピンに対して割り込みを指定し
void attachEncoder() {
  attachInterrupt(digitalPinToInterrupt(EncoderA), encoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(EncoderB), encoder, CHANGE);
}
割り込みを受け付けるルーチンではこんな感じに増分と繰り上がりを処理します。
void encoder() {
  detachEncoder();
  delay(10);

  prevGray = currentGray;
  currentGray = readEncoder();
  if (currentGray == prevGray + 1 || (currentGray == 0 && prevGray == 3)) {
    encoderValue++;
  }
  if (currentGray + 1 == prevGray || (currentGray == 3 && prevGray == 0)) {
    encoderValue--;
  }

  attachEncoder();
}
delayはチャタリング対策。普通は1mSecぐらいで良いんですが、粗悪品なのでこれでやっと安定しましたw ただ、delay大きすぎると応答が鈍くなるので高速回転するものには使えません。1秒100パルスを超えるようなものについてはフォトインタラプタを使うかコンデンサ+シュミットトリガでハード的なチャタリング対策をしましょう。

あと初期化と定義部分です。
const int EncoderA = 2;
const int EncoderB = 3;
volatile char prevGray;
volatile char currentGray;
volatile int encoderValue = 0;
void setupEncoder() {
  pinMode(EncoderA, INPUT_PULLUP);
  pinMode(EncoderB, INPUT_PULLUP);
  prevGray = currentGray = readEncoder();
  attachEncoder();
}


値はメインループで
int prevValue = -1;
void loop() {
 
  if (prevValue != encoderValue) {
    Serial.println(encoderValue);
    prevValue = encoderValue;
  }
  delay(100);
}
などと使えばOKです。あ、setupEncoderはsetupから呼び出してください。

[追記]冒頭の追記に関する部分ですが、この当時のこのブログの文字修飾方法が古すぎてソースを編集できないので加筆します。

void loop()と最初の行の間に
  noInterrupts();
  int copyValue = encoderValue;
  interrupts();
として、copyValueを使うようにしてください。値をコピーする間インタラプトを止めて、コピー後にインタラプト再開しています。こうすればご指摘いただいたような現象は回避できます。また、ARMやESP32などでは4バイトのうちの一部が変わって、というような現象は起きないですが、後続処理中に値が変わったら困る場合には上記のような処置が必要です。

しかし5年前の記事か……最近はインタラプト使わずノンプリエンプティブマルチタスクのFreeRTOSで安直な人生を送っております(遠い目)[追記終わり2022/03/11]

いやー…実は機械式ロータリーエンコーダを扱うのはこれが生まれて初めてでした。IoTとかで上流ばっかり見てないで、たまには足元のあれこれもいじらないといかんですね。

なお、試作なのでジャンク使っていますが、納品用にはちゃんとアルプス製を使いますからご安心ください>お客様

ああ…ロータリーエンコーダーの操作の通りにスムーズに回転/逆転するモーター。気持ちがいい…w

余談ですが写真で赤く光っているフジソク 照光式押ボタンスイッチ CLP44-L2-2はすごく良いですね。無接点(赤外線インタラプタ)なので安心して使えて120円。昔こういうスイッチって1000円ぐらいしたんだよなぁ…と遠い目をしておしまい。

秋月のサイトから

■(久々の)ハマりポイント■

  • え!Arduinoって全ピンでInterrupt使えないの?
    配線して動かしても全然反応がありません。他のプラットフォーム同様、attachすれば全部イケるのかと思ってたんですが…Arduino nano / unoの場合にはD2, D3しかインタラプトに使えないんですね…はっはっはっはっは。

  • Encoderの仕様書がなくてピンを間違う
    aitendoで買ったほぼジャンクのエンコーダで試しています。このタイプのエンコーダには3本の端子がA-GND-Bと並んでいるものと、A-B-GNDのものがあります。今回、事前にテスターで調べてA-B-GNDだと判断したのですが、しばらく動かしてどうもおかしい。ネット上に上がっているプログラムでも正しく動かない。改めてテスターで調べたら前者ではなく後者だったという落ち。
  • prevの処理
    前回読み込んだ値と、今回読み込んだ値を比較するために、今回の値をprevGrayに保存しておくべきなのですが。忘れてました。何その致命的ボケorz

  • やけにノイズが規則的だなある程度動くようになってから、シャフトのクリック感に合わせて動かすと4,8,12,16....16,12,8,4と4の倍数ずつ増減します。たまに数字が飛ぶので、チャタリング豪快だなwと思ってしまったんですが…単にクリック間に4パルス出てた、という落ちでした。普通そう考えるだろ…。
ああ恥ずかしい…。

2 件のコメント:

  1. 割り込み処理のミスについてあれこれと・・・ご一読いただければ。
    http://igarage.cocolog-nifty.com/blog/2022/03/post-fbb7ad.html

    返信削除
  2. ご指摘ありがとうございます!

    まさにご指摘のとおりです。Z80のころにはクリチカルなところはdi / eiで囲むのが当たり前ことだったんですが、すっっっかり失念しておりました。いかんですね……。

    返信削除

注: コメントを投稿できるのは、このブログのメンバーだけです。