本品の仕様として、1秒に1回電流・電圧値を計測し、記録モードの時は指定された時間間隔(1秒の倍数)で SD カードに記録するものとする。マイコンにとっては大部分が待ち時間となるので、なるべく省電力モードで時間が来るのを待ち、割り込みで処理することを考える。
一方、タクトスイッチの入出力については、押した瞬間に処理が始まることが望ましい。1秒ごとの割り込みのタイミングを待っていたら、使い勝手が極めて悪くなるだろう。従って、タクトスイッチの入出力も、割り込みで処理することになる。タクトスイッチは入力ポートに直結しているので、チャタリング除去もソフトウェアで行う必要がある。
Up/down キーとは、データ取得の間隔を設定するためのキーである。記録モード中はこれらのキー入力は無視してよいので、上の図では記録中でないモードを想定している。
さらに細かい仕様として:(1) スタート/ストップボタンは約1秒間の長押しで処理するものとする。1秒を待たずにボタンが離されたらその前の状態に戻る。(2) SD カードに記録したときに印(* など)を表示する。あまり表示時間が短いと見えないので、0.4 秒ぐらいは表示しておきたい。
長押しについては、下のような処理になる。記録モードに移行する時は、長押しが終了すると同時に割り込みのタイミングを変更して、この時点から1秒カウントが始まるようにすべきである。記録モードを終了する時は、この処理は必要ない。
"*" の表示については、下のようになる。0.4 秒を delay()
などで待つのは消費電力の増大につながるので、これも割り込みを使うことが望ましい。
ATmega328 の仕様と照らし合わせて、次のように割り込みを使うことにした。
Timer1 のクロックは、8 MHz のシステムクロックを 256 分周したものとする。8 MHz/256/31250 = 1 Hz なので、31250 でクリアされるようにすれば、1秒ごとの割り込みが実現できる。
Timer1 の仕様は複雑で、データシートを読んでもなかなか理解できなかった。設定用のレジスタのうち、TCCR1A
と TCCR1B
にある WGM13..WGM10
の4ビットで動作モードを設定する (16.11.1 Timer/Counter1 Control Register A)。これを "0100" にセットすると、タイマーは CTC (Clear Timer on Compare Match) モードとなり、値が OCR1A
に達すると 0 に戻る (Table 16-4 の "Mode 4")。このとき、TIMSK1
のビット 1 (OCIE1A
) を立てておくと、割り込みが発生する。
なお、Timer0 は使わないので、止めてしまってよい。Timer0 は millis()
や delay()
を使う場合は動かしておく必要がある。本品では、setup()
の始めの方で1回 delay()
を使っているので、そこまでは Timer0 が動いていないといけないが、その後は止めておく。
割り込みハンドラは下のように書く。今回は、「Timer1 の Compare Match A の割り込み」なので、ISR
のカッコ内は TIMER1_COMPA_vect
となる。この定数名はどこで定義されているかと言うと、hardware/tools/avr/avr/include/avr/iom328p.h
に書かれている(最初の hardware
というフォルダは、Mac の場合 Arduino.cpp/Contents/Resources/Java
の中にある)。
上のコードにある通り、割り込みハンドラでは大域変数の timerCount
をインクリメントしているだけである。メインの処理はどうやって行うのか? それは、loop()
関数の実装を見ればわかる。このコードは Arduino の環境を流用しているので、初期化時に setup()
が呼ばれ、その後 loop()
が繰り返し呼び出される仕様になっている。
loop()
の中で、上記の割り込みに関係するところを説明する。まず最初に、static
変数である lastTimerCount
と timerCount
の値が比較される。これが異なっていれば、この loop()
呼び出しの前に一度タイマー割り込みがかかったことを意味する。局所変数の action
は「今回の loop()
呼び出しではどの動作を実施するか」を表すので、これを ACTION_TIMERA
にセットする。
次に、action
の値によって処理を振り分ける。action == ACTION_TIMERA
の場合は、タイマー割り込みの処理をここで行う。処理が終わると switch
文を抜けて、そのまま loop()
は終了する。
次に再び loop()
が呼ばれ、他に何もアクションがなかった場合は、switch
文の最後の default:
節が実行される。ここでは、sleep_cpu()
で CPU がスリープモードに入る。このスリープ状態は、タイマーまたは Pin Change 割り込み(後述)が起きるまで保たれる。割り込みが起きると、割り込みルーチンが実行され、その後 sleep_cpu()
の次の行の sleep_disable()
に制御が移る。そこで、次の処理を行うため、loop()
をいったん終了する。次に再び loop()
が呼ばれた時に、何らかの割り込み処理を行うことになる。
タイマー割り込みの処理は、以下のようになる。最初に、電流・電圧のデータと、電池電圧の測定を行い、タイトル行の表示を更新する。
電池電圧が低い場合は、この時点で一切の動作を停止する。スリープモードを SLEEP_MODE_PWR_DOWN
にして、ADC とアナログコンパレータを止めて、割り込みも止めて、スリープする。このスリープからは、リセット以外では復帰しない。
正常動作の場合は、液晶に現在の値を表示する(スタート/ストップボタン長押し中は別。後述)。
記録中の場合は、経過時間(秒数)を intervals[intervalIndex]
で割った余りが0なら、SD カードにデータを書き込む。1回ごとにファイルをオープン/クローズする。こうしないと、途中で間違って電源を切ったりした時にファイルシステムが壊れてしまう。
記録した印の "*" を液晶画面に表示する。これは 0.4 秒後に消す。消すタイミングは、Timer1 の OCR1B
割り込みで指定する。OCR1B
の値を、(現在のタイマーの値 + 0.4 秒) mod TIMER1_MAX
に設定し、OCR1B
の割り込みを許可する。また、OCF1B
フラグを明示的にクリアしておく。これを行わないと、すでに割り込み条件が満たされているものと判断されて、0.4 秒後でなく直ちに割り込みが発生する場合がある。
タクトスイッチの入力は Pin Change 割り込みで処理する。PD2, PD3, PD5 を入力に設定し、内部プルアップを有効にする。そして、Pin Change 割り込みを PD2, PD3, PD5 について有効にする。割り込み判定のため、直前のポート D の入力値 (PIND
) と、その時の Timer1 の値、timerCount
の値を volatile
変数で記録しておく。また、チャタリング処理のため、「割り込み処理を行った」時のこれらの値を save...
という名前の変数に保持するようにする。
割り込みハンドラは下の通り。単に volatile
変数に値をセットするだけ。
loop()
では、まずキー割り込みがあったかどうかを判定する。このときにチャタリング処理も行う。「押された」場合と「離された」場合で処理が異なることに注意。特に、スタートボタンの長押し中にボタンが離された時は、長押しをキャンセルする処理に入る。
キー割り込みの処理は下の通り。待機中または記録中にスタートボタンが押された時は、OCR1B
に値をセットして 1/16 秒後に割り込みをかけるようにする。この割り込みでは、液晶の2行目(データ行)に "." を1つずつ表示し、16 個に達したら長押しが完了したとして記録中または待機中モードに移行する。
先に示した通り、「"*"印の消去」「長押し中の表示」のために、Timer1 の OCR1B
割り込みを使っている。この割り込みハンドラは下の通り。timerBOffset
は、長押しの処理の時に TIMER1_MAX / 16
にセットされている。
loop()
の中では、timerBInvoked
でこの割り込みの有無を判定する。
実際の処理は下の通り。長押し中と記録中で処理を分けている。待機中からスタートボタンの長押しが完了した場合、次に記録が始まるので、そのための処理を行う。SD カードのファイルを書き込みオープンして1行目(" t_sec v_mV i_mA")を書き込み、Timer1 の値を TIMER1_MAX - 1
にセットする。これで、1/31250 秒後に記録を始めることができる。
スリープを使うことで、どの程度省電力の効果があるか、検証してみた。エネループ2本 (2883 mV) を使って、同様の仕様の電流電圧計で消費電流を測定してみた。
loop()
中の sleep_enable(); sleep_cpu();
をコメントアウト:7.0 mAやはり、余分な割り込みは極力止めて、スリープを使うことで、省電力に効果があることがわかる。