この記事では、Arduino Uno R3の消費電力を抑えるスリープを練習します。使わない間はマイコンを眠らせ、必要なときだけ起こす、電池で動かすときに欠かせない仕組みです。

マイコンは、何もしていなくてもプログラムが回っている限り電気を使い続けます。コンセントで動かすなら気になりませんが、電池で長く動かしたいときは、この「待っているだけの消費」が問題になります。そこで、処理がないときはスリープで眠らせ、消費を大きく減らします。

まず眠らせずに動かす形を確認し、次にスリープモードを設定して眠り、ウォッチドッグタイマの割り込みで一定時間ごとに起こす形にします。割り込みで起きる考え方は、タイマの記事で扱ったものと同じです。

<自己紹介>
筆者は現役の車載組込みソフトエンジニアです。転職前に作った温湿度調整システムでは、常時起動だと電気を使いすぎるため、消費電力が最小になるよう眠らせる設計にしました。実務でも、電池やバッテリで動く機器では省電力の作り込みが評価されます。

スリープで学ぶこと

スリープは、マイコンの動きを一時的に止めて消費電力を抑える機能です。眠っている間はほとんど電気を使わず、決めた合図で起きて処理を再開します。

このテーマの要点

  • 何もしないときに眠らせると、消費電力を大きく減らせる
  • 眠りの深さはスリープモードで選ぶ(深いほど省電力・復帰は限られる)
  • モードの設定はSMCRというレジスタで行う
  • 起こす合図には、ウォッチドッグタイマや外部割り込みを使う

本記事のコードは、Arduino Uno(ATmega328P)向けにPlatformIOでコンパイル確認しました。消費電力の測定には電流計が要るため、まずは「眠って、一定時間ごとに起きる」動きを、LEDとシリアルで確認しながら進めてください。

準備するもの

使うもの 用途
Arduino Uno R3またはR3互換ボード ATmega328Pのスリープとウォッチドッグを使う。本記事はUno R4対象外
USBケーブル スケッチの書き込みと、起きたことのシリアル確認に使う
電流計(任意) 消費電流の変化を実測したい場合

割り込みで起きる仕組みを使うので、先にタイマのドリルで割り込みの考え方を確認しておくと理解しやすくなります。

まず眠らせずに動かす(比較用)

最初に、眠らせない普通のループを見ておきます。delay()で待つ間も、マイコンは動き続けて電気を使っています。

always_on.ino

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    delay(8000);   // 8秒待つ。この間もCPUは動き続けている
}

これでも8秒ごとにLEDは反転しますが、待っている間ずっと電気を使います。電池で動かすなら、この待ち時間こそ眠らせたいところです。

次にスリープで眠らせ、割り込みで起こす

同じ「8秒ごとに反転」を、待ち時間はスリープで眠り、ウォッチドッグタイマの割り込みで起きる形にします。ここでも、いきなり設定を書く前に、データシートでスリープの仕組みを確認します。

データシートからレジスタを読み解く

使うのはATmega328Pのデータシート(Microchip、文書番号7810D–AVR)の「9. Power Management and Sleep Modes」(レジスタ定義は9.11)と「10.8 Watchdog Timer」の節です。以下では、データシートの該当箇所を原文引用しながら、3つの手順で読み解きます。

① スリープモードを選ぶ

スリープには深さの違うモードがあります。深いほど消費は小さくなりますが、起こせる合図が限られます。代表的なモードは次のとおりです。

モード 消費電力 主な用途
アイドル やや少ない CPUだけ止める。タイマなどは動いたまま
パワーダウン もっとも少ない ほぼ全部止める。ウォッチドッグや外部割り込みで起こす

電池で長く動かすなら、いちばん省電力のパワーダウンが基本です。今回もこれを使います。

② SMCRでモードを設定して眠る

スリープモードと、眠る許可はSMCRというレジスタで決めます。SM2・SM1・SM0でモードを選び、SEで眠る許可を出します。データシートの「9.11.1 SMCR」の定義図(p.37)とTable 9-2「Sleep Mode Select」(p.38)は次のとおりで、パワーダウンはSM2・SM1・SM0 = 0・1・0です。

ATmega328PのSMCRレジスタ定義とTable 9-2スリープモード選択。SM2・SM1・SM0が0・1・0でPower-down、bit0がSE
図:SMCRの定義とTable 9-2「Sleep Mode Select」。SM2・SM1・SM0=0・1・0でPower-down、bit0がSE(眠る許可)。出典:ATmega328P Datasheet(Microchip, 7810D–AVR)9.11.1 SMCR(p.37)・Table 9-2(p.38)より原文引用(© Microchip Technology)

眠る許可のSEについて、データシートには原文でこう書かれています(英文は原文引用です)。

Bit 0 – SE: Sleep Enable
The SE bit must be written to logic one to make the MCU enter the sleep mode when the SLEEP instruction is executed. … it is recommended to write the sleep enable (SE) bit to one just before the execution of the SLEEP instruction and to clear it immediately after waking up.
(訳:ビット0 – SE:スリープ許可。SLEEP命令を実行したときにMCUをスリープへ入れるには、SEビットに1を書かなければならない。……SEビットはSLEEP命令の直前に1にし、起きた直後に0へ戻すことが推奨される。)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR)9.11.1 SMCR「Bit 0 – SE」, p.38 より原文引用(© Microchip Technology)

下の表は、この定義から今回使うビットを抜き出したものです。

レジスタ bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
SMCR SM2 SM1 SM0 SE

ビットを直接触ってもよいのですが、Arduinoにはavr/sleep.hという便利なヘッダがあり、set_sleep_mode()sleep_mode()がこのSMCRの操作を肩代わりしてくれます。中身は、モードのビットを立ててSEを1にし、眠る命令を実行しているだけです。

③ 起こす合図を用意する(ウォッチドッグタイマ)

パワーダウンで眠ると、普通のタイマも止まります。そこで使うのが、独立して動くウォッチドッグタイマ。一定時間ごとに割り込みを出し、それを起こす合図にします。ウォッチドッグはWDTCSRで設定し、WDIEを1にすると割り込みモードになります。WDPで時間を選び、最長で約8秒ごとに起こせます。

sleep_wdt.ino

#include <Arduino.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/interrupt.h>

volatile uint8_t g_wake = 0;

ISR(WDT_vect) {                // ウォッチドッグ割り込みで起きる
    g_wake = 1;
}

void wdt_setup_8s(void) {
    cli();
    wdt_reset();
    WDTCSR |= _BV(WDCE) | _BV(WDE);              // 設定変更を許可
    WDTCSR = _BV(WDIE) | _BV(WDP3) | _BV(WDP0);  // 割り込みモード・約8秒
    sei();
}

void enter_sleep(void) {
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);   // パワーダウン(SMCRを設定)
    sleep_enable();
    sleep_mode();                          // ここで眠る
    sleep_disable();                       // 起きたら続き
}

void setup() {
    DDRB |= _BV(DDB5);
    wdt_setup_8s();
}

void loop() {
    enter_sleep();             // 待ち時間は眠って消費を抑える

    if (g_wake) {
        g_wake = 0;
        PORTB ^= _BV(PORTB5);  // 起きたらLEDを反転
    }
}
delay版との違いdelay(8000)は、8秒間CPUを動かしたまま待ちます。スリープ版は、同じ8秒をほとんど電気を使わずに眠って過ごし、ウォッチドッグの合図で起きて処理します。やりたいこと(8秒ごとに反転)は同じでも、待ち時間の消費がまったく違います。

練習問題(基礎5+発展2)

基礎5問は、スリープモードと起こす合図の考え方を確認します。発展2問は、周期を延ばす工夫と、眠る前の注意を扱います。まず自分で考えてから回答を開いてください。

基礎 Q1

いちばん消費電力が小さいスリープモードはどれですか。

解答・解説を見る

解答:パワーダウン(SLEEP_MODE_PWR_DOWN)。

解説:ほぼ全部を止めるため消費が最小です。そのぶん、起こせるのはウォッチドッグや外部割り込みなど限られた合図だけになります。

基礎 Q2

スリープモードの選択と、眠る許可を出すレジスタはどれですか。

解答・解説を見る

解答:SMCR

解説:SM2・SM1・SM0でモードを選び、SEで眠る許可を出します。Arduinoのset_sleep_mode()sleep_mode()が、この操作を肩代わりします。

基礎 Q3

パワーダウン中は普通のタイマも止まります。では、一定時間ごとに起こすには何を使いますか。

解答・解説を見る

解答:ウォッチドッグタイマ。

解説:ウォッチドッグは独立して動くので、パワーダウン中でも時間を数えられます。WDIEを1にすると割り込みモードになり、決めた時間ごとに起こす合図を出せます。

基礎 Q4

avr/sleep.hを使って、パワーダウンで眠る一連の処理を書いてください。

解答・解説を見る
q4_sleep.ino

set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode();       // ここで眠る
sleep_disable();    // 起きたら続き

解説:モードを選び、眠る許可を出し、sleep_mode()で眠ります。起きると次の行から続きます。sleep_disable()で、意図しない再スリープを防ぎます。

基礎 Q5

ウォッチドッグを「リセットではなく割り込みで起こす」モードにするビットはどれですか。

解答・解説を見る

解答:WDIE

解説:ウォッチドッグには、時間切れでマイコンをリセットする使い方と、割り込みを出す使い方があります。WDTCSRWDIEを1にすると、割り込みモードになりISR(WDT_vect)が呼ばれます。

発展 Q6

ウォッチドッグの最長は約8秒です。約40秒ごとに処理したいとき、どうしますか。

解答・解説を見る
q6_count.ino

volatile uint8_t count = 0;

ISR(WDT_vect) {
    count++;
    if (count >= 5) {   // 8秒 × 5 = 約40秒
        count = 0;
        g_wake = 1;
    }
}

解説:8秒ごとの割り込みを数え、決めた回数に達したら処理の合図を立てます。総合課題の「5分ごとに測る」も、この数え方で作れます(8秒 × 約38回)。

発展 Q7

眠る直前にシリアルで文字を送っていると、途中で切れることがあります。なぜですか。どう防ぎますか。

解答・解説を見る

解答:送信が終わる前に眠ると、送信が止まるから。Serial.flush()で送り終わってから眠る。

解説:シリアルの送信は、少しずつ進みます。パワーダウンで眠ると送信も止まるため、途中で切れます。眠る前にSerial.flush()を呼んで、送り終えるのを待ってからスリープに入ります。

例:実務で実際に書くコード

電池やバッテリで動く機器では、省電力の作り込みが動作時間を左右します。基本の形は「眠る → 合図で起きる → 短く処理する → また眠る」の繰り返しです。起きている時間を短くするほど、電池が長持ちします。

low_power_loop.c

for (;;) {
    enter_sleep();        // ほとんどの時間は眠る

    if (g_wake) {
        g_wake = 0;
        read_sensor();    // 起きている時間は短く
        control();
        save_if_changed();
    }
}

温湿度調整システムでも、常に動かし続けると電気を使いすぎます。そこで、ほとんどの時間は眠り、一定時間ごとに起きて温度を測り、必要なら制御して、また眠る形にしました。省電力は、電源が切れても状態を残す前の記事の工夫と合わせて、「電池でも安心して置いておける機器」にするための作り込みです。

最後につまずきポイントをおさらい

起こす合図がないと、二度と起きないパワーダウンで眠る前に、必ず起こす合図(ウォッチドッグや外部割り込み)を用意します。合図を設定せずに眠ると、起きるきっかけがなく止まったままになります。書き込み直後に反応しなくなったら、まず起こす設定を確認してください。

もう一つは、眠る前にやりかけの処理を終わらせること。シリアル送信はSerial.flush()で送り切ってから眠ります。また、USBでつないだ状態ではパソコン側の回路も電気を使うため、本当の省電力は電池駆動で測るのが基本です。SMCRWDTCSRなどのレジスタ名はATmega328P向けで、Uno R4など別のマイコンでは変わる点も、これまでと同じです。
<補足>
スリープは、電池で動かす小型機器以外にも使用される機能です。たとえば車は、エンジンを止めた駐車中も、キーレスの受信待ちなどで一部のマイコンが動き続けています。走行中は発電機が電気を作りますが、駐車中はバッテリーだけが頼りです。ここでマイコンが眠らずに電気を使い続けると、数日の駐車でバッテリーが上がってしまいます。実際、車載の現場では駐車中に流してよい電流に厳しい上限があり、車載ソフトにおいてもスリープの作り込みは必須です。

よくある質問(FAQ)

どのくらい消費が減りますか?

条件によりますが、パワーダウンでは動作中と比べて消費電流が大きく下がります。実際の値はボードや周辺回路で変わるので、電流計で測って確かめるのが確実です。起きている時間を短くするほど、平均の消費は小さくなります。

ライブラリを使ってもよいですか?

省電力用のライブラリもあり、実際の開発では使ってかまいません。このドリルの目的は、SMCRやウォッチドッグが何をしているかを理解することです。中身が分かっていれば、ライブラリの設定の意味も読み取れます。

外部割り込みでも起こせますか?

起こせます。ボタンなどを外部割り込みにつなげば、その入力で眠りから復帰できます。一定時間ごとに起こしたいならウォッチドッグ、何かが起きたときに起こしたいなら外部割り込み、と使い分けます。

次に読む

ここまでで、出力・シリアル・タイマ・センサ・保存・省電力と、システムを作るための部品がそろいました。次は、これらを組み合わせて、赤外線でエアコンを操作する出力へ進み、最後に総合課題でひとつのシステムに仕上げます。