Bluetooth接続のキーボードやIoTセンサーの送信機を作りたくて、XIAO BLE nRF52840というマイコンモジュールを買いました。実はRP2040と間違えて買ってしまったんですが、結果的に省電力のnRF52840が乗ったマイコンに出会えて良かったです。XIAO BLE nRF52840は秋月電子やスイッチサイエンスさんで購入できます。
今まではマルチメーターで電流を測定していたのですが、もっと正確な測定がしたいと思い、Nordic SemiconductorのPower Profiler Kit IIというデバイス(PPK2)を購入しました。PPK2はPC上で電流値をモニターすることができます。
PPK2が素晴らしいところが、2種類の電流測定モードがあり、外部電源と測定対象の間にPPK2を挟んで測定する方法と、PPK2自身から電源を供給する方法があります。これ滅茶苦茶便利です。あとまだ使ったことはありませんが、8chの簡易ロジアナ機能があるので、特定のピンの状態を見ながら比較するなんてこともできそうです。
追記:1桁μAのDeep Sleep方法を追記しました。
電流測定
コイン電池CR2032で駆動することを想定して開発を行っているので、PPK2の電圧設定は3000mVにしました。PPK2の電源端子はXIAO BLE nRF52840のBAT+ BAT-に接続しています。逆流防止の機構は入れてないので、USBに繋いでプログラムを書き込むときは電源を抜いています。
delayのみの場合(Light Sleep)
#include <Adafruit_TinyUSB.h>
void setup() {
delay(9999999);
}
void loop() {};
平均電流 | 最大値 | 電荷量 |
23uA | 42uA | 233uC / 10s |
delay()で何も処理をしていない状態です。稼働はしているものの何も処理を行っていない状態で 23uA ほど流れていました。
while + delay(1000) の場合
#include <Adafruit_TinyUSB.h>
void setup() {
while (1) delay(1000);
}
void loop() {};
平均電流 | 最大値 | 電荷量 |
24uA | 6950uA | 238uC / 10s |
whileでdelay(1000)を回してみたところ、ヒゲ状の波形が出てきました。CPU仕事してんだな。実際のところ影響は軽微で、平均電流はほとんど変わりませんでした。
while + delay(10) の場合
#include <Adafruit_TinyUSB.h>
void setup() {
while (1) delay(10);
}
void loop() {};
平均電流 | 最大値 | 電荷量 |
56uA | 8530uA | 557uC / 10s |
今度はdelay(10)にしてみたところ、平均電流が大きく上がりました。USB接続なら誤差みたいな値で気にする必要はないでしょうが、ボタン電池で駆動するような場合は、不必要な短いループは避けた方がよさそうですね。
Deep Sleep の場合
#include <Adafruit_TinyUSB.h>
void setup() {
delay(3000);
NRF_POWER->SYSTEMOFF = 1;
}
void loop() {};
平均電流 | 最大値 | 電荷量 |
20uA | 41uA | 138uC / 6.816s |
Deep Sleepを行ったところ波形は大きく変わりましたが、平均電流はそれほど大きく変わってませんね。1桁台になると期待していたんですが、23→20uAとはちょっと期待外れでした。更に下げる方法がないか調べてみたいと思います。→追記が最後にあります
136.5ms周期で動いてるのは、割り込みの処理ですかね。
オンボードLEDを点灯させた場合
#include <Adafruit_TinyUSB.h>
void setup() {
delay(3000);
pinMode(LED_RED, OUTPUT);
digitalWrite(LED_RED, LOW);
delay(2000);
pinMode(LED_GREEN, OUTPUT);
digitalWrite(LED_GREEN, LOW);
delay(2000);
pinMode(LED_BLUE, OUTPUT);
digitalWrite(LED_BLUE, LOW);
delay(999999);
}
void loop() {};
平均電流 | 最大値 | 電荷量 |
601uA | 1730uA | 1120uC / 1.858s |
平均電流 | 最大値 | 電荷量 |
674uA | 1790uA | 1210uC / 1.789s |
平均電流 | 最大値 | 電荷量 |
862uA | 1990uA | 1560uC / 1.812s |
上から順に R, G, B と点灯させた場合です。同時に点灯しているので正しくは R, RG, RGB ですね。何もしてない状態のときは23uAなので、引くとR単体で578uAとなりますが、それではなぜRとGの差が73uAしかないのか、GとBの差が188uAしかないのか、なんか納得いきません。
入力ピンのプルアップ・プルダウン
#include <Adafruit_TinyUSB.h>
void setup() {
delay(3000);
pinMode(D0, INPUT);
delay(2000);
pinMode(D0, INPUT_PULLDOWN);
delay(2000);
pinMode(D0, INPUT_PULLUP);
delay(2000);
pinMode(D0, OUTPUT);
delay(999999);
}
void loop() {};
INPUT | PULLDOWN | PULLUP | OUTPUT |
30uA | 24uA | 24uA | 24uA |
プルアップ・プルダウン抵抗に電流が流れると思ったのですが、何も変わりませんでした。試しにD0~D10すべてのピン設定を変えてみましたが変わりませんでした。そんなに微弱なものなのでしょうかね。
実践編
それでは次からは実践編。実際にアプリケーションで測定していきたいと思います。現在私が開発しているのが、コイン電池からBLEを使って測定データを送信するアプリケーションです。ペアリングせずにビーコンのみでデータを送信するので動作もシンプルで、長時間の稼働を期待できます。乗せられるデータ数はわずかしかありませんが、センサーのデータなら数バイトで十分です。
#include <Arduino.h>
#include <Adafruit_TinyUSB.h> // for Serial
// BLE関連
#include "bluefruit.h"
// 送信するデータの構造体(nRF52840では1バイトはパディングされるので注意)
typedef struct {
uint8_t maker[2]; // maker_id は 0xffff に固定
uint16_t smaker; // 子機(nRF52840)の識別用
uint16_t id; // センサーの識別用
int16_t volt; // 電圧データ
int16_t temp; // 温度データ
uint16_t seq; // シーケンス番号
} AdvData;
AdvData advData = {
.maker = {0xff, 0xff},
.smaker = 0x3412,
.id = 1,
.seq = 0,
};
// 測定してデータを送信する
void measure() {
// バッテリー電圧の測定
int vbat_raw = analogRead(PIN_VBAT);
int vbat_mv = vbat_raw * 2400 / 1023; // VREF = 2.4V, 10bit A/D
vbat_mv = vbat_mv * 1510 / 510; // 1M + 510k / 510k
advData.volt = (int16_t)vbat_mv;
// CPUの温度測定
advData.temp = (int16_t)(readCPUTemperature() * 100.0);
advData.seq++;
// データを送信
if (advData.seq > 9999) advData.seq = 0;
Bluefruit.Advertising.clearData();
Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, &advData, sizeof(advData));
Bluefruit.Advertising.start(20); // アドバタイズを開始、引数は終了する時間(s)
}
// 初期化
void setup() {
// バッテリー電圧測定の準備
analogReference(AR_INTERNAL_2_4); // VREF = 2.4V
analogReadResolution(10); // 10bit A/D
pinMode(VBAT_ENABLE, OUTPUT);
digitalWrite(VBAT_ENABLE, LOW); // VBAT_ENABLEをLOWにすると測定できる
// BLEの設定
Bluefruit.begin();
Bluefruit.autoConnLed(false);
Bluefruit.setTxPower(0); // 送信強度 最小 -40, 最大 +8 dBm
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.setType(BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED);
//Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, &advData, sizeof(advData));
Bluefruit.Advertising.setIntervalMS(500, 1000); // 500-1000msの間隔で送信する
Bluefruit.Advertising.setFastTimeout(10); // 高速アドバタイズの終了時間
measure(); // 測定してデータを送信する
delay(999999);
}
void loop() {}
BLEのアドバタイズ
実はbluefruitライブラリの挙動をよく理解してなかったのですが、これを見るとわかりやすいですね。前半は500ms間隔、後半は1000ms間隔です。
Bluefruit.Advertising.setIntervalMS(500, 1000); // 500-1000msの間隔で送信する
短い間隔で10秒後、長い間隔で10秒続き、その後停止しています。
Bluefruit.Advertising.setFastTimeout(10); // 高速アドバタイズの終了時間
Bluefruit.Advertising.start(20); // アドバタイズを開始、引数は終了する時間(s)
この辺の設定値をChatGPTに質問した回答がちょっと納得できないでいたのですが、これを見ると納得って感じです。それぞれの電流値はこちら。ChatGPTはカジュアルに嘘をつく。
500ms間隔時 | 1000ms間隔時 | 停止時 |
53uA | 38uA | 26uA |
BLEってこんなに低消費電力なの?? すごいですね。それでは次は送信強度を変えて試してみます。
送信強度の変更
Bluefruit.setTxPower(0); // 送信強度 最小 -40, 最大 +8 dBm
Bluefruit.setTxPower()で -40から+8まで与えることができます。先ほどの結果は0の場合なので、-40と+8を試してみます。
-40dB | 0dB | 8dB |
43uA | 53uA | 86uA |
最大出力でもそれほど大きくないですね。この辺は受信機側までの距離や受信状況を見ながら調整が必要です。下手に出力を下げてデータを取りこぼしてしまっては意味がありませんし、むしろ出力を上げて間隔を下げた方がいいかもしれません。
XIAO BLE nRF52840は最小で何Vまで動作するか?
nRF52840の供給電圧範囲は1.7V~5.5Vだそうです。3Vのコイン電池を使うとそこからどんどん電圧が下がってきますから、電圧が低くなっても動作する必要があります。電源はPPK2から出力して、BLEの電波が正常に受信できる範囲を試してみました。
結果は1.9Vまでは正常に動作しました。1.8Vだとリセットがかかってしまうようでした。また1.9VというのはあくまでPPK2の出力電圧で、実際には配線による電圧降下の可能性があります。今回はプログラムでBAT+端子の電圧測定をしているので確認してみると、電圧は1.74Vでした。スペック通りの電圧で動作してるってことですね。
まとめ
ESP32は消費電力が大きすぎて電池駆動するには厳しいところがありましたが、nRF52840ならコイン電池1個で数か月は余裕で動かせそうな気がします。アイドル時の20uAが1桁台まで下がれば、1年でもいけるはずです。最近nRF52840に手を出したばかりでよくわかっていませんが、1桁μAを目指して開発していきたいと思います。
追記
オンボードQSPI Flash MemoryをDeep Sleepモードにする
更に省電力化する方法がないか調べていたところ、Seeedのフォーラムで省電力化に関するトピックを見つけました。どうやらオンボードのQSPI Flash MemoryをDeep Sleepモードにするそうです。
#include <Adafruit_TinyUSB.h>
#include "flashmemoryDeepSleep.h"
void setup() {
delay(2000); // Light Sleep (QSPI-Flash on)
flashmemoryDeepSleep(); // QSPI-FlashmemoryをDeep Sleep Modeにする
delay(2000); // Light Sleep (QSPI-Flash off)
NRF_POWER->SYSTEMOFF = 1; // Deep Sleep
}
void loop() {};
// オンボードQSPI Flash MemoryをDeep Power-downモードにして省電力化する
// from Seed Studio Forum by msfujino
// https://forum.seeedstudio.com/t/sleep-current-of-xiao-nrf52840-deep-sleep-vs-light-sleep/271841
// P25Q16H Datasheet
// https://files.seeedstudio.com/wiki/github_weiruanexample/Flash_P25Q16H-UXH-IR_Datasheet.pdf
//----------------------------------------------------------------------------------------------
// BSP : Seeed nRF52 Borads 1.1.1
// Board : Seeed nRF52 Borads / Seeed XIAO nRF52840 Sense
//----------------------------------------------------------------------------------------------
// 2023/08/11
#include <bluefruit.h> // /Arduino15/packages/Seeeduino/hardware/nrf52/1.1.1/libraries/Bluefruit52Lib/src
#include <Adafruit_SPIFlash.h> // Need to be deleted /Documents/Arduino/libraries/SdFat
// Built from the P25Q16H datasheet.
// https://gitlab.com/arduino5184213/seeed_xiao_nrf52840/flash_speedtest/-/tree/master
SPIFlash_Device_t const P25Q16H {
.total_size = (1UL << 21), // 2MiB
.start_up_time_us = 10000, // Don't know where to find that value
.manufacturer_id = 0x85,
.memory_type = 0x60,
.capacity = 0x15,
.max_clock_speed_mhz = 55,
.quad_enable_bit_mask = 0x02, // Datasheet p. 27
.has_sector_protection = 1, // Datasheet p. 27
.supports_fast_read = 1, // Datasheet p. 29
.supports_qspi = 1, // Obviously
.supports_qspi_writes = 1, // Datasheet p. 41
.write_status_register_split = 1, // Datasheet p. 28
.single_status_byte = 0, // 2 bytes
.is_fram = 0, // Flash Memory
};
Adafruit_FlashTransport_QSPI flashTransport;
Adafruit_SPIFlash flash(&flashTransport);
void flashmemoryDeepSleep() {
// on board Flash enter to Deep Power-Down Mode
flashTransport.begin();
flashTransport.runCommand(0xB9); // enter deep power-down mode Datasheet p.44
delayMicroseconds(5); // tDP=3uS
flashTransport.end();
}
Light Sleep | Light Sleep(flash on) | Deep Sleep(flash off) |
23.68uA | 5.45uA | 2.16uA |
なんとDeep Sleep時に 2.16uA まで下がりました。素晴らしい!msfujinoさんに感謝。外部Flashメモリは使ってなければ最初からオフにしてしまってもよさそうですね。
あとはタイマーでDeep Sleepからの復帰ができるとよかったのですが、フォーラムを読んでもGPIOからの割り込みでしか復帰ができない(?)ようで、一定時間後に自動復帰するような使い方は難しそうでした。でもLight Sleepでも5uAですから、delay()でLight Sleepしとけばいいですね。これならコイン電池で1年動くはず。
とにかくこれで完全目的達成です!