AE-TYBLE16 with AndroidIDEを徹底的に省電力化する(10uAでLチカ)

# 目的

https://qiita.com/KameLong/items/5e7e61f44d4e7a74539c

先日、AE-TYBLE16をArduinoIDEで使用するための記事を書きました。
今回はAE-TYBLE16(NRF51822)の醍醐味である省電力性能を追求していきます。

# 今回使用する回路構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492269/ee470684-6a7e-2515-b9fe-70d29bdeecc2.png)


前回の回路からUARTを削除し、代わりにデバッグ用LEDを追加しています。
回路図右側、GND端子の部分に100オームの抵抗を挟み、その両端の電圧をオシロスコープで測定することで、消費電流を測定します。

# iBeacon、Lチカの電力測定
## iBeacon
まずはお試しで、BLEPeriheralのiBeaconサンプルを書き込み、オシロスコープの波形を見ました。
100msごとに信号を送信するため、その瞬間に電流が跳ね上がっているのがわかります。
非送信時は500mV=5mA、送信時は1500mV=15mA　程度消費しているようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492269/fb3bee57-cb8e-7a4a-f9ce-c145978e30d1.png)

意外なんですけど、GNDで1500mV消費しているということはマイコン部分で3300mV-1500mVの1800mVしかかかっていないことになるのですが、
問題なく動作しているようです。

## Lチカ
```
void LEDsetup()
{
  pinMode(21, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(5, OUTPUT);
}
// 4bitの数値を出力します
void LEDout(int value)
{
  digitalWrite(21, (value >> 0) & 1);
  digitalWrite(4, (value >> 1) & 1);
  digitalWrite(6, (value >> 2) & 1);
  digitalWrite(5, (value >> 3) & 1);
}

void setup()
{
  LEDsetup();
}
void loop()
{
  LEDout(0b1111);//LED全点灯
  delay(500);
  LEDout(0b0000);//LED全消灯
  delay(500);
}  
```

このコードを動作したところ、CPUの消費電流は5mA程度でした。
※LEDの消費電流は今回測定しません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492269/ef8b7483-9e34-3148-cbab-b5d3b978f879.png)
データシートによると、CPUの消費電流が4.1mAと書いてあるので妥当な値ですね。


# 本番：電流削減するために悪戦苦闘

## スリープを試してみる
CPUがON状態だとそれだけで4mA消費してしまうので、まずはスリープを試してみます。
スリープから復帰する方法は後回しにして、とりあえずスリープ時の電流を極限まで減らしてみます。
### ①愚直にWaitForEvent命令挿入
```
void setup()
{
  LEDsetup();
  LEDout(0b1111);//LED全点灯
  delay(500);
  LEDout(0b0000);//LED全消灯
  delay(500);
  __WFE(); //①スリープ命令(wait for event)挿入
}
```
これで1.4mAまで下がりました。
### ②デバッグモードを解除する
参考：[BLE Nano (nRF51822) でどうしても 1mA 以上電流食うぞというとき](https://lowreal.net/2016/08/25/2)

ST-LINKで書き込んだ直後はデバッグモードで動いているようです。
この状態だと追加で電流を消費するので、再起動（STLinkを抜き差し）してリセットをかけます。
これで0.5mAまで下がりました。
### ③HFCLKを停止する
残りの500uAですが、おそらく32Mhzのcrystal oscillatorによるものだと思います。
スリープ時にはHFCLKを止めて、LFCLK(32kHz)のみにしてしまいましょう。

この部分を削るためには、Arduino部分のソースも書き換える必要があります。

C:\Users\kamelong\AppData\Local\Arduino15\packages\sandeepmistry\hardware\nRF5\0.7.0\cores\nRF5\main.cpp
を書き換えます。
```
int main( void )
{
  // init();
  // initVariant();
  // delay(1);
  setup();
  for (;;)
  {
    loop();
    // if (serialEventRun) serialEventRun();
  }
  return 0;
}
```
init()でクロックの指定をしているのでコメントアウトします。
ここをコメントアウトすると、delay関数が使えなくなってしまうので、delayを使う可能性がある場所もコメントアウトしましょう。

setup()関数でもdelayを使っています。
こちらは代わりに自作のでDelay関数を作ってそちらに置き換えます。
```
void LEDsetup()
{
  pinMode(21, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(5, OUTPUT);
}
// 4bitの数値を出力します
void LEDout(int value)
{
  digitalWrite(21, (value >> 0) & 1);
  digitalWrite(4, (value >> 1) & 1);
  digitalWrite(6, (value >> 2) & 1);
  digitalWrite(5, (value >> 3) & 1);
}

// delay関数が使えなくなるので
//だいたい指定した時間待ってくれる自作のdelay
void myDelay(int ms){
  for(int i=0;i<ms;i++){
    //1300は適当
    for(volatile int j=0;j<1300;j++){

    }
  }

}

void setup()
{
  LEDsetup();
  LEDout(0b1111);//LED全点灯
 myDelay(500);
  LEDout(0b0000);//LED全消灯
  myDelay(500);
  __WFE(); //①スリープ命令(wait for event)挿入
}
void loop()
{
    //loopには入らないはずなので、LEDは再点灯しないはず
  LEDout(0b1111);//LED全点灯
  
}  
```
これで実行すると、
およそ1mVまで電圧が下がりました！！
10uA相当まで下がっています

ノイズに埋もれている感じだったので、10kΩの抵抗に差し替えて実行してみます。
マイコンが起動するまではGNDピンとGNDをジャンパ線でショートさせ、スリープしたらジャンパ線を外し10kΩに電流を流します。
その結果は、10kΩで20mV　つまり2uAまで消費電流が下がっていることがわかりました。

### delayで使用するクロックを内部RCクロックに変更する
なぜHFCLKが有効になるのか調べたところ、
Arduinoで使用するLFCLKがクロックがHFCLKから生成されているからでした。
ArduinoIDE側からこの設定は変更することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492269/ecffc8bb-d057-15f2-86c9-8e6dd1c4a323.png)
設定からRC Oscillatorを選択すると、HFCLKを使わずにArduinoのdelay関数を動かすことができるようになります。
main.cppをもとに戻してから、コンパイルしなおすと、スリープ時に3uA程度の消費電流になりました。
データシートより、I_RC32kは0.8uA消費するので、妥当な値だと思います。


## スリープからの復帰方法の模索
スリープから復帰するためには、割り込みイベントを起こす必要があります。

今回はLチカをしたいので、タイマー割り込みを使用します。
しかし、nRF51のRTCのうちRTC0はSoftDeviceで、RTC1はArduinoのdelay関数で使用しており、空いているRTCがありません。

SoftDevice側を犠牲にするわけにはいきませんので、必然的にArduinoのdelay関数で使われるRTC1を使用することになります。
delay関数も取っておきたいので、Arudino側の関数と整合性を取りながら改造することになります（難しい…）。

### ArduinoでのRTC1の使われ方
wiring.cのinit関数内を見ると、
```
  NRF_RTC1->PRESCALER = 0;
  NRF_RTC1->INTENSET = RTC_INTENSET_OVRFLW_Msk;
  NRF_RTC1->EVTENSET = RTC_EVTEN_OVRFLW_Msk;
  NRF_RTC1->TASKS_START = 1;
```
とあります。この記述と、nRF51_RM_v3.0データシートと照らし合わせると、LFCLKの分周はなし（PRESCALER = 0)、
overflow時のイベント割り込みのみ許可されています(RTC_INTENSET_OVRFLW_Msk,RTC_EVTEN_OVRFLW_Msk)。


LFCLKは32768Hz=2^15Hzで動いており、カウントレジスタは24bitなので、この設定だと2^9秒=512秒ごとにoverflowイベントが走ります。

次に、delay.cを見ると、
```
static volatile uint32_t overflows = 0;
uint32_t millis( void )
{
  uint64_t ticks = (uint64_t)((uint64_t)overflows << (uint64_t)24) | (uint64_t)(NRF_RTC1->COUNTER);
  return (ticks * 1000) / 32768;
}
```
とあります。このticksが32768Hzでカウントアップされる数値ですね。
overflowsがこれまでオーバーフローした回数、NRF_RTC1->COUNTERが現在のカウンターの値です。

overflowの検知は
```
void RTC1_IRQHandler(void)
{
  NRF_RTC1->EVENTS_OVRFLW = 0;
#if __CORTEX_M == 0x04
    volatile uint32_t dummy = NRF_RTC1->EVENTS_OVRFLW;
    (void)dummy;
#endif
  overflows = (overflows + 1) & 0xff;
}
```
この部分で行っているようです。
このRTC1_IRQHandlerという関数はアセンブラ(gcc_startup_nrf51.S)で割り込み関数に割り当てられています。

### 100ms毎に割り込みを発生させる
それではこのタイマーをいじって、100msごとに割り込みを発生させていきます。
方針として、タイマーカウンターの比較一致割り込みを使います。
RTC1のCC0レジスタに3276(32768/10)を入れ、カウンターがこの値になると割り込みが発生するよう、
INTENSETレジスタに0x00010000
EVTENSETレジスタにも0x00010000を入力します

wiring.cのコードは
```
//wiring.c
  NRF_CLOCK->TASKS_LFCLKSTART = 1UL;
  NRF_RTC1->PRESCALER = 0;
  NRF_RTC1->CC[0]=3276;//100ms秒おき
  //  NRF_RTC1->INTENSET = RTC_INTENSET_OVRFLW_Msk;
  NRF_RTC1->INTENSET =RTC_INTENSET_COMPARE0_Msk;
  //  NRF_RTC1->EVTENSET = RTC_EVTEN_OVRFLW_Msk;
  NRF_RTC1->EVTENSET = RTC_EVTEN_COMPARE0_Msk;
```
このようになります。

次に、delay.cも修正します。
100msごとにoverflowsがカウントアップされるので、
```
//delay.c
uint32_t millis( void )
{
  uint64_t ticks = (uint64_t)(uint64_t)overflows*3276 + (uint64_t)(NRF_RTC1->COUNTER);

  return (ticks * 1000) / 32768;
}
```
RTC1_IRQHandlerは自分のソース側で使用したいので、
削除します。代わりにoverflowsをインクリメントする関数を用意します。
```
//delay.c
// void RTC1_IRQHandler(void)
// {
//   NRF_RTC1->EVENTS_OVRFLW = 0;
// #if __CORTEX_M == 0x04
//     volatile uint32_t dummy = NRF_RTC1->EVENTS_OVRFLW;
//     (void)dummy;
// #endif
//   overflows = (overflows + 1) & 0xff;
// }
void overflow(){
    overflows = (overflows + 1) ;
}
```
delay.hに忘れずにoverflow()の宣言を追加しておきましょう。

最後に本体のソースコードに、RTC1_IRQHandlerを追加します。

```
void LEDsetup()
{
  pinMode(21, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(5, OUTPUT);
}
// 4bitの数値を出力します
void LEDout(int value)
{
  digitalWrite(21, (value >> 0) & 1);
  digitalWrite(4, (value >> 1) & 1);
  digitalWrite(6, (value >> 2) & 1);
  digitalWrite(5, (value >> 3) & 1);
}
void setup()
{
  LEDsetup();
}
void loop()
{
    __WFE(); //①スリープ命令(wait for event)挿入
}  
extern "C" {
int k=0;//LEDのカウントアップ
void RTC1_IRQHandler(void)
{
  NRF_RTC1->TASKS_CLEAR = 1;
  NRF_RTC1->EVENTS_COMPARE[0] = 0;
  overflow();
  k++;
  LEDout(k);
}
}
```
これで完成です。

書き込むと100msごとにLEDの表示がカウントアップしていきます。

消費電流も測定しました。
使用した抵抗は100オームです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492269/0f33256d-79d8-6cd6-147e-bce8e2e5572f.png)

オシロスコープで測るとこのような波形が100msごとに繰り返しています。
ピーク時で200mV=2mA程度の消費のようです。
横軸は1メモリ100usなので200us程度で消費電流が
落ちていることがわかります。
積分するとおよそ0.75msmA程度でしょうか
スリープ状態だとおよそ3uA程度の消費電流なので
平均すると、3uA*100ms+750uAms=1050uAms 10uA程度の消費電力になりそうです。