Seeed Studio XIAO ESP32C3の省電力運用について考える

## はじめに
この記事は[TDU CPSLab Advent Calendar 2022](https://adventar.org/calendars/7417)の19日目の記事になります。
１８日目の記事：[nanoFrameworkでNode-REDにMQTT](https://adventar.org/calendars/7417#:~:text=https%3A//zenn.dev/matsuataru/articles/0237a66beb73db)
２０日目の記事：TODO

まず、お前は誰だ？という疑問が出てくる人がいると思うので自己紹介です。
2012年入学して2018年に修士課程を卒業したのCPSLab OBの人です。主にハードウェアを色々いじって夜な夜なラボに大量にある基板からガジェットを作ってい遊んでいました。

研究をしていく中で、屋外設置の装置を作る場合が出てくると思います。長期間設置したくて、さらに恒常電源を引いてくることができない場合には省電力化とバッテリー運用がキーになってきます。

そこで今回は趣味でも仕事でもこれから使っていきたいと思っているSeeed Studio XIAO ESP32C3(以後XIAO)を使って省電力運用が可能かの検討をしていきます。

## Seeed Studio XIAO ESP32C3って？

Seeed Studio社が出している14ピンマイコンボードシリーズのうちの一つです。

https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/#hardware-overview

このシリーズにはラズベリーパイの一種である`RP2040`や、BLEが使えるマイコンである`nRF52840`などが搭載されたものがあります。

この中でも今回取り上げる`ESP32C3`マイコンが乗ったXIAOは個人的には激推しになっています。
`無印のESP32`マイコンではプログラムの書き込みにはUSB-シリアル変換チップが必要でしたが、`ESP32C3`マイコンはコアがRISC-Vになることで直接USB端子からプログラムの書き込みができるようになっています。
これによって`ESP32 Dev-kit`や`M5Stack`などの開発ボードではプログラム側をどう工夫してもスリープ時に**数mA~数十mA**あった消費電流がかなり抑えられます。
他にこのボードの特徴として、このボードはWiFi対応、Bluetooth5(LE)の他に**基板裏面にバッテリー用のパッドがあり、リチウムイオン電池のチャージコントロールICとレギュレータが載っている**事が挙げられます。
<img width="300" src="https://files.seeedstudio.com/wiki/XIAO_WiFi/back-label-6.png">


この特徴から、リチウムイオン電池を接続するだけで動作ができて、さらにUSB端子からバッテリーを充電できるすぐれものです。
この基板がスリープで長期運用に耐えられるのかの調査をしていきます。

ちなみに、私はこれまでは無印のESP32モジュールを使いいろいろな開発を行ってきましたが、今回のXIAOに搭載されているESP32C3を触るのは初めてです。


## XIAOのDeepSleepの理論値
まずはスペック上の消費電力理論値を大雑把に出してみましょう
[ESP32C3 Series - Datasheet - Espressif Systems](https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf)によると、DeepSleep時の消費電流は`5uA`

単体でXIAOを動かしたときに他に電力消費のあるパーツは回路図を見ると、ESP32C3向けの電源を作っているレギュレータ(XC6210B332MR)とその周辺回路であることがわかります。
このパーツのデータシートを見ると、消費電流は`35uA`
![スクリーンショット 2022-12-19 18.14.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107175/53379919-16eb-b390-e441-0b19ffa7babb.png)

これらを合計すると理論値は、`5uA + 35uA = 40uA`となります。

## 実験装置
実験装置はXIAOの裏側のパッドにPHコネクタが接続できるようにパーツをはんだ付けしたものを使います。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107175/4acbd440-16b6-7edf-1ee9-2dfdef212433.png">
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107175/f3b031bd-da34-0080-3a6f-c133437efd21.png">
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107175/53965e88-6138-1f50-12cd-336aae277446.png">

PHコネクタの先に安定化電源をつないで3.7Vを供給し、この電源ライン上に入れた電流計で計測します。(レギュレータの動作電圧は1.50V～6.00V)
プログラムを書き込むときにはUSB経由で書き込みますが、USBが接続されていると電源供給がUSB側に切り替わってしまう回路が組まれているため、**計測時はUSBケーブルは外します。**
デバッグには外付けのUSBシリアル変換基板をD6、D7に接続してUART出力を読み取ります。
USBシリアル変換基板との接続は以下になります。
| XIAO | USBシリアル変換基板 |
|:-:|:-:|
|  GND |  GND |
|  D6 | Rx  |
| D7  | Tx  |


## 実験１：普通にDeepSleep

起動して10秒経過後にdeepsleepに入り１０秒後に再起動するプログラム



```c++:
void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial.println("Start!!");
  delay(10000);
  Serial.println("Go to DeepSleep!!");
  esp_deep_sleep(1000 * 1000 * 10);  //10sec sleep
}

void loop() {
  // put your main code here, to run repeatedly:
}
```

<font color="red">**※注意点**
CPUがUSB経由でのプログラムのアップロード機能を担っているため、スリープ中はUSBがつながっていてもシリアルポートが認識されなくなるようです。
なので、通常起動している時間をあまりにも短くしすぎるとプログラムのアップロードがとても大変になります。今回のプログラムでは10秒のディレイを確保しています。
(何故かESP32のときみたいにBOOTボタンを押しながらリセットボタンを押すことでプログラム書き込みモードに入る機能が動作できなかった...)
</font>

### 実験結果
| スリープ前 | スリープ中 |
|:-:|:-:|
|  18.63mA | 40.9uA  |

ほぼ理論値通りの結果が得られる事がわかった

## 実験2：ADCを使った後にDeepSleep
無印のESP32では`analogRead()`関数を使った後にスリープすると内部のADCが起動したままになっていて`2mA`ほどスリープ中でも消費してしまう問題がありました。
これを回避するためには、スリープさせる前に`adc_power_off()`を行いADCを止める必要がありました。
このちょっとしたハマりどころで、なぜか想定通りのスリープ性能が出ない不具合に遭遇した経験があるので、ESP32C3でこの仕様がどうなっているか確認していきます。

```c++:
void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial.println("Start!!");
  for (int i = 0; i < 15; i++) {
    Serial.printf("%d analog :%d\n", i, analogRead(0));
    delay(1000);
  }
  Serial.println("Go to DeepSleep!!");
  esp_deep_sleep(1000 * 1000 * 10);  //10sec sleep
}

void loop() {
  // put your main code here, to run repeatedly:
}
```


### 実験結果
| スリープ前 | スリープ中 |
|:-:|:-:|
|  18.57mA | 40.5uA  |

実験1とほぼ同じ結果が得られたので特に対策はしなくても良さそうです。


## スリープからの復帰

https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32c3/api-reference/system/sleep_modes.html

あまり情報がないので、公式リファレンスを読み解いていきます。
ディープスリープからの復帰に使えそうな関数は

1. [void esp_deep_sleep(uint64_t time_in_us)](https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32c3/api-reference/system/sleep_modes.html#_CPPv414esp_deep_sleep8uint64_t)
2. [esp_err_t esp_deep_sleep_enable_gpio_wakeup(uint64_t gpio_pin_mask, esp_deepsleep_gpio_wake_up_mode_t mode)](https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32c3/api-reference/system/sleep_modes.html#_CPPv433esp_deep_sleep_enable_gpio_wakeup8uint64_t33esp_deepsleep_gpio_wake_up_mode_t)

1はすでに実験で使っているもの。この関数を使う前に明示的にWiFi/BTを止めてからでないとスリープできないらしい。
2ではスリープ復帰に使いたいGPIOの設定をすることで、この変化で起動ができるようです。
これらを踏まえて、任意のGPIOの変化があった際にスリープから復帰するサンプルを作ってみます。

```c++:
#include "esp_bt_main.h"
#include "esp_bt.h"
#include "esp_wifi.h"

RTC_DATA_ATTR int counter = 0;  //RTC coprocessor領域に変数を宣言することでスリープ復帰後も値が保持できる

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial.printf("Cause:%d\r\n", esp_sleep_get_wakeup_cause());
  wakeup_cause_print();
  Serial.printf("Counter:%d\r\n", counter++);
  Serial.println("Start!!");
  for (int i = 0; i < 15; i++) {
    Serial.printf("%d analog :%d\n", i, analogRead(0));
    delay(1000);
  }
  Serial.println("Go to DeepSleep!!");
  esp32c3_deepsleep(10, 2);  //10秒間スリープ スリープ中にGPIO2がHIGHになったら目覚める
}

void loop() {
  // put your main code here, to run repeatedly:
}

void esp32c3_deepsleep(uint8_t sleep_time, uint8_t wakeup_gpio) {
  // スリープ前にwifiとBTを明示的に止めないとエラーになる
  esp_bluedroid_disable();
  esp_bt_controller_disable();
  esp_wifi_stop();
  esp_deep_sleep_enable_gpio_wakeup(BIT(wakeup_gpio), ESP_GPIO_WAKEUP_GPIO_HIGH);  // 設定したIOピンがHIGHになったら目覚める
  esp_deep_sleep(1000 * 1000 * sleep_time);
}

void wakeup_cause_print() {
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch (wakeup_reason) {
    case 0: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_UNDEFINED"); break;
    case 1: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_ALL"); break;
    case 2: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_EXT0"); break;
    case 3: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_EXT1"); break;
    case 4: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_TIMER"); break;
    case 5: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_TOUCHPAD"); break;
    case 6: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_ULP"); break;
    case 7: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_GPIO"); break;
    case 8: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_UART"); break;
    case 9: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_WIFI"); break;
    case 10: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_COCPU"); break;
    case 11: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG"); break;
    case 12: Serial.println("Wakeup caused by ESP_SLEEP_WAKEUP_BT"); break;
    default: Serial.println("Wakeup was not caused by deep sleep"); break;
  }
}
```

GPIO2をHIGHにしたときにスリープから復帰することが確認できました。
wakeup_causeがタイマーとGPIOそれぞれに対応していることがわかります。
また、メインCPUのメモリ上に宣言した変数はDeepSleepから復帰した際に内容が揮発してしまうので、RTC fast memoryという領域に`counter`を宣言することで、電源を入れてからのスリープ復帰回数をカウントしています。

## まとめ
XIAOのDeepSleep機能を使うことで40uAで動作できることがわかった。
スリープからの復帰もタイマーとGPIO変化が使える。
ただし、スリープ中にプログラムを書き込む方法を模索する必要がありそう。
とても、省電力でバッテリーポン付けで使えるのでいろいろなガジェットに組み込んでいきたい。

## 参考

https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32c3/api-reference/system/sleep_modes.html#_CPPv418esp_sleep_source_t

https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/#hardware-overview