前回XIAO BLE nRF52840というマイコンモジュールの消費電力を調べてみましたが、今回は実際にどのくらいまで電波が届くのか実験をしてみました。比較するパラメーターは以下の3点です。

・送信する電波の出力強度
・アドバタイズの送信頻度
・送信機から受信機までの距離

これらを変更しながら最適なパラメーターを見つけていきたいと思います。なお、今回の実験はアドバタイズにセンサーのデータを乗せて送信するIoTデバイスを想定した実験です。ペアリングを行って通信する場合はまた変わってくるかと思います。

測定条件

送信側
・XIAO BLE nRF52840を使用
・電源はコイン電池CR2032をBAT+/-に接続
・9秒間アドバタイズを送信、1秒間休止、この1セットで10秒
・これを10回繰り返す

受信側
・M5Stack Core2を使用
・5秒間スキャンして受信したデータをサーバーに送信し、すぐに繰り返す
・同じデータを再び受信した場合は無視する

つまり、取得したデータ数は10個になれば全てのデータが正常に受信できたことを意味します。

測定パターン

(1) 送信する電波の出力強度
Bluefruit.setTxPower() で与えるパラメーターを、-40, 0, 4, 8 の4パターンで試しました。-40dBが最小で、+8dBが最大です。

(2) アドバタイズの送信頻度
Bluefruit.Advertising.setInterval() で与えるパラメーターを、32, 64, 128, 256, 384, 512, 1024, 2048, 4096 の9パターンで試しました。この数字はBLEの基準の周期 0.625ms の倍数とする値で、32なら20ms間隔、512なら320ms間隔で送信されます。
setInterval()には2つ引数がありますが、今回は同じ値にしています。Bluefruit.Advertising.setFastTimeout(1) としているので、常に同じ間隔で出すようになっています。

(3) 送信機から受信機までの距離
以下の3パターンで実験しました。特に最後の箇所はWiFiでも電波状況が良くない場所です。
・約50cmの至近距離
・約4m、廊下をはさんで隣の部屋
・約15m、廊下をはさんで隣の隣の部屋

プログラム

送信側(XIAO BLE nRF52840)

/*
  ble2wifi_client.ino
  Seeed XIAO BLE nRF52840でバッテリー電圧等のデータを、BLEのアドバタイズデータにのせて送信する
  
  Copyright (c) 2024 Kaz  (https://akibabara.com/blog/)
  Released under the MIT license.
  see https://opensource.org/licenses/MIT
*/

#define DEBUG 1
#include <Arduino.h>
#if DEBUG == 1
#include <Adafruit_TinyUSB.h>   // for Serial
#endif

// 設定
const uint16_t DEVICE_ID = 1;     // 子機ID (advData.id)
const uint8_t DEVICE_TYPE = 10;   // 子機種別 (advData.type)

// BLE関連
#include "bluefruit.h"

// 外部QSPI Flash Memory(省電力化のために使用)
#include <Adafruit_SPIFlash.h>
Adafruit_FlashTransport_QSPI flashTransport;
Adafruit_SPIFlash flash(&flashTransport);

// 送信するデータの構造体(nRF52840では2バイト未満はパディングされるので順番に注意)
typedef struct {
  uint8_t maker[4]; // maker_id 子機(nRF52840)の識別用
  uint16_t id;    // 子機ID
  uint8_t type;   // 子機種別
  uint8_t ttl;    // TTL (time to live)
  uint16_t seq;   // シーケンス番号   -- ここまで共通フォーマット
  int16_t volt;   // 電圧データ
  int16_t temp;   // 温度データ
} AdvData;
 AdvData advData = {
  .maker = { 0xFF, 0xFF, 0x12, 0x35 },
  .id = DEVICE_ID,
  .type = DEVICE_TYPE,
  .ttl = 0,
  .seq = 1,
 };

// デバッグに便利なマクロ定義 --------
#define sp(x) Serial.println(x)
#define spn(x) Serial.print(x)
#define spp(k,v) Serial.println(String(k)+"="+String(v))
#define spf(fmt, ...) Serial.printf(fmt, __VA_ARGS__)
#define array_length(x) (sizeof(x) / sizeof(x[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;
  if (DEBUG) sp(advData.volt);

  // CPUの温度測定
  advData.temp = (int16_t)(readCPUTemperature() * 100.0);

  // アドバタイズ中なら一旦中断(たぶんしなくていい)
  if (Bluefruit.Advertising.isRunning()) {
    Bluefruit.Advertising.stop();
  }

  // データを送信
  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(9);   // アドバタイズを開始、引数は終了する時間(s)
  advData.seq++;
}

// 初期化
void setup() {
  if (DEBUG) {
    Serial.begin(115200);
    pinMode(LED_RED, OUTPUT);
    pinMode(LED_GREEN, OUTPUT);
    pinMode(LED_BLUE, OUTPUT);
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GREEN, HIGH);
    digitalWrite(LED_BLUE, HIGH);
  }

  // オンボードQSPI Flash MemoryをDeep Power-downモードにして省電力化する
  flashTransport.begin();
  flashTransport.runCommand(0xB9);
  delayMicroseconds(5);
  flashTransport.end();

  // バッテリー電圧測定の準備
  analogReference(AR_INTERNAL_2_4); // VREF = 2.4V
  analogReadResolution(10);         // 10bit A/D
  pinMode(VBAT_ENABLE, OUTPUT);
  digitalWrite(VBAT_ENABLE, LOW);   // VBAT_ENABLEをLOWにすると測定できる

  if (DEBUG) {
    digitalWrite(LED_RED, LOW);
    delay(500);
    digitalWrite(LED_RED, HIGH);
  }

  // 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.setFastTimeout(1);  // 高速アドバタイズの終了時間 0=継続(0にするとなぜかstart()が効かない)

  // WDTの設定
  NRF_WDT->CONFIG         = 0x01;     // Configure WDT to run when CPU is asleep
  NRF_WDT->CRV            = 1+32768*120;    // CRV = timeout * 32768 + 1
  NRF_WDT->RREN           = 0x01;     // Enable the RR[0] reload register
  NRF_WDT->TASKS_START    = 1;        // Start WDT       
}

// メイン
void loop() {
  int16_t txpowers[4] = { -40, 0, 4, 8 };
  uint16_t intervals[9] = { 32, 64, 128, 256, 384, 512, 1024, 2048, 4096 };
  for (int tx=0; tx<4; tx++) {
    for (int iv=0; iv<9; iv++) {
      advData.id = tx * 100 + iv;
      advData.seq = 1;
      Bluefruit.setTxPower(txpowers[tx]);
      Bluefruit.Advertising.setInterval(intervals[iv], intervals[iv]);
      for (int i=0; i<10; i++) {
        loop_sub();
      }
    }
  }
  digitalWrite(LED_GREEN, HIGH);
  digitalWrite(LED_BLUE, HIGH);
  while (1) {
    digitalWrite(LED_RED, LOW);
    delay(500);
    digitalWrite(LED_RED, HIGH);
    delay(500);
    NRF_WDT->RR[0] = WDT_RR_RR_Reload;
  }
}
void loop_sub() {
  if (DEBUG) {
    digitalWrite(LED_GREEN, HIGH);
    digitalWrite(LED_BLUE, LOW);
  }

  measure();  // 測定してデータを送信する

  delay(500);
  if (DEBUG) {
    digitalWrite(LED_BLUE, HIGH);
    digitalWrite(LED_GREEN, LOW);
  }
  delay(9500);

  // WDT Update
  NRF_WDT->RR[0] = WDT_RR_RR_Reload;
}

受信側(M5Stack Core2)

/*
  ble2wifi_server.ino
  Seeed XIAO BLE nRF52840から受信したBLEのアドバタイズデータを、WiFiでWebサーバーに送信する

  Copyright (c) 2024 Kaz  (https://akibabara.com/blog/)
  Released under the MIT license.
  see https://opensource.org/licenses/MIT
*/

#include <M5Unified.h>
//#include <Arduino.h>

// ネットワーク設定
#define WIFI_SSID "xxxx"
#define WIFI_PASS "xxxx"
const String WEB_API_URL = "http://xxxx/bletest.php";  // 送信先URL

// 設定
const size_t RCV_CNT_MAX = 10;  // 同時に受信するデバイスの最大数
const int BLE_SCAN_TIME = 5;    // BLEのスキャンを行う時間(s)
const char XIAO[4] = { 0xFF, 0xFF, 0x12, 0x35 };   // XIAO nRF52840識別値(FFFFは固定)

// BLE関連
#include <BLEDevice.h>
BLEScan* pBLEScan;

// Wi-Fi関連
#include <WiFi.h>
#include <HTTPClient.h>

// シーケンス番号の記憶
#include <map>
std::map<uint16_t, uint16_t> seqHist;

// デバッグに便利なマクロ定義 --------
#define sp(x) Serial.println(x)
#define spn(x) Serial.print(x)
#define spp(k,v) Serial.println(String(k)+"="+String(v))
#define spf(fmt, ...) Serial.printf(fmt, __VA_ARGS__)

// その他定義
struct RcvData {
  uint16_t id;  // 子機ID
  uint8_t type; // 子機種別
  uint8_t ttl;  // TTL (time to live)
  uint16_t seq; // シーケンス番号
  float volt;   // バッテリー電圧
  float temp;   // SoC温度
  int16_t rssi; // 電波強度 RSSI -50非常に強い -80弱い
};

// Wi-Fi接続する
bool wifiConnect() {
  bool stat = false;
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Wifi connecting.");
    for (int j=0; j<10; j++) {
      WiFi.disconnect();
      WiFi.mode(WIFI_STA);
      WiFi.begin(WIFI_SSID, WIFI_PASS);  //  Wi-Fi APに接続
      for (int i=0; i<10; i++) {
        if (WiFi.status() == WL_CONNECTED) break;
        Serial.print(".");
        delay(500);
      }
      if (WiFi.status() == WL_CONNECTED) {
        Serial.println("connected!");
        Serial.println(WiFi.localIP());
        stat = true;
        break;
      } else {
        Serial.println("failed");
        WiFi.disconnect();
      }
    }
  }
  return stat;
}

// 初期化
void setup() {
  auto cfg = M5.config();
  M5.begin(cfg); 
	Serial.begin(115200);
  delay(1000);
  sp("System Start!");

  // ディスプレイの設定
  M5.Lcd.init();
  M5.Lcd.setColorDepth(16);
  M5.Lcd.fillScreen(TFT_BLUE);
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setClipRect(2,2, M5.Lcd.width()-4, M5.Lcd.height()-4);  //描画範囲

  // Wi-Fi接続
  wifiConnect();

  // BLEの設定
  sp("init BLE device...");
  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan();
  pBLEScan->setActiveScan(false);   // パッシブスキャンにする
}

// メイン
void loop() {
  RcvData rcvdatas[RCV_CNT_MAX];
  int rcvcnt = 0;

  // BLEのスキャンを行う
  sp("\nscanning...");
  BLEScanResults foundDevices = pBLEScan->start(BLE_SCAN_TIME);
  int hit = foundDevices.getCount();
  spf("found", hit);

  // 受信した一覧から対象デバイスを抽出する
  for (int i=0; i<hit; i++) {
    BLEAdvertisedDevice dev = foundDevices.getDevice(i);
    std::string data = dev.getManufacturerData();

    // debug
    if (data[0] == XIAO[0] && data[1] == XIAO[1]) {
      String tmpinfo = "";
      spf("Scanned: Device_%03d (%02d): ", i, data.length());
      for (int j=0; j<data.length(); j++) spf("%02X ",data[j]);
      sp("");
    }

    // XIAO nRF52840からのデータだったら値を格納する
    if (data.length() < 14) continue;
    if (data[0] == XIAO[0] && data[1] == XIAO[1] && data[2] == XIAO[2] && data[3] == XIAO[3]) {
      rcvdatas[rcvcnt].id = data[5] << 8 | data[4];
      rcvdatas[rcvcnt].type = data[6];
      rcvdatas[rcvcnt].ttl = data[7];
      rcvdatas[rcvcnt].seq = data[9] << 8 | data[8];
      if (seqHist[rcvdatas[rcvcnt].id] == rcvdatas[rcvcnt].seq) continue;  // 同じデータは無視
      seqHist[rcvdatas[rcvcnt].id] = rcvdatas[rcvcnt].seq;
      if (rcvdatas[rcvcnt].type == 10) {  // 種別10: 実験用 volt(i2) temp(i2)
        rcvdatas[rcvcnt].volt = (float)(data[11] << 8 | data[10]) / 1000.00;
        rcvdatas[rcvcnt].temp = (float)(data[13] << 8 | data[12]) / 100.00;
        rcvdatas[rcvcnt].rssi = (uint16_t)dev.getRSSI();  // 電波強度
        rcvcnt ++;
      }
      if (rcvcnt >= RCV_CNT_MAX) break;
    }
  }

  // ディスプレイに表示
  if (rcvcnt > 0) {
    M5.Lcd.fillScreen(TFT_BLACK);
    M5.Lcd.setCursor(2, 4);
    for (int i=0; i<rcvcnt; i++) {
      M5.Lcd.printf("%02X %.2fV %.1fC %d %d\n", rcvdatas[i].id, rcvdatas[i].volt, rcvdatas[i].temp, rcvdatas[i].rssi, rcvdatas[i].seq);
    }
  } else {
    M5.Lcd.println("no data found");
  }

  // サーバーに送信する
  if (rcvcnt > 0) {
    postServer(rcvdatas, rcvcnt);
  }

  delay(10);
}

// Webサーバーに送信
void postServer(struct RcvData* td, int cnt) {
  HTTPClient http;
  sp("Send to Web Server");

  // POSTデータ作成
  String jsonData = "{\"data\":[";
  char buff[64];
  for (int i=0; i<cnt; i++) {
    sprintf(buff, "{\"id\":%d,", td[i].id);
    jsonData += buff;
    sprintf(buff, "\"volt\":%.2f,", td[i].volt);
    jsonData += buff;
    sprintf(buff, "\"temp\":%.1f,", td[i].temp);
    jsonData += buff;
    sprintf(buff, "\"rssi\":%d,", td[i].rssi);
    jsonData += buff;
    sprintf(buff, "\"seq\":%d}", td[i].seq);
    jsonData += buff;
    if (i < cnt-1) jsonData += ",";
  }
  jsonData += "],\"count\":"+String(cnt)+"}";
  sp(jsonData);

  // 送信
  http.begin(WEB_API_URL); //HTTP
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");
  http.addHeader("Content-Length", String(jsonData.length()));
  http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
  int httpCode = http.POST(jsonData);
  sp("[HTTP] POST... code: %d\n"+String(httpCode));

  // 送信後の処理
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    sp(payload);
  } else {
    sp("[HTTP] POST... failed, error: %s\n"+String(httpCode));
  }
  http.end();
}

Webサーバー側(Linux)

<?php
//
// M5Stackから送信されたデータをCSVに格納する
//

// 設定
$SAVE_DIR = "/path/to/savedir";	// 保存先ディレクトリ(パーミッションの変更を忘れずに)
$PREFIX = "xiao";	// プレフィックス

// 引数を取り込み
$text = file_get_contents("php://input");
$json = json_decode($text, true);
if (!is_numeric($json['count'] ?? null) || !is_array($json['data'] ?? null)) {
	header("HTTP/1.1 400 Bad Request");
	exit;
}

// ファイルに保存
$update = 0;
if (count($json['data']) > 0) {
	foreach ($json['data'] as $dd) {
		if (!is_numeric($dd['id'] ?? null)) continue;
		$seq_path = sprintf("%s/%s_%d.seq", $SAVE_DIR, $PREFIX, $dd['id']);
		$csv_path = sprintf("%s/%s_%d.csv", $SAVE_DIR, $PREFIX, $dd['id']);
		$allcsv_path = sprintf("%s/%s_all.csv", $SAVE_DIR, $PREFIX);
		$last_seq = (file_exists($seq_path)) ? file_get_contents($seq_path) : -1;
		if ($dd['seq'] != $last_seq) {
			$csv = [
				date("Y-m-d H:i:s"),
				$dd['id'],
				$dd['volt'],
				$dd['temp'],
				$dd['rssi'],
				$dd['seq'],
			];
			$str = join(",",$csv)."\n";
			error_log($str, 3, $csv_path);
			error_log($str, 3, $allcsv_path);
			file_put_contents($seq_path, $dd['seq']);
			$update ++;
		}
	}
}

// 終了
$json = [ 'update'=> $update ];
header("Content-Type: application/json; charset=utf-8");
echo json_encode($json);
exit;

?>

測定結果

それぞれの距離別の測定結果は以下の通りです。縦軸は受信に成功した回数で、10は100%成功です。横軸の「AI nn」はBluefruit.Advertising.setInterval()に与えた数字で、アドバタイズの頻度です。(0.625*nn秒)

すぐ横に置いたような状態でしたが、出力が-40dBのときはちょっと厳しいようでした。

別の部屋に置いた場合、出力-40dBは全滅でしたが、それ以外はけっこういけますね。

複数の部屋をまたいだ厳しい条件では、送信出力によって大きな差がありました。出力+4dBならば、アドバタイズの頻度を下げても正常に受信できるようです。

消費電流の調査

続いて送信出力 vs アドバタイズ送信間隔の消費電流を測定してみました。測定は先ほどのプログラムをベースにしていますが、余計な消費電流を省くためにLEDの点灯をやめています。電源はPPK2から3000mVを与えています。

さすがに高出力を高頻度で行うと結構食いますね。といっても最高でも1.3mAくらいですから、滅茶苦茶省電力なことがわかります。

今までの実験結果から、距離に対しては頻度よりも出力を上げる方が効果的なことがわかりましたので、出力は4dBか8dBくらいに上げて、頻度を256(160mS)あたりにするのが、実用的で省電力でいいかもしれません。

Bluefruit.setTxPower(4);
Bluefruit.Advertising.setInterval(256, 256);

これなら平均 146uA くらいです。

実際は部屋の構造だったり受信側のアプリケーションなど、環境によって変わってくると思いますので、ちょうど良いところを探してみるといいと思います。

LINEで送る
Pocket