google-site-verification: google3bd66dd162ef54c7.html
fc2ブログ

Arduinoでロータリーエンコーダーを使う

 現在気圧センサーのLPS25Hを使った高度計を製作中です。

 気圧高度計では海面気圧の値を入力してやる必要があります。もし海面気圧が判らなくても、その場所の標高が判っていれば表示がその値を示すように海面気圧の値を調整する方法もあります。ともかく気圧高度計は、何らかの方法で指示値を調整することが必要です。
 まあ、高度の誤差が±100mくらいあってもいいなら調整は不要ですが、そんなに精度の悪い高度計を欲しがる人は少ないと思います。

 ということで、製作中の高度計では可変抵抗とアナログポートを使って海面気圧の設定を行っています。これはとても簡単な方法ですが、一旦アナログ信号を経由しているので狙った値にぴったり合わせるには微妙な調整が必要です。

 こういう時にはロータリーエンコーダーを使うのが定石なので、こっちの方法に変更することにしました。

▼ロータリーエンコーダー
ロータリーエンコーダー
 手持ちのロータリーエンコーダーです。やっと出番が回ってきました。

▼Arduinoとの接続図
Arduinoへロータリーエンコーダーを接続
 ロータリーエンコーダーにはA相、B相二つの接点があるのでこんなふうに接続します。I/Oピンからの割り込みを使う場合には使用できるピンは制限されますが、今回はI/Oピン割り込みは使わないので余っているピンならどれでも使えます。

 ところで、調べてみるとロータリーエンコーダの状態を読むプログラムにはいろんな方式があります。

1.CPUでピンの状態を監視して回転を検出
 入門書によく書いてある判りやすい方法です。でもCPUが他の処理を同時にやらないといけない時はややこしいことになります。

2.ピンの状態の変化で割り込みを掛けて回転を検出
 たぶん一番スマートな方法だと思います。ただ接点のチャッタリングがあると多重に割り込みが入る危険があるので、裏で複雑な処理をやっている場合はちょっと不安な気がします。

3.タイマー割り込みで回転を検出
 定期的にロータリーエンコーダーの状態を見に行って回転を検出する方式です。割り込みは入りますが、多重割り込みになる心配はありません。ただArduinoではタイマー割込みの使い方にに制限がある場合があるので注意が必要です。

 ということで一長一短なのですが、今回は3項のタイマー割り込みでやってみます。

▼スケッチ
/*
ロータリーエンコーダーのテスト
ロータリーエンコーダーで値を設定し、シリアルモニタへ表示
2015/3/29 ラジオペンチ、http://radiopench.blog96.fc2.com/
 */
#include <MsTimer2.h>  
volatile int X;

void setup(void) {
  Serial.begin(9600);
  pinMode(2, INPUT);                // ロータリーエンコーダーA
  digitalWrite(2, HIGH);            // プルアップ
  pinMode(3, INPUT);                // ロータリーエンコーダーB
  digitalWrite(3, HIGH);            // プルアップ

  MsTimer2::set(1, timerIRQ);       // ロータリーエンコーダ読取りのために割込み
  MsTimer2::start();
}

void loop(void) {
  Serial.println(X);
  delay(100);
}

void timerIRQ() {                   // MsTimer2割込み処理
  static byte bp = 0;               // ビットパターン記録バッファ

  bp = bp << 1;
  if (digitalRead(2) == HIGH) {     // A相の状態を
    bp |= 0x01;                     // bpの末尾に記録
  }
  bp = bp << 1;
  if (digitalRead(3) == HIGH) {     // B相の状態を
    bp |= 0x01;                     // bpの末尾に記録
  }

  bp = bp & 0x0F;                   // 下位4ビット残して上位を消す
  if (bp == 0b0111) {               // このビットパターンと一致していたら
//  if ((bp == 0b0111) | (bp == 0b1000)) {   // 場合によってはこちら、
  X ++;                           // データーをインクリメント
  }
  if (bp == 0b1011) {               // このビットパターンと一致していたら、
//  if ((bp == 0b1011) | (bp == 0b0100)) {    // 場合によってはこちら
    X --;                           // データーをデクリメント
  }
}
 ロータリーエンコーダーによっては1サイクルの中間にもディテントの停止位置がある物があります。この場合は39、43行を使って下さい。(39、43行のコメントを外し、38、42行をコメントアウト)
 判定に使うビットパターンが少し変だった(動かなくは無いけど、非対称で美しくなかった)ので修正しました。2015/3/31追記


21行目でXの値をシリアルに出力しているが、このXの値は割り込みルーチンの中で値が変化している。この変化はバイト単位で行われるため、Xを読み出すタイミングによっては不正確な値になっている場合がある(Xは16ビット変数)。この問題を回避するためには、割込み禁止(CLI) にした後でXの値を別の変数、例えばX2にコピーし、その後割り込みを許可(SEI)。シリアルへの出力はX2に対して行えばOK。
参考記事:ロータリーエンコーダーの2相パルスをピン変化割り込みで取り込む 2020/9/15追記

 MsTimer2で1ms周期の割り込みをかけてロータリーエンコーダーを監視。メインのプログラムは割り込みとは無関係の周期でループ(100ms)を回り、エンコーダーの値をシリアルに流し続けています。

 プログラムを起動してシリアルモニタを開くと、ロータリーエンコーダの値を表示します。初期値は0でマイナスの値にも調整できます。

 回転方向の検出にはif文を組み合わせてやる方法もありますが、今回は状態遷移パターンを照合して判定する方式でやってみました。ロータリーエンコーダーを触っていない場合はパターンにヒットしないので何も起こりません。エンコーダーがどちらかに回った境界では照合パターンにヒットするのでどちらに回ったかが判る仕掛けです。
 このあたりはあちこちの先人の方の記事を参考にさせていただきました。特にPIC AVR 工作室 ブログの「いまさらながらロータリーエンコーダー」の記事に書かれているパターン照合方式の解説が判りやすかったです。

 ちなみにこのスケッチで割込み処理を行っている時間(timerIRQの処理時間)を測定してみると、約17μsでした。Arduinoは遅いという印象がありますが結構高速に動いていました。

 ということでロータリーエンコーダーはちゃんと動くようになったので、気圧高度計を最終の仕上げ状態に持っていこうと思います。
関連記事

コメントの投稿

管理者にだけ表示を許可する

No title

前回は、ペルチェの方でコメントさせていただきました。
ご返信ありがとうございます。マイコン初心者で申し訳ありませんが
質問です。

実は、今エンコーダをつかって回転数を測定する装置を作っているのですが、電源OFFしても計った回転数を記憶させておきたいのでI2CのEEPROMを接続しています。そのときに、回転数をそのまま書きこみ用の関数のデータ(data)に入れられるのでしょうか?

回転数は、上記のように volatile int X;

と規定し、eepromの書き込みでは、

LOOP関数内{

i2cEEPROM_write(0x50, ADR, X)


サブ関数定義

void i2cEEPROM_write(int i2cADR, unsigned int eeADR, byte data){ 以下


コードについては、ラジオペンチ様のコードを利用させていただきました。質問がわかりにくくすいません。要は、サブ関数上byteと定義しているところに、無理やりintの変数を入れているところです。




re:はじめまして

心配されているように、呼び出し側と関数側の引数の型が一致していないと正常に動かないです。

EEPROMへの書き込みはbyte 単位なので、intの値を保するためには2バイト使って保存する必要があります。

例えば、関数側を、
void i2cEEPROM_write(int i2cADR, unsigned int eeADR, int data){
byte hi, lo;
hi = data >> 8;
lo = data & 0x0F;

などとやって、int を受け取ることが出来るようにしておいて、
上位8ビットと下位8ビットに分解し、それぞれの値をEEPROMに書くのが良いと思います。読み出すときはその逆で、

こういう問題は、環境設定でコンパイラの警告を、「全て」にしておくと、ワーニングが出て教えてくれると思います。(たぶん)

早速のご返信ありがとうございます。
すぐにやってみたいと思います。
まずは、お礼まで。

re2:はじめまして

間違ってました。下位8ビットだけ残すので、

×  lo = data & 0x0F;
〇  lo = data & 0x00FF;

でした。

あと、ちょっとやってみたのですが、引数の型の不一致は、コンパイラの警告を全てにしておいても指摘されないようでした。

No title

重ね重ね、痛み入ります。
やるつもりで、家の用事でできなくなりました。今日、トライしてみます。

当初自分でやったときに、型の不一致でもコンパイルエラーにならないので、いけるかと思いましたが、だめでした。なかなか、ここの部分は難しいようですね。

この装置は、趣味であるアマチュア無線のクランクアップタワー用高さの検出に使う予定なのですが、なかなかうまくできず苦労してました。

無線もさることですが、こういったマイコン含めた回路技術はおもしろそうですね。

No title

こんばんわ。

いつもお世話になります。
教えていただいたやり方でやってみたところ、EEPROMに書き込むことができました。ただ、値の意味合いがわからないので、たびたびすいませんが教えていただけないでしょうか?

書き込み範囲は、0000のみにしています。

EEPROM DUMP MAPでは、
      0   1   2   3   4・・・F

0000  08  08  AE  00  00  00

0010  00  00  00  ・・・・・・・・・ 00

0000番地の3番目は、書き込んでいないようです。
パルスの値からするとからすると1000を超えています。

書き込みするタイミングなどは、オシロを見ながらやるほうが
わかりやすいのでしょうね。


re3:はじめまして

すみません、ご質問の意味が正確に理解できないのですが、

先頭の2バイトに 08 08 と書かれていたが、どういう意味?
という話なら、16進で0x0808 なので10進では2056になります。なお、上位/下位は書いた順序に依存するので、プログラムを書いた人にしか判りません。

デバッグにはシリアルモニタにデータを送るようにプログラムを追加・修正するのが一般的です。オシロはそれでも原因が判らなかった時に使いますが、経験が無いと無理です。

あと、
void i2cEEPROM_write(int i2cADR, unsigned int eeADR, int data)
をどこから持ってきたんだろう?と思っていたのですが、これ、たぶん私の記事からですね、今頃気付きました。

ついでに、
ArduinoのCPUには 500バイトのEEPROMが付いているのでこれを使えば良いと思うのですが、使わないのは何か理由があるのでしょうか?
EEPROMの書き込み回数には10万回とかの制限がありますが、そのあたり考慮されていますか?

No title

お世話になります。すいません、やはり、コードがないとわかりませんよね。家に帰ったらコードをあげたいと思います。(ここに貼り付けもいいかな?)

このプログラムで、エンコーダを回転させることで、Xの値がシリアルモニターで1000いくつかを示したものです。

あと、外付けEEPROMについては、書き込み回数が外付けの方が長いという理由で選びました。100万回ということなので。

#include <MsTimer2.h> // タイマー割り込み使用
#include <Wire.h> //i2c eeprom 接続

volatile int X = 0; // エンコーダーの初期値
unsigned int startADR = 0x0000;
unsigned int endADR = 0x001;
unsigned int ADR;

void setup() {      // 初期化プログラム
Wire.begin();     //i2c eeprom 設定
Serial.begin(9600);

pinMode(2, INPUT_PULLUP); // ロータリーエンコーダーA相
pinMode(3, INPUT_PULLUP); //            B相

MsTimer2::set(5, timer2IRQ); // 割り込み周期設定
MsTimer2::start();
}

void loop(){     //メインプログラム

Serial.print(X);
for (ADR = startADR; ADR <= endADR; ADR++)

i2cEEPROM_write(0x50, ADR, X);
  delay(1000);

}

void i2cEEPROM_write(int i2cADR, unsigned int eeADR, int data){
byte hi,lo;
hi = data >> 8;
lo = data & 0xFF;
Wire.beginTransmission(i2cADR);
Wire.write(hi);
Wire.write(lo);
Wire.endTransmission();
delay(5);
}

void timer2IRQ() { // MsTimer2割込み処理(時間管理とロータリーエンコーダーの読み取り)
static byte bp = 0; // ロータリーエンコーダー状態記録バッファ
// 時間間隔測定用タイムスロット数カウンタ
bp = bp << 1; // バッファをずらして右端を開けておき、
if (digitalRead(2) == HIGH) { // A相の状態を
bp |= 0x01; // bpの末尾に記録
}
bp = bp << 1;
if (digitalRead(3) == HIGH) { // B相の状態を
bp |= 0x01; // bpの末尾に記録
}

bp = bp & 0x0F; // 下位4ビット残して上位を消す
// if (bp == 0b0111) { // このビットパターンと一致していたら
if ((bp == 0b0111) | (bp == 0b1000)) { // 物によってはこちらを使う
X ++; // データーをインクリメント
}
// if (bp == 0b1011) { // このビットパターンと一致していたら、
if ((bp == 0b1011) | (bp == 0b0100)) { // 物によってはこちら
X --; // データーをデクリメント
}
}


No title

すいません、家に帰らないとわからないかなと思いましたが、ファイルをアクセスできたので、コードをあげてしまいました。ご迷惑おかけします。

No title

私のやった作業は下記のとおりです。

1.このコードをUnoにコンパイルして書き込み、シリアルモニターをみながら、回転数X値を記憶する。シリアルモニターでの最終表示値は1000いくつか。

2.EEPROMの読み取りコード(ラジオペンチさんからDUMP表示するコードを引用させてもらいました)を新たにUnoに書き込み、シリアルモニターで、みたところ前述のようになっていたというところです。

       0   1   2    3    4
0000  08  08  AE   00   00  

0010  00  00  00   00   00  

エンコーダーのプログラムを書き込む前のEEPROMの最初の状態は、すべて00でした。
書き込みして、その後みると、08 08 AEだったのですが、シリアルでのXの値(1000?)が、この08 08 AEになったのかを知りたかったのですが、どうもそうではなさそうですね。


re4:はじめまして

ソース拝見しました。

i2cEEPROM_write 関数を修正されていますがその修正方法ではうまくいかないです。この関数で書けるのは、1バイトなのでintを保存したいなら2回呼ぶ必要があります。
なお、連続して何バイトか書く方法があったとは思いますが私はやったこと無いです。

ともかく、EEPROM内のアドレスの指定が無くなっているので、どういう動作になるか予測不能です。

あとEEPROMが100万回書き換え可能と言っても電源入れっぱなしで毎秒1回書いたら11日で制限超えちゃいます。いろんな対策がありますが、値が変化した時だけ書き込みを行う方法がが簡単なので、対策した方が良いと思います。

あと、ロータリーエンコーダーの読み飛ばしなどが発生すると誤差になるので要注意です。
loop の中の for 文の{ } を省略されていますが、間違いのも元なので入れた方がよろしいかと。私は省略するときは思いっきりコメントに書いてます。

以下は修正したコード案です。試す環境が無いので完全な動作確認まで出来てません。

#include <MsTimer2.h> // タイマー割り込み使用
#include <Wire.h> //i2c eeprom 接続

volatile int X = 0; // エンコーダーの初期値
int lastX = 0;
unsigned int startADR = 0x0000;
unsigned int endADR = 0x001;
unsigned int ADR;

void setup() {// 初期化プログラム
Wire.begin();//i2c eeprom 設定
Serial.begin(115200);

pinMode(2, INPUT_PULLUP); // ロータリーエンコーダーA相
pinMode(3, INPUT_PULLUP); //            B相

MsTimer2::set(5, timer2IRQ); // 割り込み周期設定
MsTimer2::start();
}

void loop() { //メインプログラム
if (X != lastX) { // Xが変化していた時だけ
i2cEEPROM_write(0x50, 0, (X >> 8)); // 上位保存
i2cEEPROM_write(0x50, 1, (X & 0x00FF)); // 下位保存
lastX = X; // 次回のチェック用に記録
}
delay(1000);
}
void i2cEEPROM_write(int i2cADR, unsigned int eeADR, byte data ) {
Wire.beginTransmission(i2cADR); // i2cアドレス指定
Wire.write((int)(eeADR >> 8)); // EEPROM内アドレス指定 MSB
Wire.write((int)(eeADR & 0xFF)); // LSB
Wire.write(data);
Wire.endTransmission();
delay(5); // 書き込み完了待ち
}

void timer2IRQ() { // MsTimer2割込み処理(時間管理とロータリーエンコーダーの読み取り)
static byte bp = 0; // ロータリーエンコーダー状態記録バッファ
// 時間間隔測定用タイムスロット数カウンタ
bp = bp << 1; // バッファをずらして右端を開けておき、
if (digitalRead(2) == HIGH) { // A相の状態を
bp |= 0x01; // bpの末尾に記録
}
bp = bp << 1;
if (digitalRead(3) == HIGH) { // B相の状態を
bp |= 0x01; // bpの末尾に記録
}

bp = bp & 0x0F; // 下位4ビット残して上位を消す
// if (bp == 0b0111) { // このビットパターンと一致していたら
if ((bp == 0b0111) | (bp == 0b1000)) { // 物によってはこちらを使う
X ++; // データーをインクリメント
}
// if (bp == 0b1011) { // このビットパターンと一致していたら、
if ((bp == 0b1011) | (bp == 0b0100)) { // 物によってはこちら
X --; // データーをデクリメント
}
}

No title

ありがとうございます。無事、Xの値をメモリに書き込むことができました。確かに、変化したときに記憶することのほうがよさそうですね。
エンコーダーの回転している時間は、最長で10分あるので、タイミングを1秒ではなく、10秒もしくは1分でもよいかと思っています。

Unoの電源を切ることがあるので、今度は、最後に保存した値の読出しを初期値として加算することを次に考えたいと思います。

重ね重ねありがとうございました。

カレンダー
05 | 2023/06 | 07
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 -
プロフィール

ラジオペンチ

Author:ラジオペンチ
電子工作を中心としたブログです。たまに近所(東京都稲城市)の話題など。60過ぎて視力や器用さの衰えを感じつつ日々挑戦!
コメントを入れる時にメールアドレスの記入は不要です。なお、非公開コメントは受け付けていません。
記事の内容のご利用は読者の自己責任でお願いします。

記事が気に入ったらクリックを!
最新記事
カテゴリ
最新コメント
リンク
FC2カウンター
検索フォーム
月別アーカイブ
RSSリンクの表示
QRコード
QRコード