XIAO nRF52840 Senseで作る省電力モーション検知デバイス

はじめに

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

Seeed Studio 公式Wikiより引用

省電力化のポイント

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つでした。

  1. センサーをlow-power化し、割り込みでCPUを起こす
  2. BLEは接続せずアドバタイズでデータを流す
  3. 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); // 無期限アドバタイズ
}
よかったらシェアしてね!