Arduino のスリープと、Pin Change 割込
さて。元気を出して、ソフトウェア最後の設計です。
Atmel ATmega マイコン*1には、通常の外部ピン割込の他に、Pin Change Interrupt というものが用意されています。例えば ATmega 328P では、通常の外部ピン割込は 2本しかないのですが、Pin Change 割込を使えば、他の 23本の信号線も割込入力に使えるという優れものの機能なのです。しかしなんと、
Arduino の標準ライブラリでは、Pin Change 割込をサポートしていない! (ぽい)
です。そんな訳でいきなり Atmel のデータマニュアルをひもとき、各種ウェブサイトで先達の情報を参考にさせて頂くはめに。。。
はっきり言って、綺麗にまとまった、かつビギナーにも分かりやすい説明は見つからなかったです、ハイ。(← 自信過剰) ライブラリもどきを載せるなら、もうちょっと汎用的にして欲しいところです。(← やや傲慢)
というわけで、割込プログラミング歴 30年、サポート歴 10年の私が簡単かつ、いい加減に説明してみましょー。
標準のライブラリを手直ししても使えない
残念ながら、標準ライブラリにある attachInterrupt() とかはあまり汎用的に作られていないので、それをいじって Pin Change 割込に流用するのは困難です。もっとも、あまり汎用的に作られたライブラリは、読むのも大変なので、私はあまり好きでないですが。
まず最初に、AVR マイコンの割込ですが、マニュアルを読んだところ、以下のようなベクタ構成になっています。
1: RESET 2: INT0 3: INT1 4: PCINT0 (後述するところの PCI0) 5: PCINT1 (同 PCI1) 6: PCINT2 (同 PCI2) 7: WDT (以下略)
これを見ると、attachInterrupt() の最初の引数にベクタ番号として PCINT0 とか指定できれば簡単そうに見えますが、そうはなっていません。残念。
という訳で、自力で割込をセットアップします。
Pin Change 割込の概要
Pin Change 割込は、現在の AVR マイコンのアーキテクチャでは 24本(割込ピン)まで扱えるようになっていて、これが割込ソース PCINT0, 1, 2 というふうに、8本ずつ 3ベクタにマルチプレクスされて(束ねられて)います。この名前はちょっと混乱がありまして、Pin Change 割込に使うピンの名前が PCINT0〜23 なのにも関わらず、割込ソース名も PCINT0〜2 というのはヒジョーに分かりにくいです。 > Atmel さん
ここでは、ピンのほうは PCINT と呼び、割込ソースのほうは PCI0〜PCI2 と呼ぶことにしましょう。
まとめますと、割込ソース(ベクタに一対一対応)と割込ピンは、次のように対応しています。
割込ソース: 割込ピン
PCI0: PCINT0〜7
PCI1: PCINT8〜15
PCI2: PCINT16〜23 (← PCINT20 は、この中にある)
どの物理ピンがどの PCINTn に割り当てられているかは、Atmel のデータマニュアル "Atmel 8-bit Microcontroller with 4/8/16/32KBytes In-System Programmable Flash" の、Section 1. Pin Configurations というところに載っています。今回、私が使いたい PD4 は別名 PCINT20 なので、PCI2 にマップされている訳ですね。
あ、一つ大事なことを忘れてました。Pin Change 割込は、レベル変化で発生します。rising エッジ、falling エッジの別を指定することはできません。ですので、割込ルーチンなどで工夫して区別するしかありません。
割込のセットアップ
という訳で、実際のセットアップ方法です。AVR マイコン歴 = 数日の私の理解によりますと、Pin Change 割込をイネーブルにするには、次の設定が必要です。
- SREG: AVR Status Register
まず、ここにある I ビット(Global Interrupt Enable)を設定する必要がありますが、Arduino が動作しているときは、デフォルトでイネーブルになっているので、特に意識しなくて大丈夫です。(もちろん、cli() されてたらダメですが。その場合は sei() します。)
- PCICR: Pin Change Interrupt Control Register
これは、各 PCI0〜2 を、8本の割込ピンごとまとめて有効/無効を設定するためのものです。ビットマップになっているので、PCINT20 を使う場合は、対応する PCIE2 のビットを 1 に設定します。(上に書いたように、PCINT20 は、PCI2 に含まれるからです。)
- PCMSK0〜PCMSK2: Pin Change Mask Register 0〜2
これは、各 PCI0〜2 の中で、どの信号線の変化をモニタしたいか設定するためのビットマップです。PCINT20 は、PCMSK2 レジスタのビット PCINT20(下位から 5ビットめ)にあるので、そこに 1を設定すれば良い訳です。
以上をまとめますと、PCINT20 を使う場合、Arduino Sketch では
/* * Enable Pin Change Interrupts */ PCICR = 0; // Disable all Pin Change Interrupts PCMSK0 = 0; PCMSK1 = 0; PCMSK2 = 1 << (20 - 16); // Enable PCINT20 only PCICR = 1 << 2; // Enable PCI2 only
のような手順を取れば良いでしょう。簡単ですね。
割込ハンドラ(ISR)の書き方
Arduino Sketch で割込ハンドラを書くには、ISR() というマクロを使います。従来は SIGNAL() というマクロもあったようですが、新しいコードでは ISR() を使え、となっています。詳しくは、インストールした Arduino IDT のどこかにある hardware/tools/avr/avr/include/avr/interrupt.h を参照してください。
以下に実際の例を示します。
ISR(PCINT2_vect) { // Interrupt Handling }
ここで、PCINT2_vect のような名前は何かというと、hardware/tools/avr/avr/include/avr/iom328p.h などで定義されています。中を検索してみてください。
繰り返しになりますが、Pin Change 割込はレベル変化で発生します。rising エッジ、falling エッジの別を指定することはできません。ですので、割込ルーチンなどで工夫して区別するしかありません。
ついでにスリープを組み合わせる
最後に、スリープ機能を組み合わせた例を示して終わりにします。詳細は、
などにありますが、あまり分かりやすいとは言えません。
以下では、loop() の中にループを書いたりしてますが、どうも私は C++ のコンストラクタの動きが読めない(← 勉強不足)なので、全部シーケンスが見えるようにしています。これをマネしなくて結構です。
ISR(PCINT2_vect) { sleep_disable(); // Resume to the main loop. } void loop(void) { /* * Enable Pin Change Interrupts */ PCICR = 0; // Disable all Pin Change Interrupts PCMSK0 = 0; PCMSK1 = 0; PCMSK2 = 1 << (20 - 16); // Enable PCINT20 only PCICR = 1 << 2; // Enable PCI2 only set_sleep_mode(SLEEP_MODE_PWR_DOWN); /* * Enter main loop */ for (;;) { sleep_enable(); sleep_cpu(); // NOT sleep_mode(); // Wake up here! // Something you need } }
少し説明します。
- set_sleep_mode(SLEEP_MODE_PWR_DOWN)
これは、スリープモードの設定です。後で sleep_cpu() すると、このモードでスリープします。設定可能はモードにはいろいろあるようですが、私もまだ勉強してません。ごめんなさい。SLEEP_MODE_PWR_DOWN が、一番低消費電力の、深いスリープのようです。
- sleep_enable(), sleep_disable()
これはそれぞれ、スリープの有効化と無効化です。具体的に何をしているのかは、私も勉強中です。
- sleep_cpu()
実際にスリープに入ります。資料によっては sleep_mode() を紹介しているものもありますが、sleep_mode() は
sleep_enable(); sleep_cpu(); sleep_disable();
の連続実行と同義です。
とりあえずここまで。これで、スリープしてから Pin Change 割込で起きるコードが書けると思います。おしまい。
2つほど、関連注意
一つ目は、millis() などで時間を読む場合です。スリープ中は、(スリープモードにも依ると思いますが)タイマも止まってしまう場合があります。これが問題になる場合は、quick hack として
extern volatile unsigned long timer0_millis; uint8_t oldSREG; // sleep_enable(); sleep_cpu(); oldSREG = SREG; cli(); timer0_millis += 1000; SREG = oldSREG; //
とかするしかないかも知れません(9月17日: 割込禁止にしました)。詳しくは、hardware/arduino/cores/arduino/wiring.c とか見てください。
二つ目は、シリアルメッセージが出なくなるという問題です。私の場合は、sleep_cpu() の直前に Serial.println() とか呼んでたのが敗因でした。ちゃんとシリアルを吐き出したことを保証してから sleep_cpu() したほうが良さそうです。私は、XBee の CTS 信号を見られたので、それがネゲート(de-assert)してから sleep_cpu() するようにして回避しました。
おまけの注意
自分がハマってしまったので、オマケの注意です。timer0_millis 変数は割込の中でアップデートされます。timer0_millis 変数を読み書きする場合は、割込禁止にしておきましょう。そうしないと、読み出し値が誤ったり、書き込みで壊れたりします。(9月17日)
*1:全部の製品ではないかも。その辺、詳しくない。