XIAO nRF52840 Senseで作る省電力モーション検知デバイス
はじめに
電池駆動のIoTデバイスでは「どれだけ省電力にできるか」が課題です。
今回は XIAO nRF52840 Sense を使い、加速度センサーで動きを検知し、BLEアドバタイズでデータを送信する仕組みを試作しました。

省電力化のポイント
1.センサー設定の工夫
- 内蔵IMU(LSM6DS3/BMI270相当)を low-powerモード で動作
- Wake-up割り込みを使い、動きがあったときだけCPUを起こす
これにより、CPUの稼働を最小限にできました。
2.BLE通信の工夫
通常のBLE接続では、接続維持だけで電力を消費します。
そこで今回は アドバタイズパケットの中に計測値を埋め込み、垂れ流す方式 を採用しました。
メリット:
- 接続処理が不要 → 消費電力削減
- 受信側はスキャンするだけでデータ取得可能
void updateAdvertiseData(uint32_t sendCount,uint32_t wakeCount,
int16_t ax, int16_t ay, int16_t az) {
uint8_t manuf_data[12] = {
0x59,0x00, // Company ID(Nordic)
(uint8_t)(sendCount >> 8), (uint8_t)(sendCount),
(uint8_t)(wakeCount >> 8), (uint8_t)(wakeCount),
(uint8_t)(ax >> 8), (uint8_t)ax,
(uint8_t)(ay >> 8), (uint8_t)ay,
(uint8_t)(az >> 8), (uint8_t)az
};
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA,
manuf_data, sizeof(manuf_data));
Bluefruit.Advertising.addName();
Bluefruit.Advertising.start(0); // 無期限アドバタイズ
}
3.スリープ方式の選定
省電力化はどれだけ長くスリープの状態を保てるかが大事です。
当初は deepsleep を検討していましたが、RAMが消えるため不採用になり、
最終的には WFI() を使った System ON Sleep を採用することになりました。
これにより、RAMを保持しながら待機時消費を抑えられました。
まとめ
今回のポイントは以下の3つでした。
- センサーをlow-power化し、割り込みでCPUを起こす
- BLEは接続せずアドバタイズでデータを流す
- deep sleepではなくWFI()でSystem ON Sleepにする
これらの工夫により、リチウム一次電池で約1年持ちそうなレベルの省電力IoTデバイスを作ることができました。
最後にコード全文を載せておきます。
//----------------------------------------------------------------------------------------------
// BSP : Seeed nRF52 Boards 1.1.1
// Board : Seeed XIAO nRF52840 Sense
//----------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------
// XIAO nRF52840 Sense を用いた BLE 通信&モーション検出デバイス
// - RTCで定周期起床し、加速度データをBLEアドバタイズ
// - IMUのWake-Up割り込みで動き検知 → 追加送信
// ---------------------------------------------------------------------------------------------
// BLEライブラリ、IMU用I2Cライブラリの読み込み
#include <bluefruit.h> // BLEライブラリ
#include <LSM6DS3.h> // LSM6DS3 6軸センサ
#include <Wire.h> // I2Cライブラリ
#define WAKEUP_PIN 18 // XIAO Sense の INT1 割り込みピン
#define BUTTON_PIN D2 // ボタンピン
#define SLEEP_TIME 3 // RTC周期 [秒]
#define I2C_ADDR 0x6A // LSM6DS3 のI2Cアドレス
uint32_t sendCount = 0; // 送信回数カウンタ
uint32_t wakeCount = 0; // ウェイクアップ回数カウンタ
bool intFlag = true; // RTC割り込みフラグ
volatile bool motionDetected = false; // 加速度割り込みフラグ
volatile bool countEnabled = true; // カウンターの稼働許可フラグ
float ax; //加速度
float ay;
float az;
volatile unsigned long lastMotionTime = 0; // 最後に割り込みを検知した時刻
const unsigned long motionCooldown = 500; // 500ms(=0.5秒)
LSM6DS3 imu(I2C_MODE, I2C_ADDR); // LSM6DS3インスタンス
void setup() {
setupIMUWakeup(); // IMUを低消費電力モードで初期化
digitalWrite(LED_BLUE, LOW); // 青LED ON(XIAOは LOWで点灯)
// 電源関連設定
NRF_POWER->DCDCEN = 1; // DCDCコンバータ有効化で省電力化
NRF_POWER->TASKS_LOWPWR = 1; // System-ONスリープの消費を抑えるモード設定
delay(200); // 起動後の安定化待機
setupPinMode(); //LED初期化
initRTC(32768 * SLEEP_TIME); // RTC2の初期化(SLEEP_TIME秒ごと)
setupBLE(); // BLE初期化
attachInterrupt(digitalPinToInterrupt(WAKEUP_PIN),
IRAMlessISR, RISING); // INT1 は High パルス
delay(200); // 待機
digitalWrite(LED_BLUE, HIGH); // 青LED OFF
}
// メインループ
void loop() {
//RTC割り込み
if (intFlag == true) {
intFlag = false;
if (countEnabled) { // 稼働中のみ実行
sendCount++;
int16_t ax_int = (int16_t)(ax * 1000);
int16_t ay_int = (int16_t)(ay * 1000);
int16_t az_int = (int16_t)(az * 1000);
updateAdvertiseData(sendCount, wakeCount, ax_int, ay_int, az_int);
delay(100);
Bluefruit.Advertising.stop(); //100ms送信
}
}
//加速度割り込み
if (motionDetected) {
motionDetected = false;
if (countEnabled) { // 稼働中のみ実行
digitalWrite(LED_GREEN, LOW);
delay(100);
wakeCount++;
ax = imu.readFloatAccelX();
ay = imu.readFloatAccelY();
az = imu.readFloatAccelZ();
Serial.print("Accel X: "); Serial.print(ax, 4);
Serial.print(" Y: "); Serial.print(ay, 4);
Serial.print(" Z: "); Serial.println(az, 4);
// 読み取りでラッチ解除(必須)
uint8_t src;
imu.readRegister(&src, LSM6DS3_ACC_GYRO_WAKE_UP_SRC);
digitalWrite(LED_GREEN, HIGH);
}
}
// 割り込み待機状態へ移行
__WFI();
__SEV();
__WFI();
}
// ---------- 割り込み ISR ----------
void IRAMlessISR() {
unsigned long now = millis(); //検知後しばらくは無視
if (now - lastMotionTime >= motionCooldown) {
motionDetected = true;
lastMotionTime = now;
}
}
// ---------- BLE初期設定 ----------
void setupBLE() {
Bluefruit.begin(); // BLEスタック初期化
Bluefruit.setName("BLE1"); // デバイス名を設定
Bluefruit.setTxPower(0); // 送信出力を0dBmに
Bluefruit.autoConnLed(false); // ステータスLED無効化
}
// ---------- ピンモード初期設定 ----------
void setupPinMode(){
// LED初期化
pinMode(WAKEUP_PIN, INPUT); // INT1 は推奨プッシュプル High
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
}
// ---------- RTC2の初期化 ----------
void initRTC(unsigned long count30) {
NRF_CLOCK->TASKS_LFCLKSTOP = 1; // LFクロック停止
NRF_CLOCK->LFCLKSRC = 1; // LFXO選択
NRF_CLOCK->TASKS_LFCLKSTART = 1;
while (NRF_CLOCK->LFCLKSTAT != 0x10001); // クロック安定待ち
NRF_RTC2->TASKS_STOP = 1; // RTC2停止
NRF_RTC2->TASKS_CLEAR = 1; // カウンタクリア
NRF_RTC2->PRESCALER = 0; // 分周なし→32.768kHz
NRF_RTC2->CC[0] = count30; // 割り込み周期設定
NRF_RTC2->INTENSET = 0x10000; // COMPARE0割り込み許可
NRF_RTC2->EVTENCLR = 0x10000; // イベントクリア
NVIC_SetPriority(RTC2_IRQn, 3); // 割り込み優先度設定
NVIC_EnableIRQ(RTC2_IRQn); // 割り込み有効化
NRF_RTC2->TASKS_START = 1; // RTC2開始
}
// ---------- RTC2割り込みハンドラ ----------
extern "C" void RTC2_IRQHandler(void) {
if ((NRF_RTC2->EVENTS_COMPARE[0] != 0) && ((NRF_RTC2->INTENSET & 0x10000) != 0)) {
NRF_RTC2->EVENTS_COMPARE[0] = 0; // フラグクリア
NRF_RTC2->TASKS_CLEAR = 1; // カウンタクリア
intFlag = true; // 割り込み発生通知
}
}
// ---------- IMU 設定 ----------
void setupIMUWakeup() {
Wire.begin();
imu.settings.accelEnabled = 1; // 加速度有効
imu.settings.gyroEnabled = 0; // ジャイロ無効
imu.settings.accelRange = 2; // ±2g
imu.settings.accelSampleRate = 104; // サンプルレート 104 Hz (normal mode)
if (imu.begin() != 0) { // センサ初期化
while (1); // 初期化失敗時は停止
}
// 1. low power設定
imu.writeRegister(LSM6DS3_ACC_GYRO_CTRL6_G, 0x10);
// 2. SLOPE フィルタ + 割り込みラッチ許可
imu.writeRegister(LSM6DS3_ACC_GYRO_TAP_CFG1, 0x80);
// 3. Wake‑Up 閾値 & 継続時間
imu.writeRegister(LSM6DS3_ACC_GYRO_WAKE_UP_THS, 0x08);
imu.writeRegister(LSM6DS3_ACC_GYRO_WAKE_UP_DUR, 0x04);
// 4. Wake‑Up を INT1 へルート
imu.writeRegister(LSM6DS3_ACC_GYRO_MD1_CFG, 0x20);
}
// BLEアドバタイズデータ更新
void updateAdvertiseData(uint32_t sendCount,uint32_t wakeCount, int16_t ax, int16_t ay, int16_t az) {
uint8_t manuf_data[12] = {
0x59,0x00, // Company ID(Nordic)
(uint8_t)(sendCount >> 8), (uint8_t)(sendCount),
(uint8_t)(wakeCount >> 8), (uint8_t)(wakeCount),
(uint8_t)(ax >> 8), (uint8_t)ax,
(uint8_t)(ay >> 8), (uint8_t)ay,
(uint8_t)(az >> 8), (uint8_t)az
};
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, manuf_data, sizeof(manuf_data));
Bluefruit.Advertising.addName();
Bluefruit.Advertising.start(0); // 無期限アドバタイズ
}