目的
先日、AE-TYBLE16をArduinoIDEで使用するための記事を書きました。
今回はAE-TYBLE16(NRF51822)の醍醐味である省電力性能を追求していきます。
今回使用する回路構成
前回の回路からUARTを削除し、代わりにデバッグ用LEDを追加しています。
回路図右側、GND端子の部分に100オームの抵抗を挟み、その両端の電圧をオシロスコープで測定することで、消費電流を測定します。
iBeacon、Lチカの電力測定
iBeacon
まずはお試しで、BLEPeriheralのiBeaconサンプルを書き込み、オシロスコープの波形を見ました。
100msごとに信号を送信するため、その瞬間に電流が跳ね上がっているのがわかります。
非送信時は500mV=5mA、送信時は1500mV=15mA 程度消費しているようです。
意外なんですけど、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の消費電流は今回測定しません。
データシートによると、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 以上電流食うぞというとき
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側からこの設定は変更することができます。
設定から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オームです。
オシロスコープで測るとこのような波形が100msごとに繰り返しています。
ピーク時で200mV=2mA程度の消費のようです。
横軸は1メモリ100usなので200us程度で消費電流が
落ちていることがわかります。
積分するとおよそ0.75msmA程度でしょうか
スリープ状態だとおよそ3uA程度の消費電流なので
平均すると、3uA*100ms+750uAms=1050uAms 10uA程度の消費電力になりそうです。