ESP-WROOM-32 SPIフラッシュのメモリマップ
ESP32モジュールのSPIフラッシュ内には、nvs (non volatile storage)と呼ばれる領域(0x9000から20Kバイト) があり、文字どおり不揮発領域として使われている。フラッシュメモリなのでモジュールの電源を落としても内容が消えないのは当たり前なのだけど、Arduino IDEからスケッチを書き込んだ際にも、この領域の内容は上書きされない。なので、スケッチが利用する諸情報をあらかじめnvs領域に仕込んでおき、スケッチ自体ではそれらをあたかもプロパティのように使うことができる(と思う)。
一つ前の話で、Arduino IDEからコンパイルしたスケッチをフラッシュに書き込む際には、スケッチを含めて4つのブロックのデータが転送されると書いたのだけど、パーティションテーブルの内容などを参考に転送先のイメージを図にすると以下のようになる。
(注: 現時点でのパーティションテーブル(default.csv) に定義されているのは0x9000以降だが、転送時のメッセージからすると上記のようになっていると推測した。また、パーティションテーブルの定義はシステムの更新に応じてどんどん変更されるかもしれない)。
このメモリマップのうち、nvs領域として使われているのは 0x9000 からの20K(0x5000) バイトのようである。パーティションテーブル領域自体はPCで作成されたファイルがESP32モジュールに転送されて書き込まれるので更新されるが、転送されるバイナリのサイズは4Kバイト未満なのでnvs領域の内容が上書きされることはない。
nvs領域を読み出してみる
nvs領域もフラッシュメモリの一部なので、esproolを使って読み出すことができる。
esptool.exe --port COM3 --baud 921600 read_flash 0x9000 0x5000 part.bin
を実行してpart.binというファイルに吸い上げた。この内容の一部をダンプしてみると、次のような具合。
0x00040以降の32バイトごとにキーのインデックスらしきものが入っている。nvsの概要、使い方、内部構造についてはNon-volatile storage library というドキュメントを参照。
上記のダンプからは、ネームスペース”misc”が内部インデックス番号 1 を、ネームスペース “nvs.net80211″がインデックス番号2 をもっており、”misc”には”log”というキー文字列が、”nvs.net80211″にはキー名 “opmode”や “country”といったキー文字列が登録されていることが読み取れる。内部解析じみたことをやってもあまり得るものがないので、nvs用のapiを使って内容を読み出してみることにした。
今回のプログラムは、ESP32モジュールを使い始めたときから気になっている、アクセスポイント名やパスワードがnvsに保存されていることと、プログラムから読み出しが可能なことを確認するために作成した。
NvsDump.h
nvs内の指定のネームスペース、指定のキー名のデータをblob型として読み出してシリアルモニタにダンプする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
#include <nvs.h> #include <nvs_flash.h> #include "esp_err.h" class NvsDump { nvs_handle h; bool got_handle = false; esp_err_t last_err; size_t blob_bytes = 0; char* pblob = 0; public: NvsDump(const char* name) { nvs_flash_init(); if ((last_err = nvs_open(name, NVS_READONLY, &h)) != ESP_OK) { Serial.print("nvs_open failed. return "); Serial.println(last_err, 16); } else got_handle = true; } ~NvsDump() { clear(); if (got_handle) nvs_close(h); } void clear() { if (pblob) free(pblob); pblob = 0; blob_bytes = 0; } static String bin2hex(byte b) { static const char* cp = "0123456789abcdef"; return String(cp[(b >> 4) & 0xf]) + String(cp[(b & 0xf)]); } void dump_blob() { if (!pblob || blob_bytes < 1) return; int index = 0; String hexes = ""; String chars = ""; while( index < blob_bytes ) { byte b = pblob[index]; hexes += bin2hex(b); if (b > 0x1f && b < 0x80) chars += String(pblob[index]); else chars += "."; hexes += " "; index++; if (index % 16 == 0 || index == blob_bytes) { if (index % 16 != 0) { for(int i = 0; i < 16 - (index % 16); i++) { hexes += " "; chars += " "; } } Serial.print(hexes); Serial.print(" "); Serial.println(chars); hexes = ""; chars = ""; } } } bool read_blob(const char* key) { clear(); if ((last_err = nvs_get_blob(h, key, NULL, &blob_bytes)) != ESP_OK) { Serial.print("nvs_get_blob " + String(key) + " failed. return "); Serial.println(last_err, 16); return false; } Serial.print("nvs_get_blob( " + String(key) + " ) return "); Serial.println(String(blob_bytes) + " bytes"); pblob = (char*)malloc(blob_bytes); if (pblob) { if ((last_err = nvs_get_blob(h, key, pblob, &blob_bytes)) == ESP_OK) return true; Serial.print("nvs_get_blob failed. return "); Serial.println(last_err, 16); clear(); } return false; } }; |
nvs_open() を NVS_READONLY 以外で呼ぶと、存在しないネームスペースを指定したり、存在しないキー名を指定すると、どんどん作られてしまうかもしれないことに注意が必要。
このクラスを使う実装側として、WIFIステーションとして指定したアクセスポイントに接続するだけのスケッチを用意した。
ESP32_WIFI_STA1.ino
このスケッチをNvsDump.hと同じディレクトリに置いておき、Arduino IDEで開くと2つのファイルがおのおのソースタブとして表示されるので、選択/編集が容易にできる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// esp32 wifi sta #include <WiFi.h> #include "NvsDump.h" bool connect_to_ap() { WiFi.begin("AndroidAP", "esp32passwd"); int n = 0; int counter = 0; while ((n = WiFi.status()) != WL_CONNECTED) { Serial.print(String(n) + " "); delay(400); if (counter++ > 25) { Serial.println("\nwifi-connect failed\nprintDiag()\n"); WiFi.printDiag(Serial); return false; } } return true; } void setup() { NvsDump nvs("nvs.net80211"); Serial.begin(115200); delay(10); Serial.println(""); WiFi.mode(WIFI_STA); WiFi.setAutoConnect(false); if (connect_to_ap()) { Serial.println("\nconnected. WiFi.printDiag()\n"); WiFi.printDiag(Serial); Serial.println(WiFi.localIP().toString()); } Serial.println(""); if (nvs.read_blob("ap.mac")) nvs.dump_blob(); if (nvs.read_blob("ap.ssid")) nvs.dump_blob(); if (nvs.read_blob("ap.passwd")) nvs.dump_blob(); if (nvs.read_blob("sta.mac")) nvs.dump_blob(); if (nvs.read_blob("sta.ssid")) nvs.dump_blob(); if (nvs.read_blob("sta.pswd")) nvs.dump_blob(); } void loop() { delay(10); } |
指定のアクセスポイント(今回は、AndroidスマホのWiFIティザリング機能を使った)に接続し、その状態で関係ありそうな項目をnvsから読み出してシリアルモニタに表示する。このスケッチではWIFI_STAモードしか使わないが、前回書いたスケッチでのアクセスポイント情報も読み出してみた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
255 [D][WiFiGeneric.cpp:182] _eventCallback(): Event: 2 - STA_START 6 6 [D][WiFiGeneric.cpp:182] _eventCallback(): Event: 4 - STA_CONNECTED [D][WiFiGeneric.cpp:182] _eventCallback(): Event: 7 - STA_GOT_IP connected. WiFi.printDiag() Mode: STA Channel: 6 Auto connect: 0 SSID (9): AndroidAP Passphrase (11): esp32passwd BSSID set: 0 192.168.43.170 nvs_get_blob( ap.mac ) return 6 bytes 30 ae a4 05 62 f5 0...b. nvs_get_blob( ap.ssid ) return 36 bytes 07 00 00 00 45 53 50 33 32 41 50 00 d0 cc fc 3f ....ESP32AP....? 00 00 00 00 00 00 00 00 00 00 00 00 11 1c 08 40 ...............@ 03 00 00 00 .... nvs_get_blob( ap.passwd ) return 65 bytes 70 61 73 73 77 6f 72 64 00 00 00 00 00 00 00 00 password........ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 . nvs_get_blob( sta.mac ) return 6 bytes 30 ae a4 05 62 f4 0...b. nvs_get_blob( sta.ssid ) return 36 bytes 09 00 00 00 41 6e 64 72 6f 69 64 41 50 00 fc 3f ....AndroidAP..? 40 a3 fc 3f 01 00 00 00 9e 6d 0f 80 00 cc fc 3f @..?.....m.....? 10 00 00 00 .... nvs_get_blob( sta.pswd ) return 65 bytes 65 73 70 33 32 70 61 73 73 77 64 00 00 00 00 00 esp32passwd..... 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 |
上記のように、WIFI_STAでの接続情報だけではなく、別のスケッチで書いたWIFI_AP用の設定情報(ap.ssidとap.passwd)もちゃんと残ってました。
さらに、下記のようなnvs内容の表示だけを行うスケッチを実行しても、シリアルモニタには同じ内容が表示される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include "NvsDump.h" void setup() { NvsDump nvs("nvs.net80211"); Serial.begin(115200); delay(10); Serial.println(""); if (nvs.read_blob("ap.mac")) nvs.dump_blob(); if (nvs.read_blob("ap.ssid")) nvs.dump_blob(); if (nvs.read_blob("ap.passwd")) nvs.dump_blob(); if (nvs.read_blob("sta.mac")) nvs.dump_blob(); if (nvs.read_blob("sta.ssid")) nvs.dump_blob(); if (nvs.read_blob("sta.pswd")) nvs.dump_blob(); } void loop() { delay(10); } |
先の書いた方の ESP32_WIFI_STA1.ino というスケッチでは、 WiFi.setAutoConnect(false); によってリスタート時の自動接続を禁止しているが、自動接続を許可している場合はnvsから接続先情報を読み出して自動接続すると思われる。
きょうのまとめ
nvs apiには保存されているネームスペースの一覧や、あるネームスペースに登録されているキー名の一覧を得る方法がない。しょうがないので最初に吸い上げた part.bin のダンプからそれらしいものを探して使った。
同じ機能をもつが接続先AP名がいろいろなWiFiステーションだったり、同じ場所にWIFI_AP機能をもつ複数のモジュールを配置するような場合、nvsを外から書き換える方法だけ考えておけば、プロパティ用にファイルシステムを使うこともないし、スケッチも一つで済むから楽かもしれない。何か思いついたら使ってみることにしよう。