コード

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

  • 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を使ってみる)

ESP8266 (ESP-WROOM-02) の Deep Sleep でデータを引き継ぐ | tech - 氾濫原 を試しているとき、起動時に必ず一定のゴミっぽいのがつくので気になっていた。

が、実はこれブート時に必ずでるメッセージのようで、ボーレートがあっていない。ボーレートを 74880 にすると (書きこむコードでも 74880 を使うように変更)

と、完全なメッセージがみれる。ちなみに rst cause (リセット原因) は RTC メモリのシステム用の領域に入っているみたい。

そういうわけなので、デバッグ用に UART 使うなら 74880 にあわせておいたほうが気持ち悪くない。

  1. トップ
  2. tech
  3. ESP8266 起動時にシリアルにでるゴミっぽいもの

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 のおもしろいところ

アナログ回路への理解を深めたい。まだ全然、やりたいことをすぐに実現できるレベルにならない。どうすればできるのかわからない、ということが多すぎる。

2015年もたくさんコードかきました。他人に承認されないことはせめて自分で承認しましょう。つらい

Chemr

[tech] リファレンスマニュアルをインクリメンタル検索するやつを Electron で実装した | Wed, Oct 14. 2015 - 氾濫原

ドキュメントビューワ。Mac AppStore まで出してみたが全く売れておりません。自分では便利に使ってる

Arduino-meta

[tech] Arduino の digitalWrite / pinMode / digitalRead をコンパイル時に展開する | Wed, Dec 16. 2015 - 氾濫原

digitalWrite とかを静的に解決するやつ。

go-KX3-panadapter

[tech] もう僕らは OpenGL ライブラリにリンクするビルドに悩むことはない | Thu, Oct 8. 2015 - 氾濫原

KX3(無線機)用のパナダプター(Panoramic Adapter = FFTウォーターフォール) WebGL に書きなおしたりした。便利

GrblServer

[tech] CNC フライス Grbl 制御編 | Sun, Aug 16. 2015 - 氾濫原

CNCフライス制御用のアプリケーション。最近あんまり切削してない。

KeyCast

[tech] スクリーンキャスト用のキーストローク表示アプリ | Sat, Feb 14. 2015 - 氾濫原

入力したキーを画面に表示するやつ。

CopyHook

[tech] CopyHook というペーストボードの中身をいじるツールを作った | Wed, Mar 18. 2015 - 氾濫原

クリップボードコピーしたときにフックでJS実行するやつ。なんかうまく動いてない気がするがデバッグの時間がとれない。

reedsolomon.js

[tech] JavaScript で書かれたリードソロモン符号のエンコーダ・デコーダ | Tue, Mar 31. 2015 - 氾濫原

任意のリードソロモン(誤り訂正)符号のエンコーダデコーダ。Zxing (Java) の一部ライブラリの移植

webaudio-filter-frequency-response

[tech] WebAudio の BiquadFilterNode の周波数特性をグラフにするやつ | Fri, Mar 20. 2015 - 氾濫原

WebAudio のフィルタの周波数特性を出すやつ。

dekaimoji-a4

[tech] デカい文字をA4で分割して印刷するツールをJSで書いた | Sat, Mar 7. 2015 - 氾濫原

https://cho45.stfuawsc.com/dekaimoji-a4/

A4プリンタでデカい文字が印刷できるやつ。

  1. トップ
  2. tech
  3. 2015年つくったもの

ビットフィールドとは

C/C++にはほとんど使われてないがビットフィールドという機能がある。

union {
	uint8_t raw;
	struct {
		unsigned FAULT_QUEUE      : 2;
		unsigned CT_PIN_POLARITY  : 1;
		unsigned INT_PIN_POLARITY : 1;
		unsigned INT_CT_MODE      : 1;
		unsigned OPERATION_MODE   : 2;
		unsigned RESOLUTION       : 1;
	};
} config;

このように書ける。struct 内で名前の後ろについているのが、そのフィールドで消費するビット数で、この場合合計で8bitになり、それを uint8_t と共用している。

こうすると config.OPERATION_MODE = 2; などと、マスクやシフトを伴わずに直接書けて、結果をconfig.rawでとれる。

めっちゃ便利なので使わない手はなさそうだと思いきや、実際のところ実用するのは不安がある。というのも、この struct 内のビット配置の順序は実装依存となっていて、uint8_t として評価したとき、どのような結果が返ってくるか確かなことがいえない。

コンパイラ依存

再発明

そこで、上記のようなビットフィールドを以下のように書きなおす

template <class T, uint8_t s, uint8_t e = s>
struct bits {
	T ref;
	static constexpr T mask = (T)(~( (T)(~0) << (e - s + 1))) << s;
	void operator=(const T val) { ref = (ref & ~mask) | ((val & (mask >> s)) << s); }
	operator T() const { return (ref & mask) >> s; }
};

template <uint8_t s, uint8_t e = s>
using bits8 = bits<uint8_t, s, e>;


union {
	uint8_t raw = 0;
	bits8<0, 1> FAULT_QUEUE      ;
	bits8<2>    CT_PIN_POLARITY  ;
	bits8<3>    INT_PIN_POLARITY ;
	bits8<4>    INT_CT_MODE      ;
	bits8<5, 6> OPERATION_MODE   ;
	bits8<7>    RESOLUTION       ;
} config;

uint8_t 全体を明確に共用する複数のstructという形にし、明示的にビットシフトやマスクを行っている。それぞれ、テンプレートの第一引数〜第二引数のビットを扱うクラスになっている。

用途

組み込みで他のデジタルICとやりとりをする場合、だいたいデータシートには [0:1] foobar みたいな形でビット範囲と値の説明が書いてあるので、それをその通り書きうつして union を作れば間違いなくビット操作できる状態になる。

これで安心してビットフィールドっぽいものが使える。

生成バイナリ

試した限りだと完全にインライン化される。また、1bitだけ書く場合andかorだけにまで最適化される。

int main(void) {
	asm volatile ("nop");
	config.OPERATION_MODE = 0b11;
	asm volatile ("nop");
	config.RESOLUTION = 1;
	asm volatile ("nop");
	config.FAULT_QUEUE = 1;
	asm volatile ("nop");

	for (;;) {
	}

	return 0;
}


こういうコードは

000000a0 <main>:
  a0:	00 00       	nop
  a2:	00 00       	nop
  a4:	00 00       	nop
  a6:	80 91 00 01 	lds	r24, 0x0100
  aa:	8c 71       	andi	r24, 0x1C	; 28
  ac:	81 6e       	ori	r24, 0xE1	; 225
  ae:	80 93 00 01 	sts	0x0100, r24
  b2:	00 00       	nop
  b4:	ff cf       	rjmp	.-2      	; 0xb4 <main+0x14>

こうなる

ref. https://gist.github.com/cho45/397f834791bae67166e1

テスト

#include <cstdio>
#include <stdint.h>
#include <iostream>
template <class T, class U>
void is(T got, U expected) {
if (got == expected) {
std::cout << "ok" << std::endl;
} else {
std::cout << "not ok " << got << " != " << expected << std::endl;
}
}
template <class T, uint8_t s, uint8_t e = s>
struct bits {
T ref;
static constexpr T mask = (T)(~( (T)(~0) << (e - s + 1))) << s;
void operator=(const T val) { ref = (ref & ~mask) | ((val & (mask >> s)) << s); }
operator T() const { return (ref & mask) >> s; }
};
template <uint8_t s, uint8_t e = s>
using bits8 = bits<uint8_t, s, e>;
int main () {
union {
uint8_t raw = 0;
bits8<0, 1> FAULT_QUEUE ;
bits8<2> CT_PIN_POLARITY ;
bits8<3> INT_PIN_POLARITY ;
bits8<4> INT_CT_MODE ;
bits8<5, 6> OPERATION_MODE ;
bits8<7> RESOLUTION ;
} config;
config.OPERATION_MODE = 0b11;
is((uint)config.raw, 0b01100000);
config.FAULT_QUEUE = 0b10;
is((uint)config.raw, 0b01100010);
config.RESOLUTION = 1;
is((uint)config.raw, 0b11100010);
config.OPERATION_MODE = 0;
is((uint)config.raw, 0b10000010);
config.raw = 0;
is((uint)config.OPERATION_MODE, 0b00);
config.raw = 0b01000000;
is((uint)config.OPERATION_MODE, 0b10);
config.FAULT_QUEUE = 0b111;
is((uint)config.raw, 0b01000011);
return 0;
}

  1. トップ
  2. tech
  3. C++ でビットフィールドを再発明する

I2Cセンサーとかを扱うと固定小数点表現によく出会う。が、固定小数点のままだと計算がめんどうなので、とりあえず浮動小数点に変換しときたいというケースがまぁまぁある。

そういうときに雑に使えるスニペットがほしかったので書いた。

#include <type_traits>
template <uint8_t int_bits, uint8_t fractional_bits, class T>
inline float fixed_point_to_float(const T fixed) {
	static_assert(std::is_unsigned<T>::value, "argument must be unsigned");
	constexpr uint8_t msb = int_bits + fractional_bits - 1;
	constexpr T mask = static_cast<T>(~(( static_cast<T>(~0)) << msb));
	constexpr float deno = 1<<fractional_bits;
	if (fixed & (1<<msb)) {
		// negative
		return -( ( (~fixed & mask) + 1) / deno);
	} else {
		// positive
		return fixed / deno;
	}
}

type_traits がない環境の場合、include と static_assert を消すだけで動く。これはエラーチェックにしか使ってなくて、もし消したとしても、負の signed を渡すと左シフトが不正になるのでエラーになる。

センサ出力とかの場合、8bit単位のビット数ではないことが多いので、渡された型のサイズに関わらずに処理できるようにマスクを作っている。

// usage
int main (int argc, char* argv[]) {
	// from ADT7410 datasheet
	is(fixed_point_to_float<9, 4>((uint16_t)0b0000000000001), 0.0625);
	is(fixed_point_to_float<9, 4>((uint16_t)0b0100101100000), 150.0);
	is(fixed_point_to_float<9, 4>((uint16_t)0b0000000000000), 0);
	is(fixed_point_to_float<9, 4>((uint16_t)0b1110010010000), -55.0);
	is(fixed_point_to_float<9, 4>((uint16_t)0b1111111111111), -0.0625);
	is(fixed_point_to_float<9, 4>((uint32_t)0b1111111111111), -0.0625 );
	/** compile error : argument must be unsigned
	printf("%f\n", fixed_point_to_float<9, 4>((int16_t)0b1111111111111));
	*/

	// from MCP3425 datasheet
	is(fixed_point_to_float<1, 11>((uint16_t)0x001) * (2.048), 1e-3);
	is(fixed_point_to_float<1, 13>((uint16_t)0x001) * (2.048), 250e-6);
	is(fixed_point_to_float<1, 15>((uint16_t)0x001) * (2.048), 62.5e-6);

	// from MPL115A2 datasheet
	// a0 coefficient
	is(fixed_point_to_float<13, 3>((uint16_t)0x3ECE), 2009.75);
	// b1 coefficient
	is(fixed_point_to_float<3, 13>((uint16_t)0xB3F9), -2.37585);
	// b2 coefficient
	is(fixed_point_to_float<2, 14>((uint16_t)0xC517), -0.92047);
	// c12 coefficient
	is(fixed_point_to_float<1, 15>((uint16_t)0x33C8)/(1<<9), 0.000790);

	// test dynamic variable
	volatile uint16_t x = 0x001;
	is(fixed_point_to_float<1, 11>(x) * (2.048), 1e-3);
}

テンプレートの第1引数は整数部(符号込み)のビット数・第2引数は小数点分のビット数

これはQ表記に対応する。

Q表記だと Q1.15 だと符号分1・整数部なし・15ビットの小数点桁。Q9.4 だと符号付き整数部8bit、小数部4bit。

メモ

固定小数点数用のクラス作って可能な限りは固定小数点で演算したほうがいい気はする。ヘッダ1ファイルとかで使えるの、当然もうありそうだけど見つけられてない。

  1. トップ
  2. tech
  3. 任意固定小数点→浮動小数点変換スニペット

モバイバッテリーは低電流時、充電完了と判断してパワーオフする(出力回路の動作をやめる)が、これをやらせたくない場合どうすればいいか。現時点でのメモ

Arduino に適当なプログラムを書きこんで、ポートに抵抗を繋ぎ(複数ポートにわけて) パワーオフするかどうかを調べた。測定時は電源供給経路途中に1Ωの抵抗をいれ、オシロでこの抵抗の両端電圧を測ることで間接的に実測の電流値を求めている。

Arduino のベースの消費電力は40mA程度。なので以下でさらに大きな電流を流しているが、40mA との切り替えということになる。

Anker

Anker は製品説明書に最低限充電電流が書いてある (50mA)
しかし連続で流し続けなければならないのか、パルスでいいのかはわからない。

A1208

 -

3.0 / 5.0

検知できないと30秒ぐらいでパワーオフする

  • 50mA 50ms / 1s → ダメ
  • 100mA 50ms / 1s → ダメ
  • 100mA 100ms / 1s → ダメ
  • 100mA 100ms / 10ms → ok
  • 100mA 100ms / 100ms → ok

すくなくとも短いパルスではリセットできないっぽい

  • 100mA 1s / 5s → ok
  • 50mA 1s / 5s → ng
  • 60mA 1s / 5s → ok
  • 60mA 1s / 10s → ok
  • 60mA 1s / 30s → ok
  • 60mA 500ms / 30s → ok
  • 60mA 250ms / 30s → ng

一定時間内の平均消費をみている?

Aukey

PB-T1

 -

3.0 / 5.0

説明書には特にオートパワーオフの閾値の記載なし。

約3分後にパワーオフ。電流を検知している間はバッテリーランプが点灯するっぽい?

  • 60mA 250ms / 30s → ng
  • 100mA 250ms / 30s → ng
  • 100mA 50ms / 1s → ok
  • 100mA 50ms / 10s → ng
  • 100mA 50ms / 5s → ng
  • 100mA 50ms / 2.5s → ok

ランプが5秒ぐらいで消灯するが、その前にパルスを検知すればいいっぽい… 謎

その他

100mA ぐらい常時流しといたらいいんちゃう?

→ 5V 100mA (0.5W)

3.3V 10000mAh のやつは26Whぐらいなので、とても厳しいという状態でなければそれでもいいかもしれない。

Quick Charge バッテリの場合、電流値で検出しているのか電力値で検出しているのかで大きくわかれる。12V 100mA 流すことになったら常時 1.2W 消費ということになりつらい。

  1. トップ
  2. tech
  3. 今夜は寝かさないぞモバイルバッテリー