コード

以下のようなコードで試した。概要としては

  • 2.5秒に1度起動する (それ以外は deep sleep)
  • 起動時にデータをRTCメモリに書きむ (センサーデータ取得を想定)
  • 4回に1度 WiFi に接続し、UDP でセンサーデータを送信する

というもの

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include "rtc_memory.hpp"
#include "config.h"
WiFiClient wifiClient;
struct {
char SSID[255] = WIFI_SSID;
char Password[255] = WIFI_PASS;
} wifi_config;
bool startWifi(int timeout) {
WiFi.disconnect();
WiFi.mode(WIFI_STA);
Serial.println("Reading wifi_config");
Serial.println("wifi_config:");
Serial.print("SSID: ");
Serial.print(wifi_config.SSID);
Serial.print("\n");
if (strlen(wifi_config.SSID) == 0) {
Serial.println("SSID is not configured");
return false;
}
WiFi.begin(wifi_config.SSID, wifi_config.Password);
int time = 0;
for (;;) {
switch (WiFi.status()) {
case WL_CONNECTED:
Serial.println("connected!");
WiFi.printDiag(Serial);
Serial.print("IPAddress: ");
Serial.println(WiFi.localIP());
return true;
case WL_CONNECT_FAILED:
Serial.println("connect failed");
return false;
}
delay(500);
Serial.print(".");
time++;
if (time >= timeout) {
break;
}
}
return false;
}
struct deep_sleep_data_t {
uint16_t count = 0;
uint8_t send = 0;
uint16_t data[12];
void add_data(uint16_t n) {
data[count] = n;
}
template <class T>
void run_every_count(uint16_t n, T func) {
count++;
if (!send) {
send = count % (n - 1) == 0;
} else {
send = 0;
count = 0;
func();
}
}
};
rtc_memory<deep_sleep_data_t> deep_sleep_data;
void post_sensor_data();
void setup() {
pinMode(13, OUTPUT);
Serial.begin(74880);
Serial.println("Initializing...");
// データ読みこみ
if (!deep_sleep_data.read()) {
Serial.println("system_rtc_mem_read failed");
}
Serial.print("deep_sleep_data->count = ");
Serial.println(deep_sleep_data->count);
// データの変更処理(任意)
deep_sleep_data->add_data(deep_sleep_data->count);
deep_sleep_data->run_every_count(4, [&]{
Serial.println("send data");
// なんか定期的に書きこみたい処理
post_sensor_data();
});
if (!deep_sleep_data.write()) {
Serial.print("system_rtc_mem_write failed");
}
if (deep_sleep_data->send) {
ESP.deepSleep(2.5e6, WAKE_RF_DEFAULT);
} else {
// sendしない場合は WIFI をオフで起動させる
ESP.deepSleep(2.5e6, WAKE_RF_DISABLED);
}
}
void loop() {
}
// dummy
void post_sensor_data() {
if (startWifi(30)) {
// do http etc...
WiFiUDP udp;
IPAddress serverIP(192, 168, 0, 5);
udp.beginPacket(serverIP, 5432);
udp.write((uint8_t*)deep_sleep_data->data, sizeof(deep_sleep_data->data));
udp.endPacket();
// wait for sending packet
delay(1000);
} else {
Serial.println("failed to start wifi");
ESP.restart();
}
}
view raw main.cpp hosted with ❤ by GitHub
extern "C" {
#include <user_interface.h>
};
template <class T>
class rtc_memory {
// 4 bytes aligned memory block address in rtc memory.
// user data must use 64 or larger block.
// but system_rtc_mem_read() is failed for 64 so use 65.
static constexpr uint32_t USER_DATA_ADDR = 65;
static constexpr uint32_t USER_DATA_SIZE = 512 - ((USER_DATA_ADDR - 64) * 4);
static uint32_t fnv_1_hash_32(uint8_t *bytes, size_t length) {
static const uint32_t FNV_OFFSET_BASIS_32 = 2166136261U;
static const uint32_t FNV_PRIME_32 = 16777619U;
uint32_t hash = FNV_OFFSET_BASIS_32;
for (size_t i = 0 ; i < length ; ++i) hash = (FNV_PRIME_32 * hash) ^ (bytes[i]);
return hash;
}
uint32_t calc_hash(T& data) const {
return fnv_1_hash_32((uint8_t*)&data, sizeof(data));
}
public:
uint32_t hash;
T data;
static_assert(sizeof(T) <= (USER_DATA_SIZE - sizeof(hash)), "sizeof(T) it too big");
bool read() {
// Read memory to temporary variable to retain initial values in struct T.
rtc_memory<T> read;
// An initial rtc memory is random.
bool ok = system_rtc_mem_read(USER_DATA_ADDR, &read, sizeof(read));
if (ok) {
// Only hashes are matched and copy to struct.
if (read.hash == calc_hash(read.data)) {
memcpy(this, &read, sizeof(read));
}
}
return ok;
}
bool write() {
hash = calc_hash(data);
return system_rtc_mem_write(USER_DATA_ADDR, this, sizeof(*this));
}
T* operator->() {
return &data;
}
};
view raw rtc_memory.cpp hosted with ❤ by GitHub

実測

説明をいれると

約5秒でAPに接続できている。その後UDPで送信し、送信が終わるのをちょっと無駄に1秒待っている。

V: 31.72mA となっているのが画面内の平均だけど、WiFi 接続が2回起きているので、1サイクルの平均ではない。

1サイクル(約16秒)あたりは、WiFi に 80mA 6秒、スリープ 20uA 10秒とし平均30mA。

もし単三 NiMH 2400mAh 3本(公称1.2V * 3 = 3.6V)をLDOで3.3Vにして電源にしたとすると、バッテリ容量は2400mAh * 1.2V * 3 = 8640mWh。このケースのESP8266の平均消費電力は 3.6V * 30mA = 108mW (LDO損失こみ)。8640 / 108 = 80時間。

支配的なのは圧倒的にWiFi接続時間なので、とにかく周期を長くとれば、それだけ電池寿命は伸びる。実際のところ電池3本で実用的なのは5分以上かなあという気がする (5分間隔でも2ヶ月ぐらい)。

これよりも短い間隔でデータのやりとりをする場合、電池駆動は考えないほうがよさそう。どうしても電池駆動したい場合WiFi 使うのが間違っているので ZigBee とか使うべきなんだと思う。とはいえ ESP8266 が圧倒的に安すぎるので、なんでも WiFi でやりたくなってしまう。

  1. トップ
  2. tech
  3. メモ: ESP8266 での実測電流

経緯

メモ: ESP8266 での実測電流 | tech - 氾濫原 というのを書いたあと、WiFi のコネクションなしでもデータを適当に送信できたらいいのではないか?と考えました。

具体的には SSID などをビーコン情報としてコネクションレスでブロードキャストしているのだから、これにセンサーデータをつけてしまえばいいだろう、というアイデアです。

ということで ESP8266 の API を調べてみましたが、どうやら WiFi のフレームを生で扱うようなAPIはないようでした。じゃあSSID にデータを入れて softap としてビーコンを発射すればとも思いましたが、さすがに邪悪すぎます。

もうちょっとAPIを眺めていると ESP-NOW というのがあることに気付きました。独自拡張のような形でIEEE802.11のフレームを飛ばしてWiFiコネクションレスで送受信できるもののようです。これを使えば前述のような邪悪なことをしなくても同じようなことができそうです。

ESP-NOW の概念

常時データを受けとるスレーブと、必要なときにデータを送信するコントローラに分かれます (送受信はどちらもできる)。スレーブは常時チャンネルを受信していなければならないので、基本的に固定の電源が必要です。コントローラはデータを投げるだけなのでバッテリ駆動できます。

送受信するまえにお互いの Mac アドレスを知っている必要があります。ESP-NOW としては、お互いに一旦 softap となって BSSID (Macアドレス) を公開してペアリングすることを想定しているようです。

なお、(おそらく)フレームをそのまま送信しているだけなので、確実に到達する保証はなく、もし要求されるならACKみたいなものは自力で実装する必要もあります。

暗号化のサポートもあるようですがそこまでとりあえずは調べていません。

実装

スレーブとコントローラいずれも ESP8266 である必要があるので (ローレベルなIEEE802.11のフレームを扱えるなら互換デバイスは作れそうですが)、ESP8266 は2台必要です。

ペアリング部分はめんどうなので、肝心のところだけ試しに実装して試してみました。

スレーブ

スレーブはデータを受けとって表示するだけです。現実的なアプリケーションなら、STATION+AP モードにして、別のホストに中継するように動作させると思いますが、今回はAPモードで動作させています。

esp_now_register_send_cb してますが send してないので呼ばれません。

ピアのMacアドレスは、STATION_IF のMacアドレスです。

#include <Arduino.h>
#include <ESP8266WiFi.h>
extern "C" {
	#include <espnow.h>
	#include <user_interface.h>
}

#define WIFI_DEFAULT_CHANNEL 1

uint8_t mac[] = {0x5C,0xCF,0x7F,0x8,0x37,0xC7};

void printMacAddress(uint8_t* macaddr) {
	Serial.print("{");
	for (int i = 0; i < 6; i++) {
		Serial.print("0x");
		Serial.print(macaddr[i], HEX);
		if (i < 5) Serial.print(',');
	}
	Serial.println("}");
}

void setup() {
	pinMode(13, OUTPUT);

	Serial.begin(74880);
	Serial.println("Initializing...");

	WiFi.mode(WIFI_AP);
	WiFi.softAP("foobar", "12345678", 1, 0);

	uint8_t macaddr[6];
	wifi_get_macaddr(STATION_IF, macaddr);
	Serial.print("mac address (STATION_IF): ");
	printMacAddress(macaddr);

	wifi_get_macaddr(SOFTAP_IF, macaddr);
	Serial.print("mac address (SOFTAP_IF): ");
	printMacAddress(macaddr);

	if (esp_now_init() == 0) {
		Serial.println("init");
	} else {
		Serial.println("init failed");
		ESP.restart();
		return;
	}

	esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
	esp_now_register_recv_cb([](uint8_t *macaddr, uint8_t *data, uint8_t len) {
		Serial.println("recv_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("data: ");
		for (int i = 0; i < len; i++) {
			Serial.print(" 0x");
			Serial.print(data[i], HEX);
		}
		Serial.println("");
	});
	esp_now_register_send_cb([](uint8_t* macaddr, uint8_t status) {
		Serial.println("send_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("status = "); Serial.println(status);
	});

	int res = esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_CONTROLLER,(uint8_t)WIFI_DEFAULT_CHANNEL, NULL, 0);

//	esp_now_unregister_recv_cb();
//	esp_now_deinit();
}

void loop() {
}

コントローラ

スレーブとほぼ同じですが ROLE が変わっています。適当なバイト列を esp_now_send() しています。

ピアのMacアドレスは、SOFTAP_IF のMacアドレスです。

#include <Arduino.h>
#include <ESP8266WiFi.h>
extern "C" {
	#include <espnow.h>
	#include <user_interface.h>
}

#define WIFI_DEFAULT_CHANNEL 1

uint8_t mac[] = {0x1A,0xFE,0x34,0xEE,0x84,0x88};

void printMacAddress(uint8_t* macaddr) {
	Serial.print("{");
	for (int i = 0; i < 6; i++) {
		Serial.print("0x");
		Serial.print(macaddr[i], HEX);
		if (i < 5) Serial.print(',');
	}
	Serial.println("}");
}

void setup() {
	pinMode(13, OUTPUT);

	Serial.begin(74880);
	Serial.println("Initializing...");
	WiFi.mode(WIFI_STA);

	uint8_t macaddr[6];
	wifi_get_macaddr(STATION_IF, macaddr);
	Serial.print("mac address (STATION_IF): ");
	printMacAddress(macaddr);

	wifi_get_macaddr(SOFTAP_IF, macaddr);
	Serial.print("mac address (SOFTAP_IF): ");
	printMacAddress(macaddr);

	if (esp_now_init()==0) {
		Serial.println("direct link  init ok");
	} else {
		Serial.println("dl init failed");
		ESP.restart();
		return;
	}

	esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
	esp_now_register_recv_cb([](uint8_t *macaddr, uint8_t *data, uint8_t len) {
		Serial.println("recv_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("data: ");
		for (int i = 0; i < len; i++) {
			Serial.print(data[i], HEX);
		}
		Serial.println("");
	});
	esp_now_register_send_cb([](uint8_t* macaddr, uint8_t status) {
		Serial.println("send_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("status = "); Serial.println(status);
	});

	int res = esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_SLAVE,(uint8_t)WIFI_DEFAULT_CHANNEL, NULL, 0);

	uint8_t message[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08 };
	esp_now_send(mac, message, sizeof(message));
	ESP.deepSleep(2.5e6, WAKE_RF_DEFAULT);

//	esp_now_unregister_recv_cb();
//	esp_now_deinit();
}

void loop() {
}

動作と消費電力

こんな普通に簡単な実装を書いてみたら普通に動きました。

スレーブ側のシリアル出力

コントローラの電流値の推移

この実装だと 2.5秒ごとに毎回送信しているので割と平均消費も大きいです。しかしそれでも、前回の16秒ごとのWiFi接続の30mAの半分以下です。

コントローラの別実装

ということで、2.5秒おきに deep sleep から復帰して値を RTC メモリ書きつつ、4回に1度はWiFiで送信するようなサンプルも書いてみました。これは前回のWiFiコネクション+UDPの部分をESP-NOWを使うように置き換えたものになります。

エントリ冒頭の画像はこれの横軸を広げたものです。

平均で6.8mAぐらいです。

備考

この実装ではフレームの再送などはしていないので、データが欠落する可能性があります。とはいえ UDP の実装でも同じことがいえるので、これが原因だからダメということにはならないでしょう。

しかしこの実装だとMacアドレス以外一切何も確認していないので、もし悪意のある人が近くにいれば変なデータを送りこむのは容易です。

ESP8266 を使ってできる無線通信ではこれ以上の省電力化は難しそうな気がします。あとは送信出力を下げるぐらいしかなさそうです。

  1. トップ
  2. tech
  3. ESP8266 の低消費電力の限界をさぐる (ESP-NOWを使ってみる)