ATtiny85/45 ピンチェンジ割込み、アナログ式キー識別、アイドルスリープ

概要

ATtiny85/45を使って家電用の赤外線リモコンを作るにあたって以下を試してみた。

ATtiny45V
  1. ピンチェンジ割込みでパワーダウンスリープから復帰する。
  2. 抵抗分圧とA/Dコンバータを使って、押されているキーを識別する。
  3. Arduinoのライブラリを一切リンクしない。
  4. アイドルスリープも使ってみる。
  5. 確認用のデータ送信は赤外線で行う。

これらを組み合わせ、押下されたスイッチの番号などを赤外線リモコンと同じやり方で送信するようにした。受信側は以前作ったArduino nanoを使った仕組みを利用したので、今回のボードが出力するデータはPC上のシリアル端末に表示される。

Arduno Nano
赤外線受信側 (Arduino nano + OSRB38C9AA)

ピンチェンジ割込みによるパワーダウンスリープからの復帰

ATtiny5シリーズは、他のAVRマイコンと同様にほとんどの機能(クロック)を停止するパワーダウンスリープを備えている。パワーダウン中の消費電流は、VCC=3.3V以下ならば0.2μAまたはそれ以下になる。赤外線リモコンにするときは、コイン電池(CR2032など)での駆動を考えているからこの低消費電力性能は頼もしい。

パワーダウンスリープから復帰させるためには、外部から何らかのイベントを与える必要があるが、今回はピンチェンジ割込み(PCI : Pin Change Interrupt)を利用することにした。PCIは、ポートピンの状態(論理Hまたは論理L)が反転したときに生じる。つまり、オフだったスイッチがオンになったときに起こるから、リモコンのキー操作ではわかりやすい。

PCIを利用する際には以下のように準備しておく。

  • GIMSK (General Interrupt Mask)レジスタのPCIEビットを1とする。
  • PCMSK (Pin Change Mask) レジスタ内の、PCIの対象としたいポートピンのビット(PCINT5~PCINT0)を1にする。
  • そして、SREG ( Status Register) のIビットを sei() してセットする。

今回のボードではPB3にスイッチを接続しているので、メインループで以下のようにした。PCIはスリープからの復帰にのみ使っているので、割込みハンドラの中身は要らない。

こうしておくことで、スリープに入るときにオフだったスイッチがオンになるとスリープから復帰し sleep_disable(); 以下が実行される。逆に、スイッチがオンのままスリープに入ったときはオフになった時点でもスリープから復帰してくることになる。スリープ復帰後にポートピンPB3の状態を読んで、Lレベルなら余計な処理を行わないようにする。

割込みハンドラの中身が要らないからといって、 ISR (PCINT0_vect) { }  を省略してしまうとPCIが起きるたびにリセット(0番地から実行)されることに注意。これはPCI用の割込みベクタの中身(飛び先)が __bad_interrupt のままなためなのだが、これについては後述する。

抵抗分圧を使って、押されているキーを識別

今回の回路では、押下中のスイッチに応じて抵抗値が変わる分圧回路によって電源電圧を分圧し、内蔵A/Dコンバータで電圧を読み取るようにした。なので、4つのタクトスイッチを並べた構成としているがスイッチ用のポートピンはPB3だけで済んだ。

スイッチ部分は以下のような回路になっている。

スイッチによる分圧回路

抵抗R0 = 51kΩとし、R1~R4は以下のような抵抗値とした。

SW 抵抗 分圧率 変換値
SW1 R1 = 0 (直結) 100% 1023
SW2 R2 = 4.7k 92% 940
SW3 R3 = 15k 77% 787
SW4 R4 = 22k 70% 716

いずれかのキーが押されたときにPB3にかかる電圧は、Vcc × R0 / (Rn + R0) になる。ATtinyのA/Dコンバータはリファレンス電圧としてVccを選択できるから、A/D変換結果がフルスケール(1023)の何%なのかを計算し、スイッチごとの分圧率と比較すれば押下されたスイッチを識別できる。電池の消耗によって電源電圧が変動したとしても影響を受けない。

重要なポイントは、PB3にかかる最小電圧がATtinyの最小入力H電圧(VIH Min.) を下回らないことだろう。下回ってしまうと、スイッチを押したとしてもPB3の状態は論理LのままなのでPCIが起きないことになる。

コイン電池での駆動時にはVcc= 2.0Vまでの動作を期待したいので、データシートのDC特性に基づいてVIH (Min) = 0.7 × Vcc として抵抗値を決めた。Vcc (min) = 2.4Vとするならば、0.6 × Vccまでは論理Hとなるので、もうちょっと大きな抵抗を使うこともできるだろうし、1つのポートピンに接続するスイッチ数を増やすこともできる。

今回PB3をキー入力に使うことにしたのは、Arduino PRO MINIを使ったISPボードに載せたままスケッチの動作確認がしたかったから。ISPボードに載せているときは、SPI用の3本( SCK = PB2、MISO = PB1、MOSI = PB0)がPRO MINIにつながっているので、電気的な影響なく使えるのはPB3とPB4だけだった。もう一本のPB4はIRLEDの駆動に使っている。

ISPボードに載せたまま使わないならば、IRLEDの駆動をPB1で行い、PB2(ADC1)、 PB3(ADC3)、PB4(ADC2)にそれぞれ4つのスイッチをつなげれば、ATtiny単独でも12個のスイッチを持つリモコンを作ることができるだろう。

Arduinoのライブラリを一切リンクしない

Arduinoは卒業とかではなくて、TC0 (タイマーカウンタ0)ユニットの割込みハンドラ( ISR(TIMER0_OVF_vect) {}  )を自前で持ちたかったので、必然的にそういうことになった。今回のスケッチではあまり活躍していないのだけど、リピート送信などタイミングを意識した使い方をするならば、アイドルスリープとタイマーによる復帰機能が必要になる。なおかつ、スリープ時にはTC1ではなくてTC0を使いたい。
そのためには、Arduinoのコアにある wiring.cがリンクされると困るのである。たとえば、スケッチの中からdelayMicroseconds(); を使おうとすると、ビルド時に以下のようなエラーがでる(空行は削除)。

これは、TC0のタイマーオーバーフロー割込みハンドラが、delayMicroseconds() のある wiring.c.o で既に定義されてますよ、重複はダメですよ、ということ。このエラーを避けるためには、wiring.c 内にある関数の利用はあきらめないといけない。

で、どうすればいいのかというと自前のスケッチ内にメイン関数として、

を書けばよかった。この話は、avrfreaks の古い書込みにあったように思う。
main() {} がスケッチ側にあれば、arduinoのcoreにあるmain.cppのコンパイル結果はリンクされずスケッチ側のmain() がユーザープログラムのエントリになる。そして、init(); やTIMER0_OVF_vect のISR を含む wiring.cのオブジェクトファイルもリンクされなくなる。
代償として、delayMicroseconds(); のような有用な関数が使えなくなってしまう。この関数は命令の実行クロック数に基づいてループを回すだけなのだが、コピーしてくるのでは面白みがない。

avr-objdump

本当に意図していないライブラリがリンクされていないかどうかの確認や、ロードモジュール全体の構成を知るには avr-objdump というツールを使う。このツールもavr-nmなどと同じフォルダに格納されている(リンク済みのシンボルを見るだけならavr-nmでもいいのだが)。avr-objdump のman はこちら

たとえば、test.ino というスケッチをArduino-IDEでビルドし、test.ino.elf というオブジェクトファイルが一時フォルダに作成されたとすると、
  avr-objdump -S test.ino.elf
を実行することで、作成されたロードモジュール全体のアセンブラリスティング(スケッチ付き)を得ることができる。以下のような内容が出力される。

長くなるので一部省略したが、0番地からベクタテーブルが始まり、28行目で main() を呼出しているのが分かる。その後を見ていっても、意図していない関数はリンクされていないようだった。自分の書いたコードがどういう命令に翻訳されたのかを見たり、実行時にスタックがどれだけ積み上がるのかを知るためにも有益だろう。

上の内容の最初の方の <__vectors> を見ると、割込みベクタ2(PCINT0)とベクタ5(TIMER0_OVF)がスケッチで定義済で、その他のベクタは__bad_interrupt を向いている。割込みハンドラがスケッチやライブラリで定義済みの割込みが起きたときは対応する関数に飛ぶが、未定義の割込みが起きたときには31行目の __bad_interruptに飛ばされリスタート(0番地にジャンプ)することが分かる。スケッチ内で、 ISR (PCINT0_vect) {}  を宣言することで、所定のベクタの内容がハンドラのアドレスに書き換わるようになっている。

アイドルスリープと消費電流

なぜTC1ユニットではだめで、TC0を使う必要があるのかについて。

アイドルスリープ機能は、あまり内部クロックをとめないスリープモードで、パワーダウンスリープに比べると大幅に電流を消費する。ATtinyのPRR(Power Reduction Register )を操作して各部の動作を禁止しながら実際に電流を測ってみた(内部1MHz, @3.0V)。

PRR設定 電流 備考
アクティブ時 590μA スリープせずに無限ループ実行
(PRR = 3, ADC, USI停止)
(以下はアイドルスリープ中)
PRR = 0 325μA PRR操作なし
PRR = 3 298μA ADCとUSIを停止
PRR = 7 290μA ADC, USI, TC0停止
PRR = 11 220μA ADC, USI, TC1停止
PRR = 15 213μA ADC, USI, TC0, TC1停止

TC1はTC0よりも大幅に電流を使うから、アイドルスリープからの復帰用途には使わず、PRRを使ってTC1を停止しておきたい。今回は、TC1は赤外線送出のためのpwm出力時にのみ有効にすることにした。

アイドルスリープを使った遅延関数 delay_us(), delay_ms()

アイドルスリープの勉強も兼ねて、delayMicroseconds(); delay(); の代わりになる void delay_us(uint16_t us); と void delay_ms(uint16_T ms);  という関数をやっつけで作った。いずれもTC0のオーバーフロー割込みとアイドルスリープを利用する。
オーバーフロー割込みはTC0のカウンタTCNT0が255のとき次のクロックで起きるから、プリスケーラでカウンタの分解能を指定し、カウンタの初期値を指定してやることで都合のよい時点で割込みを起こすことができる。

delay_us()の方は、TC0を1/8 プリスケーラ で動かすのでカウンタTCNT0は8μsecずつ増えていく。2048μsec以上を指定した場合はアイドルスリープに入る。タイマー割込みを使うためのオーバーヘッドもあって、引数に50μsec以上を指定した場合にのみ有効。それ以下の場合、すぐに(4μsec程度)でリターンする。より短く正確な遅延が必要な場合は、自前の割込みハンドラをあきらめてdelayMicroseconds(); を使うことになる。

delay_ms() の方は、1/64のプリスケーラを設定し、オーバーフロー割込みは10msec単位に起きるようにした。こちらは1msec~65535 msecまで指定可能。

節電効果からみると、例えば delay_us(10000); とした場合、約8.2msecの間はアイドルスリープ(220uA)し、約1.8msecの間はアクティブ動作(590uA)する。10msec間のアクティブ動作に比べると、(220 × 8.2 + 590 × 1.8) / (590 × 10) = 0.48なので50%強の節約。

delay_ms(100)とした場合、ほとんどスリープで過ごし10回目のオーバーフロー割り込みが起きるとリターンするから、590μA × 100msecに対して220μA × 100msecということになり、約63%程度の節電になる。

TC1やADCを使うことが分かっている場合、PRRの操作には気をつける必要がある。今回のスケッチでは、setup() においてUSI、ADC、TC1を停止し、ADCを使うときのみADCを有効化、赤外線データを送信するときのみC1を有効化するようにした。

回路図

Arduino PRO MINIを使ったISPボード上に載せたままでも動くよう、以下のような回路にした。

ATINY_IR_KEY1(ISP)

むろん、ISPボードから外して専用の電源にしても同じように動く。

注意した点は、ISPボードに載せているときの電源はPCのUSBから来る+5V弱なのに対し、想定している赤外線リモコンはコイン電池で駆動するから電源電圧は+3.3~+2.0Vになること。そのため、IRLED周辺の抵抗(R5, R6)は、+5.0V時にIFが100mAを超えないよう大きめにし、トランジスタのベースに流す電流も控えめ(2mA程度)にした。コイン電池のみで駆動するならば、R5、R6共により小さな抵抗値にすることになる。

今回のボードで電源電圧を変えながら試してみたところ、+2.0Vまでは赤外線データを正しく出力していた(受信側でデータを把握できた)が、それを下回ると怪しくなった。送信側と受信側の距離が50cm程度なことを考えると、今回の抵抗値はコイン電池で駆動するリモコンとしては実用的ではなさそうである(低電圧版のATtiny45Vを使用)。

ATINY_IR_KEY1(ISP)

左側がArduino PROMINIのISPボードで書込み先としてATtiny45Vを載せている。右側がスイッチ、トランジスタおよびIRLEDを載せたターゲットボード。ATtinyのPB3とPB4をターゲットボード側にジャンパワイヤで接続している。ターゲットボード側でジャンパワイヤがささっているあたりにATtinyを挿すことになる。

ATINY_IR_KEY1(ISP)

写真では電源を接続していないが、ターゲットボード側にATtinyを載せると単独で動作する。

IRLED L-53F3

気まぐれなのだけど、今回はIRLEDとしてL-53F3BTを使った。レンズが青いのが特徴で、今までの工作で使ったOSI5LA5113Aよりも照射角が広い(データシートはこちら)。
このIRLEDのIFの絶対最大定格は連続時で50mA、パルス時(デューティ1/100, H期間10μsec) で1200mAとなっている。今回のIRLED周りの抵抗値は+3.3V動作のつもりで決めたので、+5V時は6~7μsec程度のパルスだがIF=70mA程度のはず。+5Vまで動作範囲に考えるならば、上の回路のR5は68Ωとすべきかもしれない。

IRLED L-53F3

このIRLEDも、東芝の2SC1815もディスコンなので、どこかで在庫を見つけたらまとめ買いしておきたいところ。

スケッチ

スケッチは以下のようになった。

ATtiny45V(Internal 1MHz) をターゲットとしてビルドしたところ、以下のように表示された。

avr-objdump で見たところ、1234バイトのロードモジュールのうち、スケッチ自体は800バイト程度で、残りの主要部分は掛け算や割り算のためのランタイムライブラリだった。ATtiny5シリーズには乗算命令がないんですね。また、スタックも20バイト程度は使うようだが、SRAMにはまだ余裕がある。

pwmによる赤外線波形の作成については、前に載せたのと同じようにした。受信側の都合で1フレームは4バイトとして送信しており、PRRの操作によりその間だけTC1ユニットの動作を許可している。

全体の流れ

今回のスケッチはmain() から開始する。まずsetup() でポートやレジスタの初期化、不要な周辺デバイスの無効化、ウォッチドッグの禁止を行いメインループに入る。

メインループでは、PB3のPCIを許可してすぐにパワーダウンスリープに入る。いずれかのスイッチの状態変化によってパワーダウンスリープから復帰すると、スイッチのバウンスを意識した20msecの遅延のあとでPB3の状態を読み、論理Lならばスイッチオフによる復帰なので、何もしないでループを続行する。

論理Hならば、read_key(); によりPB3(ADC3)の電圧を読み取ってフルスケール(1023)の何%なのかを計算している。戻り値がスイッチの番号、引数には分圧率 (パーセント)が帰る。スイッチ番号が取得できた場合は、赤外線リモコンと同じフォーマットでスイッチ番号とA/D変換結果を送信し、ループの先頭に戻る。

read_key() について

まずはPRRを使ってADCの動作許可を行い、ADCの電圧リファレンス(Vcc = 0)とシングルエンドでの変換対象チャネル(ADC3 = 3) をADMUXに指定している。そして、ADCSRC( ADC Control and Status Register A) に対してADCの動作許可(MSB = 1)と、ADCクロックのプリスケーラ(3 = 1/8 clock) を指定。データシートによれば、精度を求めるならADCクロックを50k~200kHzで使えとあるので125kHzとした。

ADCSRAのADSCビット(ビット6) を1にすることで変換が開始する。A/D変換に要する時間は最悪25クロック、通常13クロックということなので104~200μsecといったところ。変換が終了するとADSCビットが0になる。

今回のスケッチでは、A/D変換を3回やってその平均値を得ているが、スイッチのバウンスによって実際はスイッチがオフなのに変換を開始してしまったかもしれない。そのため毎回読み取り値を評価するようにした。

3回のA/D変換が終わると、ADCを動作禁止してからPRRを元に戻し(ADCクロック停止)、分圧率を求めてからどのスイッチがオンになっているのかを調べている。念のため比較の範囲は広めにした。

実行結果

スイッチ1を押してみると、PC側のシリアル端末には以下のように表示された。( Arduino nanoを使った赤外線リモコンデータの受信器の改訂版スケッチ による出力)。

Trasfer DATAの1バイト目がスイッチの番号、2番目の64 (16進)が分圧率なので、100%だということが分かる。電源電圧を+5Vから+2.0Vまで変更してみたが、各スイッチとも数パーセントの誤差が生じることがあるものの、スイッチ番号を取り違えるようなことはなかった。

きょうのまとめ

送出するデータを入れてやればシンプルな赤外線リモコンとして機能する程度までのことはできた。もともと、実物の代替品ではなく特殊な機能をもつリモコンを作ろうと思って始めた準備だったので、タイマー関係のお勉強に時間を使ってしまった。

ウォッチドッグタイマーによるスリープからの復帰

今回は試してみなかったが、 ウォッチドッグタイマーを使った割込みによって一定のタイミングでパワーダウンスリープから復帰する方法がある。
今回のスケッチでは、ウォッチドッグタイマーの動作を禁止して実行しているのでパワーダウン時の消費電流は0.2μA以下だが、常にウォッチドッグ動作を許可したままだと4μA弱になる。そのあたりが気になっていたのだが、一定時間後に復帰させたいときだけウォッチドッグ動作を許可するなどしてやれば、タイマーを使わなくてもいいかもしれないので、試してみようと思っている。

そうなると、Arduinoのライブラリをリンクしても問題ないような気もする。ちょっと回り道してしまったかもしれないが、一つ知見を得たということにしよう。

おまけ。小物撮影用のライト

この投稿の先頭に載せたATtiny45Vの写真は、OM-D E-M5mkIIに、MZD30mmマクロを付けて撮影したものだけど、デバイス表面のマーキングがはっきり写らなくてちょっと苦労した。上からの光を反射しずらくしているのだろうか。

撮影風景

昔の胃カメラ風のフレキシブルアームの先にチップ型の白色LEDのついたUSBライト(送料等込みで1本あたり200円程度)を4本使い、横から光を当てるようにすることで、マーキングが読める程度にはなった。
こういった形状のマクロ撮影用ライトを作りたいと思っていたのだけど、安くて細いフレキシブルアームがなかなか見つからない。200円なら分解して素材にしてもいいかなと思って購入したのだけど、4口のUSBハブ(セルフパワー)を電源にすることでそのまま使えた。分解するのは壊れてからにする予定。

E-PM2 + MZD30mm F3.5
E-PM2 + MAL-1 + MZD30mm F3.5

こちらは以前にも載せたものだが、E-PM2にオリンパス純正のマクロアームライト MAL-1を装着して使っているところ。MAL-1もデバイスの撮影には重宝しているのだけど、E-PM2からSDカードを抜くときはカメラを三脚の雲台から外す必要があって億劫である。オリンパス純正のPC接続ケーブル(変なコネクタ付き)も長さに制限があるのでカメラの場所を選んでしまう。

残念なことにMAL-1は最近のOM-Dでは使えない。どうやらMAL-1自体も製造終了しているようだ(そりゃ、そうだろうけど)。代わりになるものをいろいろと物色中なのだけど、なかなか安くて良さそうなものがない。リング状のライトは、ここまで近接した撮影では中央部に光が当たらないから向いていないように思う。MAL-1を所有していることが、E-PM2を使い続ける理由の一つになってはいるのだが。