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

ピンチェンジ割込みを使ってロータリーエンコーダーを読む (Arduino)

 少し前の記事でウォッチドッグタイマー割込みを使ってロータリーエンコーダーの読取りをやってみました。これは、ウォッチドッグタイマーの使い方を試す事例としては悪くないと思います。でも、割り込み周期を15msまでしか短く出来ないので、ロータリーエンコーダーをすばやく廻した時に取りこぼしが出てしまうという問題がありました。

 タイマー割込みを使ってロータリーエンコーダーの読取りを行えばこんなことにはならないのですが、タイマーの割り込みが余っていない場合はそうもいきません。

 ということで困っていたのですが、ピンチェンジ割り込み (Pin Change Interrupt) を使えば何とかなりそうなことに気付きました。

 ピンチェンジ割り込みはCPUの機能として存在しているのですが、Arduinoの言語仕様では定義されていません。ということで、これを使うためには、自分で制御レジスタを設定する必要があります。なお、この割り込みはArduinoの外部割込み(INT0, INT1)とは別の割込みになります。

 CPU(ATmega328P)のデーターシートを読むと、ピンチェンジ割込みの詳しい使い方が書かれています。でも、物理ピン名や割込み信号名の関係がややこしいので、Arduinoのピン番号順に一覧表としてこれらを整理したのが次の表です。

▼ピンチェンジ割込み関係ピン、信号対応表
ピンチェンジ割込み関係信号対応表
 今回読もうとしているロータリーエンコーダーは、この表の黄色で示すD9とD10に接続されています。その割込み信号名は PCINT1と PCINT2 で、PCMSK0 レジスタで指定するということが判ります。

 ピンチェンジ割り込みでは、まとまったグループ単位でどこかのピンの状態に変化があった時に割り込みが掛かります。実際にどのピンが変化したのかを特定するためには、プログラムで調べる必要があります。でもロータリーエンコーダーの読取りを行う場合、「二つのピンのどちらかに状態の変化があった」ということが判れば大丈夫なので処理はかなり簡単になります。

 ということで、実際に動作確認するためのプログラムを書いてみました。

▼ピンチェンジ割り込みでロータリーエンコーダーを読むスケッチ
/* ピンチェンジ割込みを使ってロータリーエンコーダーを読む
* 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();
}
}
注:割込み処理ルーチンで値が更新されるグローバル変数 (X, nIRQ, lastMicros) は読み出すタイミングによっては不正確な値になっている場合がある(8ビット単位で更新されるため)。この問題を回避するためには割り込みを停止した状態で別変数にコピーするなどのアトミック操作を行うように修正したほうが良い。
参考記事:ロータリーエンコーダーの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でピン変化割り込み
関連記事

コメントの投稿

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

割り込みとの競合 めったにおこらないだろけれど

「ロータリーエンコーダーの2相パルスをピン変化割り込みで取り込む」
http://igarage.cocolog-nifty.com/blog/2020/09/post-d2d7dd.html
というタイトルのブログ記事の中で、ラジオペンチさんの
「ピンチェンジ割込みを使ってロータリーエンコーダーを読む (Arduino)」
のスケッチの一部(3行だけだけど)を使わせていただきました。
ただ・・・「これはあかん見本」で・・・

re:割り込みとの競合 めったにおこらないだろけれど

なるほど、割り込み処理ルーチンによって値が変化する変数はアトミックにアクセスしないといけない、ということですね。
これは気付きませんでした、確かに「これはあかん」です。

あとで直しておきます、ご指摘ありがとうございます。

これで問題が起きたら原因調査が極めて難しいバグになるんでしょうね。

No title

そうなんです。
Arduinoを使ってロータリーエンコーダからのパルスを処理するサンプルプログラムをあれこれ見ていたら・・・・のきなみアウトやん・・・だったんです。
8bitカウンタにしていれば問題なしなんですが、みなさんのカウントデータはintやlong.。

エンコーダ処理での「アウト」、他の方は多バイトデータのアクセスの問題でしたが、ラジオペンチさんのサンプルは、ちょいと珍しいパターンでした。
この場合、「X」が1バイトデータでもアウトになりますんで。

逆回転と誤検知してしまう

タイマ割り込みでもポーリングでもない、ピン変化割り込みによる実装例として大変参考になっています。
ただ、私の手元の安価なロータリーエンコーダでは、一定方向に回し続けた場合でも、10数回に一度、逆方向回転が検出されることがありました。チャタリング防止コンデンサは入れています。

そこでソフト的に、同じ方向に回転し続けていることを履歴上もう一段過去からみるようにしたところ、上記現象は解消しました。

ISR(PCINT0_vect) {
:
static byte lastbp = 0; // 1回前のbpの値
:
lastbp = bp;
:
if (bp == 0b0111 && lastbp == 0b0001) .. // 0001->0111と変化してきた場合

if (bp == 0b1011 && lastbp == 0b0010) .. // 0010->1011と変化してきた場合
:
}

re:逆回転と誤検知してしまう

なるほどです。記事を読んでいただいてありがとうございます。

bp &= 0x0F; // 下位4ビット残して上位を消す
とやってるのを、
bp &= 0x3F; // 下位6ビット残して上位を消す
とやっておいて、

if (bp == 0b000111) .. // このパターンが出現したら
とやっても良い気がしますが、デバッグ出来る環境が無いので確認が出来ません。

割り込みのミスを検証

割り込みとの競合、まとめてみました。
http://igarage.cocolog-nifty.com/blog/2022/03/post-f884ac.html

re:割り込みのミスを検証

記事拝見しました、すごい検証実験ですね。
sei(); と cli(); をちゃんと使わないとダメ!と言う話ですね。

あと、気になるのが割り込みを検知する最小パルス幅です。イベントはクロックの周期でラッチされる、つまりクロックが16MHzならパルス幅が62.5ns以上あれば割り込みがかかる、と考えれば良さそうですよね。たぶんデーターシートのどこかに書いてありそうなので、時間のある時に探してみます。

テスト用パルスを可変に

テスト用パルス出力をPWMにして、周期を可変できるようにしてみました。
http://igarage.cocolog-nifty.com/blog/2022/03/post-f706d8.html

INT0入力、最小クロック幅でも割り込みがかかりましたよ。

re:テスト用パルスを可変に

すごい確認をやりましたね。

マイコンなのに65nsのパルスを見逃さないってのは凄いですね。
カレンダー
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コード