目次 [ Contents ]
part 1、part 2と、Arduinoでロータリエンコーダを使うためのスケッチを試行錯誤してきましたが、2つの記事で得た知恵を統合して、新たなスケッチを書いてみました。
名づけて「Dual Encoder」。
今回書いたスケッチで目指したのは、
- デュアルエンコーダ(複数のエンコーダ値を読み取る)
- 「外部割込み」ではなく「タイマー割り込み」で
- クリック有り・無し対応
です。
前回の問題点解決に試行錯誤した結果、精度が程よく改善されたかと思います。特にクリックなしタイプに対しては、自分的に満足出来るレベルになりました。
ただし、完全にチャタリングを排除できたわけではないし、状況によっては他のタイマーと競合してスケッチ全体のレスポンスに影響を与える可能性もあります。また、クリックタイプへの対応は4の倍数で強制しているだけなので、バタつきも出ます。
なので、クリックタイプ&外部割込みで事足りるのであれば、そっちを使ったほうが安定しているかな、と(part 1 参照)。まあ、一度試して判断してもらえれば…。
本来ならライブラリにしたほうが便利なんでしょうけど、技量・時間不足なのでスケッチファイルです。コードを直接書き換えて使ってください。悪しからず。
回路図
このスケッチを試すのに必要なパーツは、
- ArduinoUNO
- ロータリーエンコーダ×2
- (手元にあれば)128*64 OLED
ロータリーエンコーダは、2相とGNDの3ピンになっている安いタイプで大丈夫です。可能ならpart 2でやっていた「クリックなし」を用意できると、細かいエンコーダ操作が出来るので面白いか、と。
OLEDとu8glibの設定部分はこちらを参考にしてください。
あるいは、今回のスケッチはシリアルプリントにも返しているので、OLEDは用意しなくてもシリアルモニタでエンコーダ値を確認できます。
スケッチ
プロジェクトファイルをダウンロード、又は下記のコードをコピペするかして、Arduinoに書き込んでください。
Download – 「Dual encoder ver 1.0」
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
// jumbleat.com // // // // dual encoder version 1.0 // // by jumbler // // 2017.10.20 // #include <MsTimer2.h> #define TIM MsTimer2 #define OLED_DRAW 1 #if(OLED_DRAW) #include <U8glib.h> U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE | U8G_I2C_OPT_DEV_0); // OLED / I2C / TWI // SDA 4/ SDL 5 #endif #define ENC_NUM 2 #define ENC_TOLERANCE 25 #define TIMER_INTVAL 2 #define ENC_REPEAT ENC_TOLERANCE * (TIMER_INTVAL / 2) #define ENCA_1 4 #define ENCA_2 12 #define ENCB_1 7 #define ENCB_2 8 byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2, ENCB_1, ENCB_2}; #define ECUR B00000011 // current enc position #define EHOM B00001100 // previous enc position #define ECHG B01000000 // flag that encoder has changed #define ERES B10000000 // encoder resolution. '1' is for quarter resolution volatile int enc_count[ENC_NUM]; volatile int enc_old[ENC_NUM]; volatile byte enc_status[ENC_NUM]; byte ELAYER(byte layer) { for (byte i = 0 ; i < 8 ; i++) if ((layer >> i) & B00000001) return i; } byte EMASK(byte val, byte layer) { byte tmp = (val & layer) >> ELAYER(layer); return tmp; } bool ENC_CHECK() { bool yes_no = false; for (byte i = 0 ; i < ENC_NUM ; i++) yes_no |= ENC_CHECK(i); return yes_no; } bool ENC_CHECK(byte pin) { bool yes_no = enc_status[pin] & ECHG; return yes_no; } void SET_ENC_RES(byte pin, bool res) { if (pin < ENC_NUM) { enc_status[pin] = (enc_status[pin] & ~ERES) | (res << ELAYER(ERES)); enc_status[pin] &= ~ECHG; } } void ENC_RESET() { for (byte i = 0 ; i < 5 ; i++) ENC_READ(); for (byte i = 0 ; i < (ENC_NUM) ; i++) { enc_status[i] |= ECHG; // kick flag enc_count[i] = 0; ENC_COUNT(i); enc_status[i] |= ECHG; // kick flag } } int ENC_COUNT(byte pin) { int enc_val = 0; char vec = (enc_status[pin] & ERES) ? 4 : 1; if (enc_status[pin] & ECHG) { enc_val = (enc_count[pin] - enc_old[pin]) / vec; enc_old[pin] = enc_count[pin]; //update as previous value enc_status[pin] &= ~ECHG; // reset counting flags } return enc_val; } void ENC_READ() { static byte enc_gauge[ENC_NUM]; // read pins value for (byte ii = 0 ; ii < ENC_REPEAT ; ii++) { short pin_val[ENC_NUM]; // get current pins status of Encoder1 pin_val[0] = ((PIND & _BV(4)) ? 1 : 0) << 1; // ENC A-1 pin_val[0] |= ((PINB & _BV(4)) ? 1 : 0); // ENC A-2 #if(ENC_NUM > 1) // get current pins status of Encoder2 pin_val[1] = ((PIND & _BV(7)) ? 1 : 0) << 1; // ENC B-1 pin_val[1] |= ((PINB & _BV(0)) ? 1 : 0); // ENC B-2 #endif // for each encoder pins for (byte i = 0 ; i < ENC_NUM ; i++) { // modify order of pins value if (pin_val[i] < 2) pin_val[i] = 1 + (pin_val[i] * -1); enc_status[i] = (enc_status[i] & ~ECUR) + pin_val[i]; short pos_old = EMASK(enc_status[i], EHOM); if (pin_val[i] != pos_old) { //gauging enc_gauge[i] = min(ENC_TOLERANCE + 1, enc_gauge[i]++); //counting if (enc_gauge[i] >= ENC_TOLERANCE) { // increase or decrease ? bool dir = (pin_val[i] > pos_old) ? 1 : 0; if (pin_val[i] == 0 && pos_old == 3) dir = 1; else if (pin_val[i] == 3 && pos_old == 0) dir = 0; // add count by the direction if (dir) enc_count[i]++; else enc_count[i]--; boolean change_flag = false; // forced count correction for Click-type if (enc_status[i] & ERES) { if (pin_val[i] == 3) { char rem = enc_count[i] % 4; if (rem != 0) { enc_count[i] = (enc_count[i] / 4) * 4; if (abs(rem) > 2) { char vec = (enc_count[i] < 0) ? -1 : 1; enc_count[i] += 4 * vec; } } change_flag = true; } } else { change_flag = true; } if (enc_count[i] == enc_old[i]) change_flag = false; // update current pos to home pos enc_status[i] = (enc_status[i] & ~EHOM) | (pin_val[i] << ELAYER(EHOM)); // set count change flag enc_status[i] |= change_flag << ELAYER(ECHG); enc_gauge[i] = 0; } } else { enc_gauge[i] = max(0, enc_gauge[i]--); } } } } void setup() { for (byte i = 0 ; i < (ENC_NUM * 2) ; i++) pinMode(enc_pins[i], INPUT_PULLUP); SET_ENC_RES(0, 1); // SET_ENC_RES(encoder number, click = 1 / non_click = 0) SET_ENC_RES(1, 1); // SET_ENC_RES(encoder number, click = 1 / non_click = 0) TIM::set(TIMER_INTVAL, ENC_READ); TIM::start(); Serial.begin(38400); char title[2][21] = {"Dual Encoder ver 1.0", "by jumbleat.com"}; Serial.println(title[0]); Serial.println(title[1]); #if(OLED_DRAW) u8g.setColorIndex(1); // OLED pixel on u8g.setFont(u8g_font_orgv01r); //719 Byte / 11x11 /* u8g.firstPage(); do { u8g.setPrintPos(15, 20); u8g.print(title[0]); u8g.setPrintPos(50, 55); u8g.print(title[1]); } while (u8g.nextPage()); #endif delay(3000); ENC_RESET(); } void loop() { static int val[ENC_NUM]; if (ENC_CHECK()) { for (byte i = 0 ; i < ENC_NUM ; i++) { val[i] += ENC_COUNT(i); Serial.print((char)('A' + i)); Serial.print(":"); Serial.print(val[i]); Serial.print(" "); } Serial.println(); #if(OLED_DRAW) // --- OLED Draw int val_draw[ENC_NUM]; for (byte i = 0 ; i < ENC_NUM ; i++) val_draw[i] = constrain(val[i], -64, 64); u8g.firstPage(); do { for (byte i = 0 ; i < ENC_NUM ; i++) { byte space = 16; u8g.drawFrame(0, i * space, 127, 4); u8g.drawBox(min(64, 64 + val_draw[i]), i * space, abs(val_draw[i]), 4); u8g.setPrintPos(0, 10 + i * space); u8g.print(val[i]); } } while (u8g.nextPage()); // --- OLED Draw end #endif } } |
注意点
タイマー割り込み
このエンコーダ読み取りスケッチは「MsTimer2」というタイマー割り込み用のライブラリを使っています。(part 2を参考に)IDEへ組み込んでください。
もし、他のタイマーを使いたい場合、スケッチ内を書き換えれば対応できるとは思います。
9 10 11 12 |
#include <MsTimer2.h> #define TIM MsTimer2 #define OLED_DRAW 1 |
185 186 187 188 189 190 191 192 |
void setup() { for (byte i = 0 ; i < (ENC_NUM * 2) ; i++) pinMode(enc_pins[i], INPUT_PULLUP); SET_ENC_RES(0, 1); // SET_ENC_RES(encoder number, click = 1 / non_click = 0) SET_ENC_RES(1, 1); // SET_ENC_RES(encoder number, click = 1 / non_click = 0) TIM::set(TIMER_INTVAL, ENC_READ); TIM::start(); |
ポート操作
今回のスケッチはdigitalRead関数ではなく、直接ポートを読みに行くようにしました。この方法だと割り込みに割く時間を節約できるからです。
なので、エンコーダに接続しているピン設定は基本変更出来ないと考えてください。また、ArduinoUNO系統(ATmega328)以外のマイコン、つまり、ポート配置が違うタイプも、このままでは動かない場合があると思います。
ピンの設定を変更したい場合は、自分で調べて、ポート読み取りの部分を書き換えてください。
PIN# & _BV(#)の部分です。
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
void ENC_READ() { static byte enc_gauge[ENC_NUM]; // read pins value for (byte ii = 0 ; ii < ENC_REPEAT ; ii++) { short pin_val[ENC_NUM]; // get current pins status of Encoder1 pin_val[0] = ((PIND & _BV(4)) ? 1 : 0) << 1; // ENC A-1 pin_val[0] |= ((PINB & _BV(4)) ? 1 : 0); // ENC A-2 #if(ENC_NUM > 1) // get current pins status of Encoder2 pin_val[1] = ((PIND & _BV(7)) ? 1 : 0) << 1; // ENC B-1 pin_val[1] |= ((PINB & _BV(0)) ? 1 : 0); // ENC B-2 #endif |
ちなみに、「ポート読み取り」にリンクしてませんが、最初に#define定義しているピンはpinModeの設定用なので、同様に書き換える必要があります。要注意。
22 23 24 25 26 27 28 29 30 31 |
#define ENC_NUM 2 #define ENC_TOLERANCE 25 #define TIMER_INTVAL 2 #define ENC_REPEAT ENC_TOLERANCE * (TIMER_INTVAL / 2) #define ENCA_1 4 #define ENCA_2 12 #define ENCB_1 7 #define ENCB_2 8 byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2, ENCB_1, ENCB_2}; |
使い方
諸々の関数は下記に詳細を書いておきますが、基本はENC_COUNT()の返り値を変更したい変数に投げるだけです。タイマー開始後なら好きなところに呼び出せます。
スケッチ内の仕組みや構造はこの記事では触れないので、気になる方はpart 1 / part 2を参考にしてください。
もしモサモサ感が気になるなら、それはOLEDを使うu8glibに起因するものなので、define定義した「OLED_DRAW」を0にするか、u8glibに絡む行をコメントアウトして、シリアルモニタだけの確認にしてください。
9 10 11 12 13 14 15 16 17 18 19 20 |
#include <MsTimer2.h> #define TIM MsTimer2 #define OLED_DRAW 1 #if(OLED_DRAW) #include <U8glib.h> U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE | U8G_I2C_OPT_DEV_0); // OLED / I2C / TWI // SDA 4/ SDL 5 #endif |
Dual Encoderの関数
void SET_ENC_RES (byte encoder num, bool resolution);
使うロータリーエンコーダの分解能を設定します。返り値はありません。
クリックのある一般的なエンコーダは、4パターンを1クリック(1カウント)として扱うので、trueにすると、それに倣ったカウントをします。
encoder num | エンコーダ番号の指定。1個目は(0)、2個目は(1)になります。 |
resolution | 分解能の指定。 0(false) ノンクリックタイプの高分解能 1(true) クリックタイプ |
例えば、1個目のエンコーダをクリックタイプとして扱いたい場合は、
SET_ENC_RES(0, true);
デフォルトの使用はノンクリックタイプを想定しているので、クリックタイプのロータリーエンコーダを使わない場合は、この関数で設定する必要はありません。
boolean ENC_CHECK (); / ENC_CHECK (byte encoder num);
エンコーダの増減があったかをチェックしにいきます。変化があれば1(true)、なければ0(false)を返してきます。
encoder num | エンコーダ番号の指定。1個目は(0)、2個目は(1)になります。 |
ENC_CHECK()場合、全部のエンコーダの中から1つでも増減があればtrueを返します。
ENC_CHECK(#)の場合は、指定したエンコーダのみの増減有無を返します。
この返り値は、ENC_COUNT()関数を呼ぶことでリセットされます。よって、リセットしたくない時、この関数で「増減有無の確認」だけすることができます。
int ENC_COUNT (byte encoder num);
実際のエンコーダ値を取得。前回読んでから増減した+-差分を返り値として返します。
encoder num | エンコーダ番号の指定。1個目は(0)、2個目は(1)になります。 |
例えば、「val」という変数へ1個目のエンコーダ増減を足したいなら、
val += ENC_COUNT(0);
今回のスケッチではu8glibを組み込んでいますが、その中のdo~while分が全体のもたつきを作っていて、思うように更新されていないように見えます。ただ、(チャタリングによる取りこぼしがなければ)現実にロータリーエンコーダが回った分は拾っているハズなので、最終的な変更値はちゃんと得られると思います。
この関数を呼ぶことで、ENC_CHECK()の返り値はリセットされます。
void ENC_RESET()
エンコーダを読み取る上で、実際は「enc_count[]」という変数が裏で連続カウント値を記録しています。そのenc_count[]変数を0にリセットします。この変数はint型にしているので、よほどのことがなければオーバーフローしないと思いますが、使い方によってはその可能性もあるので、そういった危険がありそうな場合は、この関数を適宜組み込んでください。
参考 ) ArduinoUnoでint型が扱える数値範囲 -32768 ~ 32767
define定義値
ENC_NUM
ロータリーエンコーダの個数を指定をします。
1個しか使用しないのであれば、数を減らすことでタイマー割り込みに割く時間を節約できます。 ただし、その場合、ピン配列等も書き換えてください。
22 23 24 25 26 27 28 29 30 31 |
#define ENC_NUM 1 #define ENC_TOLERANCE 25 #define TIMER_INTVAL 2 #define ENC_REPEAT ENC_TOLERANCE * (TIMER_INTVAL / 2) #define ENCA_1 4 #define ENCA_2 12 //#define ENCB_1 7 //#define ENCB_2 8 byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2}; |
ENC_TOLERANCE
part 2に同じく、このスケッチは「ゲージ判定」でチャタリング対策しています。その閾値です。
この値はbyte型にしているので、255以上に設定しないでください。
TIMER_INTVAL
タイマー割り込みのインターバル時間(ms)です。
値を小さくするほど、正確な読み取りができるようになると思いますが、その分他のタスクを圧迫することになります。逆に、メインのタスクが上手く動かない場合に大きくすると、負荷が減るかと思います。ただし、だからと言って、大き過ぎてもエンコーダの読み取りが上手くいかなくなります。
ENC_REPEAT
1度のタイマー割り込みで1回のゲージ判定だと割り出しは難しいので、1回割り込んだら何回もピン状態を確認し、ゲージ分量を確保しています。つまり、1回の割り込み当たりに繰り返す「読み取り回数」の数値です。
数値を増やすことで、よりエンコーダの変化を細かく見れるようになりますが、その分他のタスクを圧迫することになります。
こちらも、255以上に設定しないでください。
ENC_TOLERANCE、TIMER_INTVAL、ENC_REPEAT、この3つの数値バランスでエンコーダの読み取り精度が変わってくるので、自分のプロジェクトに合わせ、上手く調整してください。
最後に
このスケッチは2つのエンコーダを同時に読めるよう目指したものですが、実は少し書き換えるだけで扱える個数を増やせます。
とりあえず、エンコーダ4個まで試しました。
ただし、割り込みに割く時間も増えるので、他のタスクに影響が出たり、取りこぼしが増えたりするかもしれません。特にPWMを扱うピンとのカブりが出てくると、憂慮することが増えるので厄介です。それでもよければ、と言う感じです。
例えば、3つ目のエンコーダをArduinoのA0、A1ピンに追加する場合、下記のように書き足します。
ピン定義
22 23 24 25 26 27 28 29 30 31 32 33 |
#define ENC_NUM 3 #define ENC_TOLERANCE 25 #define TIMER_INTVAL 2 #define ENC_REPEAT ENC_TOLERANCE * (TIMER_INTVAL / 2) #define ENCA_1 4 #define ENCA_2 12 #define ENCB_1 7 #define ENCB_2 8 #define ENCC_1 A0 #define ENCC_2 A1 byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2, ENCB_1, ENCB_2, ENCC_1, ENCC_2}; |
ポート操作
106 107 108 109 110 111 112 113 114 115 116 117 |
// get current pins status of Encoder1 pin_val[0] = ((PIND & _BV(4)) ? 1 : 0) << 1; // ENC A-1 pin_val[0] |= ((PINB & _BV(4)) ? 1 : 0); // ENC A-2 #if(ENC_NUM > 1) // get current pins status of Encoder2 pin_val[1] = ((PIND & _BV(7)) ? 1 : 0) << 1; // ENC B-1 pin_val[1] |= ((PINB & _BV(0)) ? 1 : 0); // ENC B-2 pin_val[2] = ((PINC & _BV(0)) ? 1 : 0) << 1; // ENC C-1 pin_val[2] |= ((PINC & _BV(1)) ? 1 : 0); // ENC C-2 #endif |
後は上述に倣い、
int val += ENC_COUNT(2);
というように値を取得していくだけです。
ロータリーエンコーダを複数扱うようなプロジェクトは中々ないかもしれませんが、1個だけでも、充分機能してくれると思うので、是非お役立てください。
参考リンク