CircuitPythonでDeep Sleepする方法
AdafruitのLearning Guideの非公式日本語訳です。
英語独特の言い回しを可能な限り日本語的な表現に直していますが、不自然に感じる部分も残っていますことを何卒ご容赦ください。
訳注:本記事はESP32-S2を搭載したAdafruit MagTagを例に書かれていますが、他のマイコンでもDeep SleepやLight Sleepを行うalarmモジュールを使用することが可能です。alarmモジュールに対応している主なマイコンはこちらです。
RP2040
Adafruit KB2040
Adafruit MACROPAD RP2040
Raspberry Pi Pico
ItsyBitsy RP2040
QT Py RP2040
Adafruit Feather RP2040ESP32
Seeed Studio XIAO ESP32C3nRF52840
Seeed Studio XIAO BLE
Feather nRF52840 Sense
Adafruit CLUE
概要
もしCircuitPythonプロジェクトでバッテリーの寿命を最大にしたいのであれば、何もしていないときにプログラムをスリープさせることができるようにする必要があります。例えば、数分または数時間おきにしか温度を読み取ったり、データを取得しないようにしたい場合があります。その間、ボードはスリープ状態になり、バッテリーからわずかな電力しか消費しないようにすることができます。
もし、Adafruit MagTagのE-inkディスプレイのように、電源が切れても見えるディスプレイを使っている場合は、ディスプレイの更新の間にスリープさせることができます。
このガイドでは、CircuitPythonで利用可能なスリープとウェイクアップアラームの機能について説明します。
AlarmとSleep
用語の定義
まずはDeep SleepとLight Sleepの区別をつけましょう。
- プログラムがdeep sleepを行った場合、まずプログラムの処理が停止し、その後マイコンはスリープに入り、できるだけ電源の消費を抑えつつも、後で起動することが出来るようにします。マイコンが起動すると、あなたのプログラム(code.py)が最初から実行されます。
- プログラムがlight sleepを行った場合、スリープはしますがプログラムの実行は継続されます。light sleepから抜けたときに、light sleepを行った処理の直後からプログラムの実行を再開します。消費電力は最小限に抑えられます。ただし、ESP32-S2など一部のボードでは、ライトスリープを行っても、time.sleep()を使用した場合と比較して電力が削減されない場合があります。
CircuitPythonはスリープから復帰するためにalarmを使用します。alarmは指定した時間に達したときや、ピンの状態が変化したときなどの外部イベントに基づいて発生させることができます。ピンがボタンに接続されている場合、ボタンが押されたときにsleepから抜けることができます。
alarmモジュール
alarmとsleepはCircuitPythonのalarmモジュールで利用できます。1つまたは複数のアラームを作成し、それを待つ間、light sleepまたはdeep sleepに入ることができます。
TimeAlarmを使ったlight sleep
タイムアラームを使って、10秒ごとにNeoPixelのステータスを点滅させ、その間に軽くスリープさせるだけの簡単なプログラムを紹介します。
import alarm
import board
import digitalio
import neopixel
import time
# Adafruit MagTagではNeoPixelへの電源をオンにしないといけません。
# board.NEOPIXEL_POWERを使用しないマイコンボードでは以下の2行を削除してください.
np_power = digitalio.DigitalInOut(board.NEOPIXEL_POWER)
np_power.switch_to_output(value=False)
np = neopixel.NeoPixel(board.NEOPIXEL, 1)
while True:
np[0] = (50, 50, 50)
time.sleep(1)
np[0] = (0, 0, 0)
# 今から10秒後に鳴るalarmを作成します。
time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 10)
# alarmが鳴るまでlight sleepします。
alarm.light_sleep_until_alarms(time_alarm)
# light sleep終了。ここからコードの実行が再開されます。
TimeAlarmを使ったdeep sleep
こちらは上記の例と同様のプログラムですが、light sleepの代わりにdeep sleepをするものです。deep sleepに入るとプログラムが終了し、deep sleepから復帰したときにプログラムの最初から実行されます。従って、このプログラムではwhile True:ループは存在しません。
import alarm
import board
import digitalio
import neopixel
import time
# Adafruit MagTagではNeoPixelへの電源をオンにしないといけません。
# board.NEOPIXEL_POWERを使用しないマイコンボードでは以下の2行を削除してください。
np_power = digitalio.DigitalInOut(board.NEOPIXEL_POWER)
np_power.switch_to_output(value=False)
np = neopixel.NeoPixel(board.NEOPIXEL, 1)
np[0] = (50, 50, 50)
time.sleep(1)
np[0] = (0, 0, 0)
# 今から20秒後に鳴るalarmを作成します。
time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 20)
# コードの実行を終了し、time_alarmで設定した時間までdeep sleepします。
alarm.exit_and_deep_sleep_until_alarms(time_alarm)
# deep sleepから復帰した時は、コードの最初から実行されますので、ここから下は実行されません。
PinAlarmを使ったdeep sleep
この例では、TimeAlarmの代わりにPinAlarmを使用しています。MagTagの右下にあるD11ボタンが押されるまでディープスリープします。MagTagでは、ボタンを押すとピンがグランドに接続されるので、Falseの値を待ちます。また、ボタンが押されていないときは、ピンをハイ(True)に保つためにプルアップを有効にします。
import alarm
import board
import digitalio
import neopixel
import time
# Adafruit MagTagではNeoPixelへの電源をオンにしないといけません。
# board.NEOPIXEL_POWERを使用しないマイコンボードでは以下の2行を削除してください。
np_power = digitalio.DigitalInOut(board.NEOPIXEL_POWER)
np_power.switch_to_output(value=False)
np = neopixel.NeoPixel(board.NEOPIXEL, 1)
np[0] = (50, 50, 50)
time.sleep(1)
np[0] = (0, 0, 0)
pin_alarm = alarm.pin.PinAlarm(pin=board.D11, value=False, pull=True)
# コードの実行を終了し、pin_alarmで設定ボタンが押されるまでdeep sleepします。
alarm.exit_and_deep_sleep_until_alarms(pin_alarm)
# deep sleepから復帰した時は、コードの最初から実行されますので、ここから下は実行されません。
TouchAlarmを使ったdeep sleep
この例は、Metro ESP32-S2用です。MagTagにはタッチに使用できるピンがありません。(MagTagのD10は理論的には使用可能ですが、保護部品が接続されているためタッチに使用できません)。
IO5ピンがタッチされるか、10秒経過するか、どちらか早い方までdeep sleepをします。プログラム開始時にオンボードLEDが1秒点滅するようにプログラミングしてあります。
import alarm
import board
import digitalio
import time
# シリアルコンソールにtime_alarmとtouch_alarmのどちらで起動したかを表示します。
print(alarm.wake_alarm)
led = digitalio.DigitalInOut(board.LED)
led.switch_to_output(value=True)
time.sleep(1)
led.value = False
# 今から10秒後に鳴るalarmを作成します。
time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 10)
# IO5ピンをタッチすると鳴るアラームを作成します。
touch_alarm = alarm.touch.TouchAlarm(pin=board.IO5)
# コードの実行を終了し、time_alarmで設定した時間またはtouch_sleepで設定したピンがタッチされるまでdeep sleepします。
alarm.exit_and_deep_sleep_until_alarms(time_alarm, touch_alarm)
# deep sleepから復帰した時は、コードの最初から実行されますので、ここから下は実行されません。
PCと接続している場合、実際にはsleepせず、sleepをシミュレートします
ボードをUSBでホストコンピュータに接続した時に本当にlight sleepやdeep sleepをすると、その時点でUSB接続が切れてしまい、プログラムのデバッグや編集がしづらくなるので困ってしまいます。そこで、ボードをPCと接続している場合はlight sleepやdeep sleepをシミュレートしています。CircuitPythonがスリープしたように見せかけても、USB接続を維持したままですので、消費電力を削減できる場合はそうしますが、PCと接続していない時ほどには電力を削減することはできません。
ですから、スリープ中にどれだけ電力を節約しているかを測定しようとするなら、PCと接続されていない状態でバッテリー電源(または電源)から測定することが必要です。
設定されているアラームを確認する
コードがlight sleepやdeep sleepから目覚めるのはalarmが設定されているからです。alarm.wake_alarm を見れば、どのようなアラームがプログラムを目覚めさせたのかがわかります。
もしコードがsleepから目覚めなかった場合、 alarm.wake_alarm は None になっています。
もしコードがlight sleepから目覚めた場合、 alarm.wake_alarm は alarm.light_sleep_until_alarms(…) に渡されたアラームオブジェクトのひとつになります。トリガーされたalarmもこの関数で返されるので、アラームの値を直接取得することができます。ここでは、トリガーされたalarmを取得する方法を2つ紹介します。
triggered_alarm = alarm.light_sleep_until_alarms(alarm1, alarm2)
# 上記は下記と同じです。
alarm.light_sleep_until_alarms(alarm1, alarm2)
triggered_alarm = alarm.wake_alarm
コードがdeep sleepの後に再起動した場合、 alarm.wake_alarm は元のアラームと同じタイプのアラームオブジェクトになりますが、全く同じオブジェクトではありません。属性が異なっていたり、何らかの形で不完全である可能性があります。例えば、TimeAlarmの場合、.monotonic_time属性は同じ値を含んでいないかもしれません。しかし、isinstance(alarm.wake_alarm, TimeAlarm)を実行すれば、プログラムを起動したのがTimeAlarmであることを知ることができます。
sleep_memory
コードはdeep sleepに入ると、コードの実行を終了してからスリープに入ります。そのため、変数に格納されている情報はすべて失われます。しかし、実際のコードでは何かを記憶しておき、再起動したときにその値を利用したいと思うかもしれません。
ボード上のCIRCUITPYドライブにファイルを書き込んだり、microcontroller.nvmを使って内蔵フラッシュメモリに書き込んだりすることができます。しかし、フラッシュメモリは寿命が限られているので、何度も書き込むのは避けた方がよいでしょう。代わりに、プログラムはディープスリープ中に電力を供給されるメモリの特別な部分(RAM)に書き込むことができます。多くのマイコンでは、この種のメモリは「バックアップRAM」と呼ばれています。CircuitPythonでは、alarm.sleep_memoryと呼んでいます。このメモリは維持するためにほとんど電力を必要としません。電源が完全に失われるとメモリは失われますが、USB電源や電池が接続されている限り、保存されている内容を記憶しています。
alarm.sleep_memoryは、数千バイトのバイト配列です。これを利用して好きな情報を保存することができますが、データをバイト列としてエンコードする必要があります。struct.packとstruct.unpackを使うか、JSONを使うか、その他都合の良い形式を使うことができます。
MagTagでの例
エンコードなしでsleep_memoryを使用する簡単な例を示します。プログラムが起動するたびに、alarm.sleep_memoryの1バイトに保持されているカウントを1増やします。このプログラムは、現在の電池電圧とカウントをMagTagのディスプレイに表示します。
最初にコードを実行するときに、カウントを初期化したいと思います。このプログラムが初めて実行されたかどうかは、alarm.wake_alarmを確認することで分かります。Noneであれば、まだ一度もdeep sleepしたことがないことが分かります。
以下の動画は、60秒の長いスリープを省略して、プログラムを実行しているところです。
import alarm
import microcontroller
import time
from adafruit_magtag.magtag import MagTag
magtag = MagTag()
magtag.add_text(
text_scale=2,
text_wrap=25,
text_maxlen=300,
text_position=(10, 10),
text_anchor_point=(0, 0),
)
# 一度もsleepをしていない場合、カウントをリセットします。
if not alarm.wake_alarm:
# sleep_memoryの5バイトカウンタ用に使用します。
alarm.sleep_memory[5] = 0
alarm.sleep_memory[5] = (alarm.sleep_memory[5] + 1) % 256
# 現在のバッテリの電圧を表示します。
magtag.set_text(
"battery: {}V count: {}".format(
magtag.peripherals.battery, alarm.sleep_memory[5]
)
)
magtag.refresh()
# 60秒deep sleepします。.
al = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 60)
alarm.exit_and_deep_sleep_until_alarms(al)
# deep sleepから復帰した時は、コードの最初から実行されますので、ここから下は実行されません。
消費電力の実測値
light sleepやdeep sleepを使用することで、実際にどの程度の電力を節約できるのでしょうか?使用するマイコンボード、sleepしていないときのコードの動作、light sleep中にオンにしておくもの、スリープと実行の間の時間(コードのデューティサイクル)によって異なります。
マイコンボード上にセンサーやNeoPixelなど、電源を入れたままにしておけるデバイスがある場合、ディープスリープを行っても自動的にそれらのデバイスがオフになるわけではないことに注意してください。電力消費を最小限に抑えたい場合は、コードから可能な限りデバイスをシャットダウンする必要があります。これらのオンボード・デバイスはマイクロコントローラのピンによって制御されており、ピンがオフになった結果としてオフになることもありますが、ボードによっては、ボードに電源が供給されるたびにオンボード・デバイスに電源が供給されるものもあります。
これらの要因を考慮して、この記事の「AlarmとSleep」のセクションにあるサンプルコードを使って、消費電力を見てみましょう。これらのコードは、NeoPixelを1秒間点滅させ、10秒間スリープさせるだけのシンプルなものでした。
以下に、消費電力グラフを多数掲載します。これらのスクリーンショットは、Nordic Semiconductor Power Profiler Kit II(PPK2)のハードウェアとソフトウェアを使用して撮影したものです。PPK2は、通常の電力計に比べてかなり安価(100ドル未満)で、使いやすく、よく動作します。
ESP32-S2: TimeAlarmを使ったDeep Sleepでの消費電力
MagTag ESP32-S2上で動作するTimeAlarmを使ったdeep sleepのコードが、10秒ごとにスリープしている様子を示したグラフを示します。スリープ後にプログラムが起動すると、大きなショートスパイクが発生します。NeoPixelを点滅させ、その後スリープしています。ボードには3.7Vを供給していますが、これは一般的なLiPoバッテリーの電圧です。
1サイクルのうち、プログラムがアクティブに動作している間の消費電力を拡大して見てみましょう。約50mAを消費していることがわかります。縦軸のスケールが変わっているのは、実行開始時の大きなスパイクを含んでいないためです。
このプログラムがWiFiを使用している場合、WiFiをアクティブに使用している間は数百mAまで、より高い消費電力が見られるでしょう。
次に、deep sleep時の消費電力を見てみましょう。上のグラフでは、消費電力の線が0に非常に近くなっています。ここでも縦軸が拡大されているので、マイクロアンペアを正確に見ることができます。スリープ時にボードが230uA弱の電力を消費していることがわかります。このうち約25-30uAはMagTag上の実際のESP32-S2モジュールで、残りは電圧レギュレータや(薄暗い)電源LEDなどのボードオーバーヘッドです。
ESP32-S2: TimeAlarmを使ったLight Sleepでの消費電力
比較のために、同じく10秒ごとにサイクルするTimeAlarmのlight sleepのデモプログラムのグラフを示します。ESP32-S2では、前述の通り、light sleepはtime.sleepと消費電力が変わりません。NeoPixelがONになる1秒前(マーカー1)の消費電力は約33mAです。NeoPixelをオンにすると、消費電流は約75mAに上昇します。その後、プログラムはスリープしますが(マーカー2)、消費電流はまだ約33mAです。
つまり、電力を節約する目的で、ESP32-S2でlight sleepを使う理由はあまりないのです。
ESP32-S2: PinAlarmを使ったDeep Sleepでの消費電力
それでは、ESP32-S2 MagTagでPinAlarmを使用した場合のdeep sleepの消費電力を見てみましょう。以下は、スリープの数サイクルです。それぞれの赤い矢印は、D11ボタンが押されたときを指しています。ボタンを押す間の時間が同じでないため、間隔の長さが異なっています。
以下は1つのPinAlarmスリープサイクルの開始点です。
残念ながら、ピンの変化を検出するためにオンになっている必要がある回路は、かなりの量の電流を消費します。ディープスリープ時でさえ、ボードは約1.65mAを使用しており、単にTimeAlarmを使用する場合よりもはるかに多くの電流を消費しています。
ESP32-S2: TouchAlarmを使ったDeep Sleepでの消費電力
ESP32-S2 TouchAlarmの消費電力も同様に高いです PinAlarmよりもさらに高く、約2.6mAです。ですから、繰り返しになりますが、本当にバッテリーを節約したい場合はTimeAlarmを使用してください。
Sleep時の消費電力のまとめ
さまざまな種類のアラームを使用した場合のdeep sleepの消費電力についてまとめてみます。
ESP32-S2 (MagTag) Deep Sleep
- TimeAlarm: 230uA
- PinAlarm: 1.65mA
- TouchAlarm: 2.6mA
ESP32-S2 Light Sleep
Sleep時の電流はtime.sleep()と同じなので、light sleepを使うメリットはありません。
PCに接続されている場合、CircuitPythonはsleepをシミュレートしているだけですので、商品電力の測定時はPC以外の電源と接続してください。
正確に消費電力を測定する
前述したように、どんなプログラムやボードでも、その消費電力に影響を与える可能性がある要因はたくさんあります。バッテリーがどれくらい持つか、不必要に電力を消費していないかを知りたい場合、電力計があると本当に便利です。
マイコンがPCと接続している時、CircuitPythonはsleepをシミュレートしている
あなたのボードがUSBでPCに接続されているとき、USBシリアルとマスストレージ(CIRCUITPY)への接続を切断したくないので、sleepが要求されても実際にはsleepしていないことを忘れないでください。
正しい消費電力を測定する時にはPCと接続されていない状態でテストする必要があります。