ピンチェンジ割込みを使ってロータリーエンコーダーを読む (Arduino)
少し前の記事でウォッチドッグタイマー割込みを使ってロータリーエンコーダーの読取りをやってみました。これは、ウォッチドッグタイマーの使い方を試す事例としては悪くないと思います。でも、割り込み周期を15msまでしか短く出来ないので、ロータリーエンコーダーをすばやく廻した時に取りこぼしが出てしまうという問題がありました。
タイマー割込みを使ってロータリーエンコーダーの読取りを行えばこんなことにはならないのですが、タイマーの割り込みが余っていない場合はそうもいきません。
ということで困っていたのですが、ピンチェンジ割り込み (Pin Change Interrupt) を使えば何とかなりそうなことに気付きました。
ピンチェンジ割り込みはCPUの機能として存在しているのですが、Arduinoの言語仕様では定義されていません。ということで、これを使うためには、自分で制御レジスタを設定する必要があります。なお、この割り込みはArduinoの外部割込み(INT0, INT1)とは別の割込みになります。
CPU(ATmega328P)のデーターシートを読むと、ピンチェンジ割込みの詳しい使い方が書かれています。でも、物理ピン名や割込み信号名の関係がややこしいので、Arduinoのピン番号順に一覧表としてこれらを整理したのが次の表です。
▼ピンチェンジ割込み関係ピン、信号対応表

今回読もうとしているロータリーエンコーダーは、この表の黄色で示すD9とD10に接続されています。その割込み信号名は PCINT1と PCINT2 で、PCMSK0 レジスタで指定するということが判ります。
ピンチェンジ割り込みでは、まとまったグループ単位でどこかのピンの状態に変化があった時に割り込みが掛かります。実際にどのピンが変化したのかを特定するためには、プログラムで調べる必要があります。でもロータリーエンコーダーの読取りを行う場合、「二つのピンのどちらかに状態の変化があった」ということが判れば大丈夫なので処理はかなり簡単になります。
ということで、実際に動作確認するためのプログラムを書いてみました。
▼ピンチェンジ割り込みでロータリーエンコーダーを読むスケッチ
参考記事:ロータリーエンコーダーの2相パルスをピン変化割り込みで取り込む
追記:2020/09/15
19、20行目がピンチェンジ割込みを起動するためのレジスタ設定です。ここで設定しておけば、割り込みが掛かるたびに割込み処理ルーチンである32行目の ISR(PCINT0_vect) が実行されます。
割込み処理ルーチンではパターン一致方式でロータリーエンコーダーの読取りを行っていますが、ちょっとひねりを加えて、素早く廻した時は早送りでカウントが進むようなルーチンを追加してあります。
実行するとシリアルモニタに状態を表示します。
▼出力例

エンコーダーのカウント値、割込み発生回数、経過時間(μs)の順に表示します。10カウントの時点ですばやく廻したのでカウントが10ステップ間隔で増えています。
割込み回数は理論的には4になるはずですが、チャッタリングの影響で多少変化しています。なお、スイッチのチャッタリング消しをプログラムで行いたかったのですが、うまい手を思い付かなかったので、回路にチャッタ消しのコンデンサを入れています(下図のC1,C2)。コンデンサの容量は割込み回数を見ながら調整すればいいのですが、今回はそこまではやっていません。
▼回路図

ということで、ピンチェンジ割り込みを使ったロータリーエンコーダー読み取りはうまくいったようです。このテクニックはロータリーエンコーダーに限らずArduinoでより複雑な物を作りたい場合に役立つと思います。
この記事の作成には以下のサイトを参考にさせていただきました。ありがとうございます。
・FIRMLOGICSさん、Arduino のスリープと、Pin Change 割込
・wsnakさん、LEDシーリングライト用赤外線リモコンの製作(9)アイリスオーヤマ用 スリープ対応
・橋本商会さん、ATmega168でピン変化割り込み
タイマー割込みを使ってロータリーエンコーダーの読取りを行えばこんなことにはならないのですが、タイマーの割り込みが余っていない場合はそうもいきません。
ということで困っていたのですが、ピンチェンジ割り込み (Pin Change Interrupt) を使えば何とかなりそうなことに気付きました。
ピンチェンジ割り込みはCPUの機能として存在しているのですが、Arduinoの言語仕様では定義されていません。ということで、これを使うためには、自分で制御レジスタを設定する必要があります。なお、この割り込みはArduinoの外部割込み(INT0, INT1)とは別の割込みになります。
CPU(ATmega328P)のデーターシートを読むと、ピンチェンジ割込みの詳しい使い方が書かれています。でも、物理ピン名や割込み信号名の関係がややこしいので、Arduinoのピン番号順に一覧表としてこれらを整理したのが次の表です。
▼ピンチェンジ割込み関係ピン、信号対応表

今回読もうとしているロータリーエンコーダーは、この表の黄色で示すD9とD10に接続されています。その割込み信号名は PCINT1と PCINT2 で、PCMSK0 レジスタで指定するということが判ります。
ピンチェンジ割り込みでは、まとまったグループ単位でどこかのピンの状態に変化があった時に割り込みが掛かります。実際にどのピンが変化したのかを特定するためには、プログラムで調べる必要があります。でもロータリーエンコーダーの読取りを行う場合、「二つのピンのどちらかに状態の変化があった」ということが判れば大丈夫なので処理はかなり簡単になります。
ということで、実際に動作確認するためのプログラムを書いてみました。
▼ピンチェンジ割り込みでロータリーエンコーダーを読むスケッチ
/* ピンチェンジ割込みを使ってロータリーエンコーダーを読む注:割込み処理ルーチンで値が更新されるグローバル変数 (X, nIRQ, lastMicros) は読み出すタイミングによっては不正確な値になっている場合がある(8ビット単位で更新されるため)。この問題を回避するためには割り込みを停止した状態で別変数にコピーするなどのアトミック操作を行うように修正したほうが良い。
* A相 = D9, PB1, PCINT1, PCIE0
* B相 = D10, PB2, PCINT2, PCIE0
* 2016/4/8 ラジオペンチ http://radiopench.blog96.fc2.com/
*/
#define ECA 9 // エンコーダーA相 = D9
#define ECB 10 // B相 = D10
volatile int X, nIRQ;
volatile unsigned long lastMicros;
int data;
void setup() {
pinMode(ECA, INPUT_PULLUP);
pinMode(ECB, INPUT_PULLUP);
Serial.begin(9600);
Serial.println("start");
PCMSK0 |= ((1 << PCINT1) | (1 << PCINT2)); // D9,D10ピンからのピンチェンジ割込みを使う
PCICR |= (1 << PCIE0); // PCIE0グループからの割込み許可
}
void loop() {
if (X != 0) { // エンコーダーの値が変化していたら
data += X;
X = 0;
Serial.print(data); Serial.print(", "); Serial.print(nIRQ); // 動作確認表示
Serial.print(", "); Serial.println(lastMicros);
}
}
ISR(PCINT0_vect) { // PCIINT0グループからの割込み処理
static byte bp = 0; // ビットパターン記録バッファ
static int n = 0;
n++; // 割込み発生回数カウント
bp = bp << 1; // ピンの状態変化をbpに右詰めで記録 0b00ABABAB
if (digitalRead(ECA) == HIGH) bp |= 0x01;
bp = bp << 1;
if (digitalRead(ECB) == HIGH) bp |= 0x01;
bp &= 0x0F; // 下位4ビット残して上位を消す 0b0000ABAB
if (bp == 0b0111) { // このパターンに
X++; // 一致していればインクリメント
nIRQ = n; // 割込み回数を記録し
n = 0; // カウンタをリセット
}
if (bp == 0b1011) {
X--; // 一致していればデクリメント
nIRQ = n;
n = 0;
}
if (n == 0) { // 値の変化があって
if ((micros() - lastMicros) < 50000) { // 50ms以内の更新だったら
X *= 10; // 早送り(10倍速)
}
lastMicros = micros();
}
}
参考記事:ロータリーエンコーダーの2相パルスをピン変化割り込みで取り込む
追記:2020/09/15
19、20行目がピンチェンジ割込みを起動するためのレジスタ設定です。ここで設定しておけば、割り込みが掛かるたびに割込み処理ルーチンである32行目の ISR(PCINT0_vect) が実行されます。
割込み処理ルーチンではパターン一致方式でロータリーエンコーダーの読取りを行っていますが、ちょっとひねりを加えて、素早く廻した時は早送りでカウントが進むようなルーチンを追加してあります。
実行するとシリアルモニタに状態を表示します。
▼出力例

エンコーダーのカウント値、割込み発生回数、経過時間(μs)の順に表示します。10カウントの時点ですばやく廻したのでカウントが10ステップ間隔で増えています。
割込み回数は理論的には4になるはずですが、チャッタリングの影響で多少変化しています。なお、スイッチのチャッタリング消しをプログラムで行いたかったのですが、うまい手を思い付かなかったので、回路にチャッタ消しのコンデンサを入れています(下図のC1,C2)。コンデンサの容量は割込み回数を見ながら調整すればいいのですが、今回はそこまではやっていません。
▼回路図

ということで、ピンチェンジ割り込みを使ったロータリーエンコーダー読み取りはうまくいったようです。このテクニックはロータリーエンコーダーに限らずArduinoでより複雑な物を作りたい場合に役立つと思います。
この記事の作成には以下のサイトを参考にさせていただきました。ありがとうございます。
・FIRMLOGICSさん、Arduino のスリープと、Pin Change 割込
・wsnakさん、LEDシーリングライト用赤外線リモコンの製作(9)アイリスオーヤマ用 スリープ対応
・橋本商会さん、ATmega168でピン変化割り込み
- 関連記事
-
- ArduinoでI2Cキャラクタ液晶を使う(その2) ライブラリの選定
- ArduinoでI2Cキャラクタ液晶を使う(その1) 基本動作確認
- ピンチェンジ割込みを使ってロータリーエンコーダーを読む (Arduino)
- ウォッチドッグタイマー割込みを使ってロータリーエンコーダーを読む
- FlexiTimer2の挙動の調査(Arduino)