ArduinoのWatchdog timerを使用

作製した「気象観測シールド」で数分おきにデータを取得するため、ArduinoのWatchdog timerの使用法と省電力動作を調査した。

Arduinoを休止モードからタイマー割り込みで復帰させるには、watchdog timerを使用するのが常法らしい。例えば“Sleeping Arduino – Part 1”から始まるポストなどにサンプルコードを見ることができる。しかし、レジスタやフラグの意味などが詳細に解説されたサイトはなかなか見当たらず、結局\hardware\tools\avr\avr\include\以下にインストールされたヘッダファイルと、ATmega328Pのデータシートに辿り着いた。それらを基にいろいろ試した結果、下記のコードとなった。

/*
 * Log Weather Data
 * Copyright 2014 FireWheel <firewheel@m40.coreserver.jp>
 * GNU General Public License V3
 * see <http://www.gnu.org/licenses/>
 */
#include <SD.h>
#include <Wire.h>
#include <RHT.h>
#include <MPL.h>
#include <avr/sleep.h>
#include <avr/power.h>
#include <avr/wdt.h>

const char FILENAME[] = "weather.csv";
const uint16_t SLEEP_CYCLE = 30; // x 8 sec = 4 min to sleep
/* more than 1 sec */
const uint16_t WAIT_FOR_RHT = 1100;
const uint8_t CS_PIN = 10;

volatile uint16_t counter = 0;

void setup() {
  /* simply exit if SD initialization failed */
  if (!SD.begin(CS_PIN))
    return;
  MPL.begin();
  /* enable only the timer0 module for using millis() in RHT */
  power_all_disable();
  power_timer0_enable();
  /*
   * set unused pins power save mode
   * pins 9 to 13 and 17 to the end (19) are in use
   */
  for (uint8_t i = 0; i <= 16; i++) {
    if (9 <= i && i <= 13)
      continue;
    pinMode(i, INPUT_PULLUP);
  }
  /* the watchdog settings */
  /* disable interrupt */
  cli();
  /* watchdog timer reset */
  wdt_reset();
  /* clear WatchDog system Reset Flag on MCU Status Register */
  MCUSR &= ~(1 << WDRF);
  /*
   * set WatchDog Change Enable and WatchDog system reset Enable
   * on WatchDog Timer Control Register
   * (setting only WDCE does not seem to work)
   */
  WDTCSR |= 1 << WDCE | 1 << WDE;
  /*
   * set WatchDog Interrupt Enable, clear WDE
   * and set prescaler to 8 sec
   */
  WDTCSR = 1 << WDIE | 0 << WDE
         | 1 << WDP3 | 0 << WDP2 | 0 << WDP1 | 1 << WDP0;
  /* enable interruput */
  sei();
}

void loop() {
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  sleep_cpu();
  /* wait until the watch dog barks "bow-wow !" */
  if (counter == SLEEP_CYCLE) {
    sleep_disable();
    wake_up_routine();
  }
}

void wake_up_routine() {
  delay(WAIT_FOR_RHT);
  power_spi_enable(); // SD
  power_twi_enable(); // MPL

  String data = "";
  data += RHT.thermoString();
  data += ",";
  data += RHT.hygroString();
  data += ",";
  data += MPL.atmString();

  File logger = SD.open(FILENAME, FILE_WRITE);

  if (logger) {
    logger.println(data);
    logger.close();
  }

  power_spi_disable();
  power_twi_disable();
}

ISR(WDT_vect) {
  if (++counter > SLEEP_CYCLE)
    counter = 1;
}

まず、省電力設定として、avr/power.hの中で定義されているpower_all_disable()マクロで全モジュールを使用不可とし、必要なモジュールだけ使用可能にする。Arduino UNOで使用されているATmega328Pでは、以下の7モジュールを使用可能にするマクロが定義されている。これらには対応するpower_*_disable()マクロも存在する。

  • power_adc_enable()(ADコンバータ使用時)
  • power_spi_enable()(SPI使用時)
  • power_usart0_enable() (シリアルコンソール使用時)
  • power_timer0_enable() (タイマー0使用時、delay()millis()も)
  • power_timer1_enable()(タイマー1使用時)
  • power_timer2_enable()(タイマー2使用時、tone()も)
  • power_twi_enable()(I2C使用時)

また、使用していないピンの出力も抑える。arduino 720の“Power Saving Techniques”によると、消費電力はINPUT_PULLUPのほうがOUTPUTLOWよりも低いとのことであった。

watchdog timer設定のコードは上述のデータシート11.8.2の記載に倣った。割り込みを禁止するcli()と許可するsei()avr/interrupts.hで、watchdog timerをリセットするwdt_reset()avr/wdt.hで定義されている。MCUSRは11.9.1に記載があり、以下の4フラグを持つ。電源電圧低下など何らかの原因でwatchdog timerが有効になると、システムリセットの無限ループに入ることになるため、セットアップルーチンでWDRFフラグををクリアすることが推奨されていた。

WDRF
第3ビット。watchdogタイマーによるリセット発生時に立つ。
BORF
第2ビット。電源電圧低下によるリセット発生時に立つ。
EXTRF
第1ビット。外部リセット発生時に立つ。
PORF
第0ビット。電源投入時リセット発生時に立つ。

WDTCSRは11.9.2に記載があり、以下の8フラグを持つ。

WDIF
第7ビット。割り込みとして設定されたwatchdogタイマーのタイムアウト発生時に立つ。1を書き込むとクリアされる。
WDIE
第6ビット。watchdogタイマーを割り込みとして設定する場合に1をセットする。下記のWDEにも1をセットして割り込み+リセットとして設定した場合は、リセット発生の度に1を再セットする必要がある。
WDCE
第4ビット。WDEやプリスケーラビット(WDP3-0)を変更する前に1をセットする必要がある(WDCE単独では効果がないようで、WDEにも1をセットする)。4クロック後に自動的にクリアされるので注意。
WDE
第3ビット。watchdogタイマーをリセットとして設定する場合に1をセットする。MCUSRで説明したWDRFで上書きされるので、WDEをクリアする場合はまずWDRFをクリアする必要がある。
WDP3-0
WDP3(第5ビット)、WDP2(第2ビット)、WDP1(第1ビット)、WDP0(第0ビット)でプリスケール(PS)値を設定する。watchdogタイマーは、水晶発振ではなくRC発振で得られるので、タイムアウト(TO)値は入力電圧や周辺温度に依存する。

watchdogタイマーのプリスケール設定
WDP3 WDP2 WDP1 WDP0 PS値 TO値
0 0 0 0 2K (2048) 16 ms
0 0 0 1 4K 32 ms
0 0 1 0 8K 64 ms
0 0 1 1 16K 128 ms
0 1 0 0 32K 0.25 s
0 1 0 1 64K 0.5 ms
0 1 1 0 128K 1 s
0 1 1 1 256K 2 s
1 0 0 0 512K 4 s
1 0 0 1 1024K 8 s

watchdogタイマーの設定が終わったらスリープモードに入れる。avr/sleep.hで定義されているset_sleep_mode()でモードを設定し、sleep_enable()でスリープを許可、sleep_cpu()でスリープを開始する。スリープから復帰した後は、sleep_disable()でスリープを禁止してから次の命令に移る。sleep_enable()sleep_cpu()sleep_disable()を順に実施するsleep_mode()マクロも定義されているが、割り込みの競合は考慮されていない。avr/sleep.hで定義されているset_sleep_mode()の引数は以下の6種。SLEEP_MODE_IDLESLEEP_MODE_PWR_DOWNあたりが頻用されるようである。

SLEEP_MODE_IDLE
Idleモード。CPUクロックとフラッシュメモリ用クロックのみを停止する。
SLEEP_MODE_ADC
ADC Noise Reductionモード。CPUクロック、フラッシュメモリ用クロック、およびI/Oクロックを停止する。ADCクロックや非同期タイマークロックは動作したままなので、高分解能測定が可能となり、ノイズの多い環境下のA/D変換を改善する。
SLEEP_MODE_PWR_DOWN
Power-downモード。すべてのシステムクロックとオシレータを停止する。外部割(INT0、INT1)、ピン変化、2線モジュール(I2C)、watchdog、電源電圧低下の各割り込みでスリープから復帰する。
SLEEP_MODE_PWR_SAVE
Power-saveモード。Power-downモードに加え、動作しているtimer2を停止させず、割り込みに使用できる。
SLEEP_MODE_STANDBY
Standbyモード。外部クロックオプションが設定されている場合、Power-downモードに加え、オシレータを停止させない。
SLEEP_MODE_EXT_STANDBY
Extended Standbyモード。外部クロックオプションが設定されている場合、Power-saveモードに加え、オシレータを停止させない。

以上でwatchdogタイマーを使用したスリープモードの設定は終了した。お疲れさまでした。と、言いたいところであるが、watchdogタイマーは最長8秒(8,192ミリ秒)である。分の単位でスリープはできない。割り込み時に呼び出されるISRでカウントアップし、規定回数に達するまでは速やかにスリープモードに再突入させるようにして擬似的にプリスケールした。

結果、データの取得は意図どおりに行えたものの、駆動時間は約20時間にとどまった。1,900 mAhのニッケル水素電池(1.2 V)2本直列電源なので、5 Vで20時間駆動とすると消費電力は約46 mAh(計算合ってるかな)。思ったより電力消費は抑えられていないようである。どこか設定に間違いがあるのか・・・。

コメントをどうぞ