概要
ESP-WROOM-02のコアにあたるESP8266EXには、3種類のスリープモードがあるが、今回はGPIOにつないだスイッチの操作によってスリープ状態 (light-sleep) から復帰できるでしょうか、という確認を行った。
データシート (ESP8266 Datasheet version 4.4, August 1, 2015 ) によれば、スリープモードには3種類あり、スリープ時の消費電力が最小なのはdeep-sleepモードとある。deep-sleepモードに入った場合、外部から復帰させるためにはRSTピンに作用する必要があり、リセットと見分けるために外付け回路が必要になるだろう。
このモードの用途としては、一定の時間間隔で目覚めて、何かやって、また寝るといった繰り返し用途が分かりやすい(温度/湿度の計測で使った)。
次に消費電力が少ないのがlight-sleepモードで、通信が行われないときにはWiFi用のRF回路をオフにするとともにCPUの実行をサスペンドし、約0.9mA程度の消費電力になるという。ただし、WIFI_STA mode (ステーションモード) でのみ有効。
もう一つがmodem-sleepモードだが、これはCPUの動作を維持したまま、非通信時にWiFi用のRF回路をオフにして電力を抑えるというもので、約15mA消費すると書いてある。
今回は、GPIOピンに接続したタクトスイッチの操作によってlight-sleepモードの開始と終了ができるかどうかを試すことにした。
回路
以下のような回路にした。
IO0、IO2とRSTにタクトスイッチを接続し、いずれも押下時にGNDに接続する。
※ 当初の投稿では、回路図に間違えがあったので修正しました。確認しながらやってますが、たまに大きな間違いがあります。掲載している回路を作ってみる際には、確認をお願いします。
今までの回路から宗旨替え(?)して、ブートモードを指定するGPIOポートもアプリケーションに使うことにした。
上の回路では、SW1を押しながらRESETボタンを押すとUART ダウンロードモードとなるし、SW1に触らずにRESETを押したり電源を投入すると、フラッシュブートモードになってスケッチが走り出す。ダウンロードモードにする際には、SW1を押したままにしてちょっと早くRESETボタンを離すのがコツ。
今さらだけど、ブートモードについての復習。
UARTダウンロードモードとフラッシュブートモード
当初から参考にしてきた、Espressif Systemsの ESP8266EX Hardware User Guide (Version1.1, Jun. 1. 2015) には以下のように書いてあった。
By default the Flash is empty. Therefore, the following procedures should be followed when you burn the programs into the Flash:
• Before burning, set the module to work under UART Download mode;
• Pull IO15 and IO0 to low-voltage level, leave IO2 dangled;
• Burning the programs into Flash;
• Burning the program into Flash using flash downloading tools;
• After burning the programs into Flash, pull down IO15 to low-voltage level, keep IO2 dangled, and pull up IO0 to high-voltage level. The module is then shifted from UART Download mode to the Flash Boot mode;
• The initialisation would read and run programs from Flash after it is powered on.
(関連するドキュメント類は https://espressif.com/en/support/download/documents から入手できる。上に掲げた文書はすでに見当たらないが、改訂版と思われる ESP8266 System Description に同じ内容が含まれている。)
この文章で気になるのは、IO2をdangledにしておけという記述で、素直に読めばブラブラにしておけ、つまり何もつなぐな、ということではないかな。
今まで作ったつたない回路では、IO0によってブートモードの切替えを行い、IO15はプルダウンしたまま使ってきている。問題のIO2はおおむね未接続のまま使っているが、先日作った赤外線受光モジュールはIO2に接続した。このモジュールはアクティブL出力なので、ブート時にはHレベルとなっていたはず。
試しにIO2をGNDに接続してリセットし、スケッチのダウンロードを試みると、
warning: espcomm_sync failed
error: espcomm_open failed
error: espcomm_upload_mem failed
となってダウンロードは失敗した。同様に、この状態でIO0をプルアップしてリセット(フラッシュブートモード)してみても、スケッチは開始しなかった。
つまり、リセット時にUARTダウンロードまたはフラッシュブートモードでブートしたいときには、IO2はHレベル(プルアップ)またはNCとしておく必要があるようだ。
今回の回路では、ブートモード指定にも用いるピンの役割を以下のようにした。
- IO0とIO2
入力用として設定し、常時HアクティブLとなるようなスイッチ操作の検出用。 - IO15
出力用として設定し、プルダウン抵抗と並列にLED(および抵抗)を接続した。HIGHの出力で点灯する。
スケッチ
以下のように動作する。
- delay()を入れたloop() により、約1.2秒間隔でLEDが点滅する。
- IO0に接続したSW1を押すとスリープ状態に入り、もう一度押すと復帰する。
- IO2に接続したSW2を押すと、非接続ならばアクセスポイントに接続し、接続中なら切断する。なお、スリープ中に押すと、スリープから復帰する。
WiFiアクセスポイントへの接続は、テスターで電流を測りたかったことと、スリープへの影響を見たかったから。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
// WROOM02-LED-1.ce3用 #include <ESP8266WiFi.h> extern "C" { #include "user_interface.h" #include "gpio.h" } #define START_MSG String(__FILE__) + " start." #define RED_LED 15 #define SW1 0 #define SW2 2 const char* ssid = "ssid"; const char* password = "password"; void ap_connect() { digitalWrite(RED_LED, 0); Serial.println("\nstart ap_connect()"); WiFi.mode(WIFI_STA); wifi_station_set_auto_connect(false); WiFi.begin(ssid, password); int n; while ((n = WiFi.status()) != WL_CONNECTED) { Serial.print(String(n) + " "); delay(500); if (n == WL_NO_SSID_AVAIL || n == WL_CONNECT_FAILED) WiFi.reconnect(); } Serial.println("\nap_connect() done."); } void ap_disconnect() { if (WiFi.status() != WL_CONNECTED) return; WiFi.disconnect(); int n; while ((n = WiFi.status()) != WL_IDLE_STATUS) { Serial.print(String(n) + " "); delay(100); } Serial.println("ap_disconnect() done."); } void setup() { Serial.begin(115200); delay(10); Serial.println("\n" + START_MSG); WiFi.mode(WIFI_STA); pinMode(RED_LED, OUTPUT); digitalWrite(RED_LED, 0); pinMode(SW1, INPUT); pinMode(SW2, INPUT); } void fpm_wakup_cb(void) { Serial.println("fpm_wakup_cb start"); gpio_pin_wakeup_disable(); wifi_fpm_close(); // disable force sleep function } void start_light_sleep() { Serial.println("start_light_sleep() start."); digitalWrite(RED_LED, 0); ap_disconnect(); WiFi.mode(WIFI_OFF); wifi_set_opmode_current(NULL_MODE); wifi_fpm_set_sleep_type(LIGHT_SLEEP_T); wifi_fpm_open(); gpio_pin_wakeup_enable(0, GPIO_PIN_INTR_LOLEVEL); gpio_pin_wakeup_enable(2, GPIO_PIN_INTR_LOLEVEL); wifi_fpm_set_wakeup_cb(fpm_wakup_cb); // Set wakeup callback wifi_fpm_do_sleep(0xFFFFFFF); // sleep until gpio activity. delay(10); Serial.println("start_light_sleep() wake up."); } void loop() { static int counter = 0; delay(400); if (counter++ % 3 == 0) { digitalWrite(RED_LED, digitalRead(RED_LED) ^ 1); Serial.print("."); } if (digitalRead(SW1) == LOW) { Serial.println("\nsw1_pushed"); start_light_sleep(); } else if (digitalRead(SW2) == LOW) { Serial.println("\nsw2_pushed"); if (WiFi.status() != WL_CONNECTED) ap_connect(); else ap_disconnect(); } } |
スケッチについて
※ light-sleepへの入り方や復帰については、ESP8266 SDK API Guide. Version 1.5.4, 2016.04 を参考にした。ただ、Example として示されているコードではうまくいかず、上記のようになった。
void setup()
I/Oピンのモード指定など。
bool ap_connect()
WiFiアクセスポイント接続用の関数。相変わらずアクセスポイントにつながり難い場所なので、WiFi.reconnect() を挟んでいる。
light-sleep 時にWiFiを無効にすることから、明示的に WiFi.mode(WIFI_STA); を使ってステーションモードとしている。
void ap_disconnect()
WiFiアクセスポイントからの切断用。わざわざステータスを見ている理由については後述。
void start_light_sleep()とvoid fpm_wakup_cb(void)
light-sleepの開始と終了時の後始末のための関数。
start_light_sleep()では、まずWiFiの切断と利用を禁止し、その後スリープに関わる操作を行っている。
- wifi_fpm_set_sleep_type();
スリープモードを指定する。wifi_fpm_open()の前に実行する。 - wifi_fpm_open();
プログラムによるスリープの許可。 - gpio_pin_wakeup_enable(pin_no, GPIO_PIN_INTR_LOLEVEL);
IO0とIO2のいずれかがLOWになったらウェークアップするように、2行書いている。試してみたらいずれのスイッチを押してもスリープから復帰した。
おそらく、ligh-sleep復帰専用のgpio割込みハンドラを割り当てるのだろう(想像ですが)。 - wifi_fpm_set_wakeup_cb(cb_func);
ウェークアップ時のコールバック関数を指定。コールバック関数は、スリープに関わる後始末専用と考えた方が良さそうだった。 - wifi_fpm_do_sleep(0xFFFFFFF);
スリープ開始。引数の0xFFFFFFF により、タイマーではなくGPIO入力の変化によってウェークアップする。
そうでない場合、ウェークアップまでの時間(μ秒単位)を指定するらしい(試していない)。 - delay(10);
実際のスリープが開始するのは、wifi_fpm_do_sleep()の呼出し後、システムがアイドル状態に入った時点ということなのでdelay()を入れた。 - gpio_pin_wakeup_disable()
fpm_wakup_cb() で必ずやらなければならないことは、このAPIの呼び出しにより、GPIOピンをlight-sleep復帰目的から切り離すことだった。これを呼ばないと、スリープ復帰後にIOピンが当初の目的を果たさなくなった。 - wifi_fpm_close()
wifi_fpm_open(); と対になっていて、プログラムによるスリープ動作を禁止する。なお、wifi_fpm_do_wakeup(); は不要。 - fpm_wakup_cb()内でap_connect() を呼んでみたら、Exception (9)が起きて落ちてしまう。コールバック関数内にはlight-sleepからの復帰に必要なこと以外は書かない方が良さそうである。
ap_disconnect() でステータスを見ている理由
当初は、start_light_sleep() 内に WiFi.disconnect(); と書いていたのだが、なぜか wifi_fpm_do_sleep() の後のdelay() を抜けて、シリアルモニタ出力の後でlight-sleepに突入することが多かった。もしかしたら loop() まで戻っていたかもしれない。
こういう動きをする理由を考えてみると、WiFi.disconnect() ( wifi_station_disconnect() ) は、切断の開始から切断ステータスの確認までを実施する処理をキックするとすぐに制御を戻すように思える。WL_IDLE_STATUS になるまでにフォアグラウンド側の処理はどんどん進み、wifi_fpm_do_sleep() でのlight-sleep開始指示もペンディングされたままになるんじゃないかな。
想像の正しさは分からないが、少なくとこのようなap_disconnect() 関数を用意することで、wifi_fpm_do_sleep() 直後の delay() でスリープ状態に突入するようになった。
きょうのまとめ
スリープから復帰するためのソースとしては、スイッチの他にセンサー出力が考えられる。また、複数のGPIOピンを復帰に使えたことから、いろいろなソースの信号をワイヤードORしてレベルセンスするような必要もないだろう。
安いテスターでもってブレッドボードの+3.3VとWROOM-02の3V3の間の電流値を読んでみたところ、以下のような値を示していた。
- スケッチ開始時: 70mA前後
- APに接続試行中: 80mA
- AP接続後: 45~70mA
- AP切断後: 70mA前後
- light-sleep中: 1mA
精度は低いが、データシートに書かれているのとおおむね近い値が読み取れた。WiFi接続時は明示的な通信は行っていない。
今回載せたスケッチでは、LED用のIO15をLOWにしてからスリープに入っているが、62行目を digitalWrite(15, HIGH); としてやると、スリープ中もLEDは点灯したままになる(当然ながら、3V3に流れる電流は約3mAになった)。つまり、出力ポートの状態は維持されるようである。
light-sleepは外部から簡単に復帰可能でプログラム状態もポート状態も維持可能なスリープモードではあるのだけど、果たして待機時1mAでオーケーですか?、というのがポイントか。