40円マイコンCH32V003で、Auto-wakeup(AWU)を使って省電力な動かし方をしてみる

## この記事は？
秋月で40円で売られているRISC-Vの8ピンマイコン **CH32V003J4M6** はAuto-wakeup（AWU）機能を備えており、低消費電力モードからの復帰を定時的にトリガーすることができます。  
これを実際に使ってみて、スリープ・復帰動作の確認と、簡易的な消費電流の測定を行います。

## 結論
* 本記事に記載するやり方により、Low-Power Modeへの切り替えおよびAWUを使用した定期的なwake-up動作を実現できた。
* Low-Power Modeへの切り替えにより、消費電流が減る様子を観察できた。~~が、データシートに期待されるほどのものは観測できなかった。Standbyに入っていなかったか、測定環境が悪かったかのいずれかが理由と思われる。~~
**1/30追記**：何かが間違っていたようで、測定しなおしたところデータシートに近い消費電流量であることが観測できた。

## 開発環境
CH32V003の開発環境として、[ch32v003fun](https://github.com/cnlohr/ch32v003fun)を使用します。

https://github.com/cnlohr/ch32v003fun

開発用のライブラリ・ツールと、一通りのサンプルソフトが準備されており、サクッと開発できます。  
ch32vfunの開発環境は以下の記事が非常に参考になります。（ありがとうございます）

https://zenn.dev/usagi1975/articles/2023-10-18-0000_ch32v003fun

https://74th.hateblo.jp/entry/using-ch32v003fun

ちなみに、auto-wakeupを利用するスリープ動作は、ch32v003fun内の[standby_autowake](https://github.com/cnlohr/ch32v003fun/tree/master/examples/standby_autowake)そのままで実現できます。  
せっかくなので今回は、マニュアルを見ながら直接レジスタを叩く実装にします。

https://github.com/cnlohr/ch32v003fun/tree/master/examples/standby_autowake

## Low-Power Modeについて

CH32V003には、`Sleep`モードと`Standby`モードの2種類のLow-Power Modeが存在します。それぞれの特徴は以下の通りです。

| モード| 供給クロック | 消費電流 （※1）| 復帰時間（※1）|復帰方法|
|:-:|:-:|:-:|:-:|:-:|
|Sleep|コアのクロック停止| 1.8-4.2mA (@ 3.3V 48MHz) | 30us  |割り込みorイベント※1|
|Standby|HSE,HSI,PLL,ペリフェラル停止|9.0-10.5uA (@ 3.3V)| 200us |イベント|

※1:CH32V003データシートより
※2:スリープに入る命令によります。`WFI`→割り込み、`WFI`→イベント

SleepモードはCPUコアのクロックのみを停止し、Standbyモードはほぼすべてのクロックを停止します。このため、消費電流は2モードで大きく差があります。

また、Sleep/Standby両モードに共通する特徴ですが、**RAMの値とI/Oポートの状態は保持されます**。また、**wakeup時、プログラムは先頭ではなく、スリープ呼び出し直後の位置から再開されます**。

## Auto-wakeup（AWU）について
Standbyモードからの定期的なwakeupに使用できる機能です。
Standbyモードではほぼすべてのクロック供給が止まってしまいますが、128kHzで駆動するLSIは止まりません。
（※ただしLSIはリセット時止まっているので、スリープ前に起動してやる必要があります。）

Auto-wakeupは、このLSIクロックを分周したクロックで駆動するカウンタによって、Standbyモードからの復帰をトリガする機能です。
カウント値が一定値に到達したらAWU（Auto-wakeup）イベントが発生し、それによってStandbyモードから復帰します。
また、分周比およびカウンタの上限値を設定することにより、sleepからwakeupまでの期間を調整することができます。

カウンタが1-64（0x00-0x3Fのレジスタ設定値で、+1した値がウィンドウサイズとして使われます）、分周が1/61440まで可能なので、最短で約7.8us、最長で約30秒のwakeup周期を設定可能です。

## 動かしてみる

AWUを利用した省電力な動作を実現するため、以下のようにコードを実装しました。
このコードを実行したところ、期待通り1.5sで文字列の出力が得られました。

なお、レジスタなどについてch32v003funのライブラリで構造体等用意されていますが、マニュアルを見て理解しながら作りたかったため、あえて直接レジスタを叩くようにしています。

```cpp
#include "ch32v003fun.h"
#include <stdio.h>

#define ADDR_PWR_CTLR ((volatile uint32_t *)(0x40007000))
#define ADDR_PWR_AWUCSR ((volatile uint32_t *)(0x40007008))
#define ADDR_PWR_AWUWR ((volatile uint32_t *)(0x4000700C))
#define ADDR_PWR_AWUPSC ((volatile uint32_t *)(0x40007010))

#define ADDR_RCC_RSTSCKR ((volatile uint32_t *)(0x40021024))
#define ADDR_RCC_APB1PCENR ((volatile uint32_t *)(0x4002101C))

#define ADDR_EXTI_EVENR ((volatile uint32_t *)(0x40010404))
#define ADDR_EXTI_RTENR ((volatile uint32_t *)(0x40010408))

#define ADDR_PFIC_SCTLR ((volatile uint32_t *)(0xE000ED10))

int main()
{
    SystemInit();

    Delay_Ms( 6000 ); // デバッガからのアクセス時間の確保

    *ADDR_RCC_APB1PCENR |= 0x10000000; //PWRモジュールへのクロック供給開始

    // AWU機能の有効化と設定
    *ADDR_PWR_AWUCSR |= 0x00000002;
    *ADDR_PWR_AWUPSC |= 0x0000000F;
    *ADDR_PWR_AWUWR |= 0x00000002;
    *ADDR_PWR_AWUWR &= 0xFFFFFFC2;

    // SLEEPDEEP = 1
    *ADDR_PFIC_SCTLR |= 0x00000004;

    // PDDS = 1
    *ADDR_PWR_CTLR |= 0x00000002;

    // EXTI9 イベントを立ち上がりエッジで有効化
    *ADDR_EXTI_EVENR |= 0x00000200;
    *ADDR_EXTI_RTENR |= 0x00000200;

    // LSI起動とクロック安定待ち
    *ADDR_RCC_RSTSCKR |= 0x00000001;
    while(((*ADDR_RCC_RSTSCKR) & 0x00000002) == 0)
    {
        // do nothing　（LSIクロック安定待ち）
    }

    while(1)
    {
        __WFE();
        printf("Wake Up!\n");
    }
```

### コード説明
コードでやっていることを、分割して説明します。

```cpp
int main()
{
    SystemInit();

    Delay_Ms( 6000 ); // デバッガからのアクセス時間の確保
```
システムの初期化をしてから、6秒間待機しています。
Standbyモードに入るコードが動き始めてしまうと、Standbyでクロックが止まっている時間が長いため、デバッガからのアクセス・プログラム書き換えが非常に困難になります。  
このため、起動直後にデバッガからアクセスできるための待機時間を設けています。

```cpp
    *ADDR_RCC_APB1PCENR |= 0x10000000; //PWRモジュールへのクロック供給開始
```
AWU機能が含まれるPWRペリフェラルへのクロック供給を開始します。これをしないと、PWR関係のレジスタへの読み書きができません。

```cpp
    // AWU機能の有効化と設定
    *ADDR_PWR_AWUCSR |= 0x00000002;
    *ADDR_PWR_AWUPSC |= 0x0000000F;
    *ADDR_PWR_AWUWR |= 0x00000002;
    *ADDR_PWR_AWUWR &= 0xFFFFFFC2;
```

1行目で、`PWR_AWUCSR`レジスタの`AWUEN`ビットに書き込み、AWU機能を有効化しています。
また、2行目でAWUに供給するクロックの分周比を3-4行目でAWUのカウンタのウィンドウ幅を設定しています。
今回は61440分周を3カウント（設定値は2）で、1.5s周期にwakeupするように設定します。

なお、`PWR_AWUWR`レジスタは**初期設定値が`0x3F`なので**、4行目のように0のビットをクリアする操作をしてやらる必要があります。

```cpp
    // SLEEPDEEP = 1
    *ADDR_PFIC_SCTLR |= 0x00000004;

    // PDDS = 1
    *ADDR_PWR_CTLR |= 0x00000002;
```
Standbyモードへの入り方としてマニュアルのP.7に記載があるとおり、`PFIC_SCTLR`レジスタの`SLEEPDEEP`ビットと、`PWR_CTLR`レジスタの`PDDS`ビットをそれぞれ1に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/5652e77c-83a8-1084-de13-70cbd169d4f0.png)

```cpp
    // EXTI9 イベントを立ち上がりエッジで有効化
    *ADDR_EXTI_EVENR |= 0x00000200;
    *ADDR_EXTI_RTENR |= 0x00000200;
```
下図（マニュアル P.34）のように、AWUのイベントは`EXTI9`に接続されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/ab054384-4444-5bf2-b0d5-17cd556e9ded.png)

このため、`EXTI9`のイベント発生有効化（`EXTI_EVENR`レジスタの設定）と、立ち上がりエッジ検出によるイベント発生の有効化（`EXTI_RTENR`）を行います。

```cpp
    // LSI起動とクロック安定待ち
    *ADDR_RCC_RSTSCKR |= 0x00000001;
    while(((*ADDR_RCC_RSTSCKR) & 0x00000002) == 0)
    {
        // do nothing　（LSIクロック安定待ち）
    }
```
128kHz LSIクロックの有効化を行います。
LSIクロックは起動時OFFで、`RCC_RSTSCKR`レジスタの`LSION`ビットを立てることでONになります。また、ON切り替え後にクロックの発振が安定すると、自動で`LSIRDY`ビットがセットされるので、値をポーリングして監視しながらそれを待ちます。

```cpp
    while(1)
    {
        __WFE();
        printf("Wake Up!\n");
    }
```
メインの動作部分です。無限ループで、`WFE`命令によるスリープ→1.5s後にAWU機能によるwakeup→文字列を出力→すぐにまた`WFE`命令でスリープ という流れになっています。  
実装が、正しい場合、1.5s周期で`Wake Up!`と出力されるはずです。

※`printf`の出力先はUART TXでも`SWDIO`でもどちらでもいいと思います。今回僕は`SWDIO`に出力して確認しました。

## 簡易的な消費電力測定

本当にStandbyモードに入って消費電力が減っているか？を確認するため、簡易的な消費電力測定を行いました。

### 測定環境
上記ソフトを書いたCH32V003を実行し、その消費電力を測定します。
消費電力はブレッドボードで以下のような回路を作り、`OUT_1`および`OUT_2`の出力電圧を、簡易オシロスコープ（PicoProbe）で測定することにより求めました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/c3a0d793-36f4-d20f-b4f4-fcc6225ef9e7.png)

抵抗部分は100Ωが2つ並列に接続されているため、50Ω抵抗とみなせます。また、この50Ω抵抗はマイコンの電源入力端子と直列に接続されています。
このため、マイコンに流入する電流と、50Ω抵抗部分に流れる電流は等しくなります。したがって、OUT_1ーOUT_2間の電圧降下と50Ω抵抗の（実際の）抵抗値から、マイコンに流れる電流を求めることができます。

※抵抗値が大きすぎた場合、抵抗での電圧降下が大きくなりすぎてしまい、マイコンに必要な電圧が供給されなくなります。データシートによると、電源電圧の最小値は2.7V、typicalの消費電流が10mA以下くらいなので、仮に10mAとして0.6(V) ÷ 0.01(A) = 60Ω よりは小さく設定するほうがよさそうです。
※50Ω抵抗を100Ω抵抗の並列で実現しているところには特別な理由はありません。単に手持ちのリード線抵抗が100Ωしかなかったためです。

### 測定結果（1回目）

本来であれば`OUT_1`と`OUT_2`は同時に測定するべきなのですが、手持ちのPicoProbeが1ch鹿測定できなかったため、電圧変動はそうないだろうという仮定の下、順番に測定します。

まず`OUT_1`を測定したところ、以下のような結果になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/2b7aece0-908d-f71d-9fde-2d5265e9ca16.png)
カーソルY1の値を読むに**3.41V**のようです。

続いて、`OUT_2`を測定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/5c97ba51-04fa-6697-1e5e-dc572e9197b6.png)

上図のように、電圧降下が1.5sごとに起きている波形が観測できました。
マイコンがStandbyから復帰し、より多く電流を消費すると、その分抵抗での電圧降下が大きくなり、`OUT_2`の電圧が低下するはずです。
1.5sごとにwakeupするレジスタの設定とも合致していることから、1.5sごとにwakeupがかかっており、あとはStandbyモードに入ることができていると判断できます。

拡大し、より詳細に電圧を測定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/e69268cf-08f8-b77e-2dde-92794354485e.png)

カーソルY1を通常実行時の、Y2をStandbyモード時の、`OUT_2`の電圧出力にあてています。それぞれの値は**3.2V**および**3.34V**です。

なお、100Ωx2の並列抵抗はテスターで測定したところ、**50.5Ω**でした。

このため、それぞれのモードにおける測定結果は下表のようになります。

|  | OUT_1[V] | OUT_2[V] | 電圧差[V] | 抵抗値[Ω] | 消費電流[mA] |
|:-:|:-:|:-:|:-:|:-:|:-:|
| 通常モード |  3.41 | 3.2 | 0.21  | 50.5  | **4.16** |
| Standbyモード | 3.41 | 3.34 |  0.07 | 50.5  | **1.39** |

通常モードに比べ、Standbyモードの消費電流は半分以下になっており、消費電流が減らせているようです。
しかし、データシートに記載の数十uA単位までは落ちていませんでした。

測定対象の電流・電圧が小さいのに加え、とりあえず近くにあるものでやや適当に測定したため、諸々（PicoProbe、抵抗、配線その他）の誤差がそのまま結果に出ている可能性が高いと思います。そもそも測定値もどこまで信用できるか謎ですし・・・・
（ただ、少なくとも消費電流が落ちているようだ、ということは言えると思います。）

また、そもそも本当にStandbyモードに入っているのか？も気になります。
観測できた事象としてはあくまで定期的な電圧降下であり、それがStandbyなのかSleepなのかはわかりません。
これについては、タイマなどHSIで動いているペリフェラルを有効にした後Standbyモードに入れ、wakeup時にペリフェラルが単独で動いていたか？を見ることにより確認できると思います。これについては、また時間があるときにやってみようと思います。

### 1/30追記：測定結果（2回目）

結果がどうにも気になったので、もう一度測定しなおしました。
今回は電源にType-Cの安定化電源を使用しました。

まず、`OUT_1`の測定結果は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/6d406e87-c7cc-d549-c86d-c8475a8eaf8f.png)

前回と同じ**3.41V**と出ました。  
安定化電源で3.3Vを入れているはずなので、オシロの誤差として100mVくらいが乗っている状態かと思います。ただ、ほかに測る手段もないためオシロの測定値を真値とします。

続いて、`OUT_2`を測定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/5bb73b6e-1029-6710-556b-ccbd9045ef40.png)


ずいぶん前回と違う波形が出ました。横軸の時間幅もms単位からus単位になっています。
1.5s周期でこの波形がトリガされていたので、やっぱりwakeup時の波形と思われます。

波形のカーソルY2の位置がLow-Power Mode時、Y1の位置が通常モード時の電圧にあっていると思われます。
それぞれ、**3.41V**および**3.21V**と出ています。
また前回と違い、それらの中間の電圧になっている時間がありました。（X1-X2カーソル間）
下図のように測ってみたところ、**3.34V**でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/0437dc85-8960-0cbe-e9fc-e849ffe2fe02.png)

X1-X2カーソル間が235usのため、これはStandbyモードからのwakeup（max300us）中であると推測します。

まとめると、以下のような結果になります。


|  | OUT_1[V] | OUT_2[V] | 電圧差[V] | 抵抗値[Ω] | 消費電流[mA] |
|:-:|:-:|:-:|:-:|:-:|:-:|
| 通常モード |  3.41 | 3.21 | 0.20  | 50.5  | **3.96** |
| wakeup中？ |  3.41 | 3.34 | 0.07  | 50.5  | **1.39** |
| Standbyモード | 3.41 | 3.41 |  < 0.01 | 50.5  | **< 0.20** |

Standbyモード時はオシロの分解能が足りず、正確な値が測定できませんでした。
が、波形を見る限りでは少なくともLow-Power Mode時の消費電流がuA単位になっており、おそらくStandbyモードには入れていると思います。

なぜこんなに違う波形になったんでしょう？
そういえば、前回測定したときはデバッガを接続して、デバッガのSWDIOにprintfしながら測定したような気がします。一方、そのことを忘れており、今回はデバッガを接続せずに測定しました。
推測ですが、「デバッガへのprintfはデバッガ接続時のみ行っている」 かつ、「デバッガ接続時は接続維持のためStandbyモードにならない」なのであれば、前回・今回両結果とも辻褄が合います。
また再現・検証してみようと思います。 


## 〆
40円マイコンのLow-Powerモードについていろいろ触ってみました。
~~少なくともStandbyモード（？）に入れることが消費電流が半分近くに落ちているので~~Standbyモードに入ることで消費電力を大きく減らすことができるので、、バッテリー駆動で何かを作るときの長寿命化に役立ちそうですね。