ESP8266 の deep sleep モードは CPU などの電源を全て落として、RTC (時刻を持ってるわけではないっぽいけど内蔵RTCがあるらしい) だけ生かして設定時刻になったら RTC にリセットさせるという挙動をする。

そういうわけで、ディープスリープ後はリセット直後と同じ状態になり、メモリなどは引き継げない。

もちろん deep sleep 時には wifi の接続も切れてしまうので、毎回繋ぎなおしになる。自分の環境だとAPに接続するまで5秒ぐらいかかるし、接続中は電流が80mA(データシート的には最大で180mA)ぐらい流れる。高い頻度で起動すると初期化のコストがかなり大きい。

ユースケース

センサーデータは数秒に一度取得したいが、データ送信はさらに少ない頻度でどこかに送信したい、というような場合。

system_rtc_mem_read / system_rtc_mem_write

ESP8266 自体にこういうAPIがあり、RTC にあるメモリ(揮発性)を読み書きすることができる。RTC は deep sleep でも有効なので、これは電源が接続されている限り保持されるメモリのようだ (電源を切ると初期化される)。ユーザ領域として最大512bytes使えることになっている。

ということで、これを使って起動回数を記録しつつ、何回かに一度 WiFi 接続するようなサンプルを書いてみた (実際接続はしてないけど)

未初期化の system_rtc_mem_read は不定値がかえってくるみたいなので、CRC なりなんなりでハッシュをとって明示的に初期化をしたほうがよさそう。

#include <Arduino.h>
extern "C" {
#include <user_interface.h>
};
	
// system_rtc_mem_write() 先のブロックアドレス。
// 4 bytes で align されており、先頭256bytes はシステム予約領域
// 64 から書けるはずだが、65 以降でないとうまくいかなかった。。
static const uint32_t USER_DATA_ADDR = 66;

// ハッシュ関数 (FNV) CRC でいいけどコード的に短いのでFNV
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;
}

// struct の hash (先頭にあることを想定) を除くデータ部分のハッシュを計算する
template <class T>
uint32_t calc_hash(T& data) {
	return fnv_1_hash_32(((uint8_t*)&data) + sizeof(data.hash), sizeof(T) - sizeof(data.hash));
}

struct {
	// retain data
	uint32_t hash;
	uint16_t count;
	uint8_t  send;
	uint16_t etc2;
} retain_data;

void post_sensor_data();

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

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

	// データ読みこみ
	bool ok;
	ok = system_rtc_mem_read(USER_DATA_ADDR, &retain_data, sizeof(retain_data));
	if (!ok) {
		Serial.println("system_rtc_mem_read failed");
	}
	Serial.print("retain_data.count = ");
	Serial.println(retain_data.count);

	// ハッシュが一致していない場合、初期化されていないとみなし、初期化処理を行う
	uint32_t hash = calc_hash(retain_data);
	if (retain_data.hash != hash) {
		Serial.println("retain_data may be uninitialized");
		retain_data.count = 0;
		retain_data.send = 0;
	}

	// データの変更処理(任意)
	retain_data.count++;
	if (!retain_data.send) {
		// 4回に1度送信する
		retain_data.send = retain_data.count % 4 == 0;
	} else {
		Serial.println("send data");
		retain_data.send = 0;
		// なんか定期的に書きこみたい処理
		post_sensor_data();
	}

	// 書きこみ処理。hash を計算していれておく
	retain_data.hash = hash = calc_hash(retain_data);
	ok = system_rtc_mem_write(USER_DATA_ADDR, &retain_data, sizeof(retain_data));
	if (!ok) {
		Serial.println("system_rtc_mem_write failed");
	}

	// 動作確認用のダミー
	digitalWrite(13, HIGH);
	delay(1000);
	digitalWrite(13, LOW);

	if (retain_data.send) {
		ESP.deepSleep(1e6, WAKE_RF_DEFAULT);
	} else {
		// sendしない場合は WIFI をオフで起動させる
		ESP.deepSleep(1e6, WAKE_RF_DISABLED);
	}
}

void loop() {
}

// dummy
void post_sensor_data() {
	for (uint i = 0; i < 5; i++) {
		digitalWrite(13, HIGH);
		delay(300);
		digitalWrite(13, LOW);
		delay(300);
	}
}
  1. トップ
  2. tech
  3. ESP8266 (ESP-WROOM-02) の Deep Sleep でデータを引き継ぐ

こんな感じで遊べる。

HttpClient (portformio だと id=66) の使いかたで微妙にハマった。

こんな感じの GrowthForecastClient クラスをつくっておいて、

HttpClient とホスト名や認証情報を渡して

WiFiClient wifiClient;
HttpClient http(wifiClient);
GrowthForecastClient gf(http, GF_HOST, GF_USER, GF_PASS);

https://github.com/cho45/esp-wroom-02-sketch/blob/master/src/main.cpp#L24

post する

	float temp = adt7410.read();
	Serial.print("adt7410 = ");
	Serial.println(temp);
	gf.post("/home/test/temp", (int32_t)(temp * 1000));

https://github.com/cho45/esp-wroom-02-sketch/blob/master/src/main.cpp#L165

GF は整数しか扱えないので、1000倍してpostして、GFのグラフの設定で ÷ 1000 を選択する。

HttpClient 以外はとくにハマりどころはない。適当にグラフ化するならこれ以上簡単なものはない。

ただ、結構 post に時間がかかる。特に request body にパラメータを書こうとするとあまりにも遅いのでクエリパラメータにしている。なんかどっかですごい効率が悪いことをしていそうだがよくわからない。

  1. トップ
  2. tech
  3. ESP8266 から GrowthForecast へセンサーデータをPOST

cmake で CGI (blosxom クローンぽいやつ) を書きました。(実用のためというわけではないです)

cmake は -P をつけるとスクリプトモードになるので、それでふつうにCGIのコードを書くだけです。

データ型として文字列しかないのがつらい感じです。リストみたいなのも使えるけど、これもセミコロン区切りの文字列をリストとみなすという仕様になっている。シェルスクリプトのノリに近い。

ただ、正規表現がある程度使えるので、文字列操作は割と楽に書くことができる。

なお、cmakeで普通使うような命令を使わない(使えない)のでまったく通常ユースのためのノウハウはほとんど溜まらず、学習という意味ではあんまり意味がないというオチでした。

  1. トップ
  2. tech
  3. cmake で CGI を書く

ただのちっちゃい Linux だと思っていたけど、RTOS が入っているコプロセッサも持っていて、ホストCPUとシームレスに連携できるということがわかった。

アーキテクチャを見ると思いのほか面白かった。

https://software.intel.com/en-us/creating-applications-with-mcu-sdk-for-intel-edison-board

ホストCPU上のLinuxからは、GPIOが sysfs 経由 (または Intel の提供するSDKなど) から見える。それと同時にコプロセッサの MCU からは直接 GPIO が見える。

ホストCPUのLinuxはリアルタイムではないので、最低でも10ms程度(カーネルの割込み頻度の設定による)、処理が遅延する。GPIO を正確にコントロールする場合この遅延は大きすぎる。

一方コプロセッサのMCUはリアルタイムになっており、クロック100MHzなので、最速では 20ns ぐらいの単位でコントロールできる。

Edison はこれらをうまく協調して動かせるように設計されているみたい。MCU 側へファームウェアを書きこんで /dev/ttymcu* で連携したりとか、ホストCPUをスリープさせてMCUだけで動かしつつホストCPUをいい感じのタイミングで起こせるとか。

おもしろい!!

Intel ボードコンピューター Intel Edison Kit for Breakout Board(MM#939977) EDI2BB.AL.K -

3.0 / 5.0

なお未だ買ってない。

  1. トップ
  2. tech
  3. Intel Edison のおもしろいところ