この記事では、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とフラッシュのクロックだけ タイマ、シリアル、ADC、SPI、I2C、ウォッチドッグ、割り込み 1.9mA
パワーダウン
(いちばん深い)
発振器が止まり、作られるクロックが全部止まる 外部割り込み、ウォッチドッグ、I2Cのアドレス監視 66µA

止まるものと残るものは、データシートの9.6 Idle Mode・9.8 Power-down Mode(p.35)に書かれているとおりです。消費電流は28.3 DC Characteristics(p.260)の代表値(16MHz・5V)で、動作中は9.2mA。アイドルでは5分の1ほど、パワーダウンでは140分の1ほどまで下がります。
残るものの顔ぶれを見ると、深さの正味の違いが分かります。パワーダウンではクロックそのものが止まるので、クロックを数えて時間を計る普通のタイマも一緒に止まり、時間で起こすことができません。残るのは、クロックがなくても動けるもの(自前の発振器を持つウォッチドッグと、信号の変化で反応する外部割り込み)だけです。だから③でウォッチドッグを使います。
電池で長く動かすなら、いちばん省電力のパワーダウンが基本です。今回もこれを使います。

② 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)

この定義を、8ビットの並びとして置き直します。

SMCR76543210
bit3〜bit1 SM2SM1SM0=どこまで止めるか(①の眠りの深さ)を選ぶbit0 SE=眠る許可(SLEEP命令の直前に立てる)

パワーダウンは SM2SM1SM00・1・0。つまりbit2のSM1だけを1にします。bit7〜bit4は未使用です。

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

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

パワーダウンで眠ると、普通のタイマも止まります。そこで使うのが、独立して動くウォッチドッグタイマ。一定時間ごとに割り込みを出し、それを起こす合図にします。ウォッチドッグはWDTCSRで設定し、WDIEを1にすると割り込みモードになります。
時間はWDP3〜WDP0の4ビットで選びます。この4ビットと時間の対応は、データシートのTable 10-3に決められています。

WDP3・2・1・0 発振サイクル数 時間(VCC = 5.0V)
0・1・1・0 128K 1.0秒
1・0・0・0 512K 4.0秒
1・0・0・1 1024K 8.0秒(最長)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR)Table 10-3「Watchdog Timer Prescale Select」, p.48 より必要な行を抜粋(© Microchip Technology)
最長の8秒にしたいので、WDP3WDP0を1にします。次のコードで_BV(WDP3) | _BV(WDP0)と書いているのは、この表の最終行を指しています。

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問は、周期を延ばす工夫と、眠る前の注意を扱います。まず自分で考えてから回答を開いてください。

はじめの1問は、前回のEEPROMからのおさらいです。間隔をあけて思い出すほど後に残るので、覚えている自信があっても一度書いてみてください。

おさらい Q0

前回のEEPROMのドリルでは、書き込みにEEMPEEEPEの2つのビットを使いました。先に立てるのはどちらで、それはなぜでしたか。

解答・解説を見る

解答:先にEEMPE(許可)を立て、そのあとEEPE(開始)。意図しない書き込みを防ぐため。

解説:今回のSMCRも同じ形です。SM2〜SM0で眠りの深さを決めてから、最後にSE(眠る許可)を立ててSLEEP命令を実行します。「設定をそろえてから、最後のひと押しで動かす」という並びは、EEPROMでもスリープでも変わりません。忘れていたらArduino EEPROMのドリルへ戻ってください。

基礎 Q1

いちばん消費電力が小さいのはパワーダウンです。ではなぜ、パワーダウン中は普通のタイマで起こせないのですか。

解答・解説を見る

解答:パワーダウンではクロックそのものが止まり、クロックで数える普通のタイマも一緒に止まるから。

解説:省電力とは、動いているものを止めることです。深く眠るほど止まる範囲が広がり、そのぶん起こしてくれるはずのものまで止まります。だからパワーダウンでは、独立した発振器で動くウォッチドッグや、クロックがなくても反応できる外部割り込みしか合図に使えません。アイドルなら普通のタイマも動いたままですが、データシートの規格値では1.9mAまでしか下がりません(パワーダウンは66µA)。「どこまで止めるか」と「何で起きられるか」は必ずセットで決まります。

基礎 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)

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

ATmega328Pのデータシートには、動作モードごとの消費電流が規格値として載っています。Uno R3と同じ16MHz・5Vの行を並べると次のとおりです。

9.2mA
動作中(16MHz・5V)
1.9mA
アイドル
66µA
パワーダウン(ウォッチドッグ動作中)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR)28.3 DC Characteristics, p.260。いずれも代表値(25℃)。ウォッチドッグを止めれば60µA。

動作中の9.2mAに対してパワーダウンは66µAなので、およそ140分の1です。アイドルでは1.9mAまでしか下がりません。眠りを深くするほど消費電流は小さくなります。
🔒 ただしこれはマイコン単体(チップ)の値で、ボード全体の値ではありません。Uno R3の基板には電源LEDやUSB変換ICが載っていて、マイコンが眠っても電気を使い続けます。手元の値を知りたいときは、電池で動かして電流計で測ってください。

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

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

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

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

1週間後に、もう一度ここへ戻る

この回の仕上げ方1週間後にこの記事をもう一度開き、解説を読まずに練習問題だけを解き直してください。特にQ4の眠る一連の処理と、Q6の8秒を数える書き方です。そのとき書けた分が、身についた分です。

間隔をあけて思い出す形にすると、続けて2回解くより後に残ります。「読み返す」ではなく「解き直す」ことだけ守ってください。読み返すと分かった気になりますが、書けるかどうかは書いてみるまで分かりません。

次に読む

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