ESP32でプログラムを作っていると、
「電源を切っても設定値を残したい」
「Wi-Fiの設定を保存したい」
「最後に設定した値を次回起動時にも使いたい」
という場面があります。
通常の変数はRAM上に保存されるため、ESP32の電源をOFFすると内容は失われます。
たとえば、
int temperature = 25;
と変数へ値を保存しても、これは電源OFF後まで保持される設定データにはなりません。
そこでESP32では、Flash Memoryへデータを保存する方法として**NVS(Non-Volatile Storage)**を利用できます。
Arduino-ESP32を使用している場合には、NVSを扱いやすくしたPreferences Libraryも用意されています。EspressifはPreferencesを従来のArduino EEPROM Libraryに代わる方法として案内しています。
この記事では、
NVSとは何か
Preferencesとの違い
実際に値を保存・読み出す方法
Flashの書き換え寿命
NVSへ保存するのに向いているデータ
まで解説します。
なぜ普通の変数ではデータを保存できない?
まずESP32のメモリについて簡単に整理します。
プログラム実行中に使用する変数の多くはRAM上に置かれます。
イメージとしては、
ESP32
RAM
├─ 変数
├─ Stack
└─ Heap
Flash
├─ Firmware
├─ NVS
└─ File Systemなど
という構成です。
RAMは高速に読み書きできますが、電源がなくなると内容が失われる揮発性メモリです。
一方、Flash Memoryは電源を切っても内容を保持できます。
したがって、
電源ON
↓
設定値を変更
↓
電源OFF
↓
次回起動
↓
前回の設定値を復元
という動作を実現するには、設定値を不揮発メモリへ保存する必要があります。
ESP32のNVSとは?
NVSはNon-Volatile Storageの略です。
ESP-IDFに用意されている、Flash Memory上へデータを保存するための仕組みです。
NVSではデータを、
Key → Value
というKey-Value形式で管理します。
たとえば、
"volume" → 8
"temp" → 25
"mode" → 1
というように、データへ名前を付けて保存できます。
さらにNVSではNamespaceを使ってKeyをグループ分けできます。
Namespace : "settings"
├─ "volume" → 8
├─ "temp" → 25
└─ "mode" → 1
別のNamespaceなら、同じKey名を使用しても衝突しません。ESP-IDFのNVS APIでも、nvs_open()でNamespaceを指定し、取得したHandleを使ってnvs_get_*()やnvs_set_*()などを操作する構造になっています。
製品Firmwareでも、
"system"
"network"
"sensor"
"user"
など、用途ごとにデータを整理できます。
NVSにはどんなデータを保存できる?
NVSでは整数だけでなく、複数種類のデータを扱えます。
代表的には、
- 8~64bitの整数
- String
- Blob(Binary Data)
などです。
Arduino-ESP32のPreferencesでは、さらにAPIとして、
putBool()
putInt()
putUInt()
putFloat()
putDouble()
putString()
putBytes()
などが用意されています。
そのため、
温度設定値
音量
動作モード
Calibration値
初期設定完了フラグ
Wi-Fi関連の設定
など、小さな設定値を保存する用途に向いています。
NVSとPreferencesは何が違う?
ここは混同しやすいポイントです。
NVSとPreferencesは別々の保存媒体ではありません。
Arduino-ESP32のPreferences Libraryは、内部でESP32のNVSを利用しています。Espressifの公式Arduino-ESP32 Documentationでも、PreferencesはESP32のNVSを利用してデータを保存すると説明されています。
関係を簡単にすると、
Arduino-ESP32
│
Preferences
│
NVS
│
Flash Memory
となります。
一方、ESP-IDFを直接使用する場合は、
ESP-IDF
│
NVS API
│
Flash Memory
という使い方ができます。
つまり、
PreferencesはNVSをArduinoから扱いやすくしたAPI
と考えると分かりやすいでしょう。
ArduinoでPreferencesを使って値を保存する
Arduino-ESP32では、Preferences Libraryを使用すると比較的簡単にNVSへ値を保存できます。
まず、
#include <Preferences.h>
Preferences preferences;
とします。
そしてNamespaceを開きます。
preferences.begin("settings", false);
"settings"がNamespace名です。
第2引数のfalseはRead/Write Modeを意味します。trueを指定するとRead Onlyになります。
値を保存する場合は、
preferences.putInt("temp", 25);
とします。
これで、
Namespace : settings
Key : temp
Value : 25
として保存できます。
処理が終わったら、
preferences.end();
でNamespaceを閉じます。
保存した値を読み出してみる
次回起動時には、保存した値を読み出せます。
たとえば、
preferences.begin("settings", true);
int temperature = preferences.getInt("temp", 20);
preferences.end();
とします。
ここでは"temp"というKeyから値を取得しています。
第2引数の20は、Keyが存在しなかった場合に返すDefault値です。
つまり初回起動などでまだデータが保存されていなくても、
保存済み
↓
保存値を使用
未保存
↓
Default値を使用
という処理にできます。
PreferencesではNamespaceをRead Onlyで開いても値の読み出しは可能です。値を書き込む場合にはRead/Write Modeで開く必要があります。
電源OFFしても本当に値は残る?
残ります。
PreferencesはESP32のNVSを利用しているため、保存したデータはRestartや電源断をまたいで保持されます。
たとえば、
初回起動
temp = 20
↓
ユーザー操作
temp = 28
↓
NVSへ保存
↓
電源OFF
↓
再び電源ON
↓
NVSから読み出す
temp = 28
という動作を作れます。
設定値を保存する機器では非常によく使う処理です。
初回起動かどうかを判定することもできる
製品では、
工場出荷状態
と
一度設定された状態
を区別したいことがあります。
PreferencesではisKey()を使って、特定のKeyが存在するか確認できます。Espressifの公式Tutorialでも、この方法を利用して初回起動時にDefault設定を作成する例が紹介されています。
たとえば、
preferences.begin("settings", false);
if (!preferences.isKey("init")) {
preferences.putInt("temp", 25);
preferences.putBool("init", true);
}
preferences.end();
とすれば、"init"が存在しない場合だけ初期値を書き込めます。
これを利用すると、
初回起動
↓
Factory Defaultを書き込む
↓
init = true
以降の起動
↓
保存済み設定を読み出す
という構成にできます。
製品Firmwareでは使いやすい方法です。
NVSは何でも保存する場所ではない
ここからが重要です。
NVSは便利ですが、Flashへ保存できるからといって、あらゆるデータをNVSへ書けばよいわけではありません。
EspressifはNVSについて、主に頻繁には変化しないConfiguration Dataの保存を推奨しています。反対に、頻繁かつ大量に更新するLogging用途などには適さないと説明しています。
たとえば、
○ 向いている
温度設定値
Wi-Fi設定
Calibration値
動作モード
ユーザー設定
初期化済みフラグ
に対して、
△~× 向いていない
高速Sampling Data
毎秒更新するLog
大量の画像Data
大きなFile
連続的な履歴Data
という違いがあります。
Arduino-ESP32のPreferences Documentationでも、多数の小さな値の保存に適しており、大量データならLittleFSなどのFile Systemを検討するよう案内されています。
Flashには書き換え寿命がある
NVSの保存先であるSPI NOR Flashには、無制限にErase/Writeできるわけではありません。
そのため、
void loop()
{
preferences.putInt("temp", temperature);
}
のように、Loopを回るたびにFlashへ書き込む設計は避けます。
NVSにはFlashの特定領域だけへ書き込みが集中しないよう、内部にWear Levelingの仕組みがあります。EspressifのFAQでも、NVSには独自のErase/Write balancing mechanismが実装されていることが説明されています。
ただし、
Wear Levelingがある=何回でも書き込んでよい
ではありません。
FlashそのものにはErase Cycleの寿命があります。
したがってFirmware側でも、不要な書き込みを減らす設計が重要です。
「値が変わったときだけ保存する」
基本的な考え方はシンプルです。
たとえば温度設定値なら、
現在値 25℃
↓
ユーザーが28℃へ変更
↓
RAM上では28℃
↓
設定確定
↓
NVSへ28℃を保存
とします。
毎Loop、
25 → 保存
25 → 保存
25 → 保存
25 → 保存
25 → 保存
とする必要はありません。
設定値が変化したときや、ユーザーが設定を確定したタイミングなど、意味のあるタイミングだけFlashへ保存する方が合理的です。
NVSは電源断にも配慮されている
製品では、Flashへ書き込んでいる途中で突然電源が切れる可能性もあります。
NVSはこの点も考慮されており、Espressifは突然のPower Lossに対する保護とAtomic UpdateをNVSの特徴として挙げています。
これは設定値を扱ううえで大きなメリットです。
ただし、これも、
「いつ電源を切ってもApplication側では何も考えなくてよい」
という意味ではありません。
製品全体としては、電源断の可能性、保存するタイミング、設定値の整合性などを考える必要があります。
ESP-IDFでNVSへデータを保存する
Arduino-ESP32ではPreferencesを利用しましたが、ESP-IDFではNVS APIを直接使用できます。
基本的な流れは、
NVSを初期化
↓
Namespaceを開く
↓
値を読み書き
↓
Commit
↓
Handleを閉じる
です。
まずNVS Flashを初期化します。
#include "nvs_flash.h"
#include "nvs.h"
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
NVS Partitionに空きPageがない場合や、現在のNVS Libraryと互換性のない形式が検出された場合などを考慮した、ESP-IDFで一般的に使われる初期化方法です。EspressifのNVS API Documentationでも同様の初期化処理が示されています。
Namespaceを開く
NVSを初期化したら、nvs_open()でNamespaceを開きます。
nvs_handle_t handle;
ESP_ERROR_CHECK(
nvs_open("settings", NVS_READWRITE, &handle)
);
ここでは、
Namespace = settings
Mode = Read / Write
として開いています。
読み出しだけでよければ、
NVS_READONLY
を指定できます。
Preferencesの、
preferences.begin("settings", false);
と考え方はよく似ています。
ESP-IDFで整数を保存する
たとえば温度設定値25を保存する場合は、
int32_t temperature = 25;
ESP_ERROR_CHECK(
nvs_set_i32(handle, "temp", temperature)
);
とします。
ただし、ESP-IDFではここで終わりではありません。
重要なのが、
ESP_ERROR_CHECK(
nvs_commit(handle)
);
です。
NVS APIでは、nvs_set_*()を実行しただけでは変更内容が必ずしもNon-Volatile Storageへ確定しているとは限りません。nvs_commit()を呼び出して変更を確定します。
最後に、
nvs_close(handle);
でHandleを閉じます。
ESP-IDFで保存値を読み出す
保存した値はnvs_get_*()で取得できます。
int32_t temperature = 20;
esp_err_t err =
nvs_get_i32(handle, "temp", &temperature);
if (err == ESP_ERR_NVS_NOT_FOUND) {
temperature = 20;
}
この例では、"temp"が存在しなければDefault値として20を使用しています。
Preferencesの、
preferences.getInt("temp", 20);
ではDefault値まで一つのAPIで指定できましたが、ESP-IDFでは戻り値を確認して処理できます。
少しコード量は増えますが、
Keyが存在しない
データ型が違う
その他のErrorが発生した
といった状態をApplication側で明確に扱えます。
nvs_commit()を忘れない
ESP-IDFでNVSを初めて使うときに注意したいのがnvs_commit()です。
基本的には、
nvs_set_xxx()
↓
nvs_set_xxx()
↓
nvs_set_xxx()
↓
nvs_commit()
という流れで変更内容を確定できます。
たとえば、
ESP_ERROR_CHECK(
nvs_set_i32(handle, "temp", 25)
);
ESP_ERROR_CHECK(
nvs_set_u8(handle, "mode", 1)
);
ESP_ERROR_CHECK(
nvs_commit(handle)
);
のように複数項目を更新したあと、Commitすることもできます。
ここでも、
1回値を変更するたびに、何も考えずFlashへ書き込む
のではなく、Applicationとして設定が確定するタイミングを考えることが重要です。
PreferencesとESP-IDF NVS APIはどちらを使う?
どちらを使うかは、使用しているFrameworkによって考えると分かりやすいです。
| 開発環境 | 選択肢 |
|---|---|
| Arduino IDE + Arduino-ESP32 | Preferences |
| PlatformIO + Arduino | Preferences |
| ESP-IDF | NVS API |
| ESP-IDF + Arduino Component | 用途に応じて選択 |
Arduino Frameworkで小さな設定値を保存するなら、Preferencesは非常に扱いやすい方法です。
一方ESP-IDFでは、NVS APIを直接使うことでError処理やPartitionなどを含めて、より細かく制御できます。
重要なのは、
PreferencesとNVSのどちらが高性能か
という比較ではありません。
Preferences自体がNVSを利用しているため、使用しているFrameworkに適したInterfaceを選ぶと考える方が自然です。
Arduino IDE・PlatformIO・ESP-IDFの違いや、どの開発環境を選べばよいかについては「ESP32の開発環境を比較|Arduino IDE・PlatformIO・ESP-IDFはどれを選ぶ?」で詳しく解説しています。
NVSとLittleFSはどう使い分ける?
ESP32ではFlashへデータを保存する方法として、NVS以外にLittleFSなどのFile Systemを利用する方法もあります。
大まかな使い分けは、
| データ | 向いている方法 |
|---|---|
| 設定値 | NVS |
| 動作モード | NVS |
| Calibration値 | NVS |
| Flag | NVS |
| 少量の文字列 | NVS |
| 大きな設定File | LittleFSなど |
| HTML / CSS | LittleFSなど |
| Fileとして扱いたいData | LittleFSなど |
| 大量のLog | 用途に適したFile System等を検討 |
と考えると分かりやすいでしょう。
NVSはKeyを指定して、
"temp" → 25
のように扱う用途が得意です。
一方、
/config/settings.json
のようにFileそのものとして管理したければ、File Systemの方が扱いやすい場合があります。
EspressifもNVSをConfiguration Dataなどの保存に適した方式として位置付け、大量データについてはFile System等の利用を案内しています。
Factory Resetはどう実装する?
製品では、保存した設定を工場出荷状態へ戻したい場合があります。
Preferencesでは、
preferences.clear();
を使用すると、現在開いているNamespace内のKeyを削除できます。
特定のKeyだけ削除するなら、
preferences.remove("temp");
も利用できます。
ESP-IDFでは、
nvs_erase_key(handle, "temp");
で特定Keyを削除できます。
Namespace内のすべてのKeyを削除する場合は、
nvs_erase_all(handle);
nvs_commit(handle);
とします。
これを利用して、
Factory Reset操作
↓
設定Namespaceを消去
↓
再起動
↓
Keyなしを検出
↓
Default設定を生成
という構成にできます。
NVS Partition全体を簡単に消さない
ここは製品Firmwareでは注意したいところです。
Factory Resetだからといって、
nvs_flash_erase();
を安易に呼ぶ設計にはしない方がよいでしょう。
NVS PartitionにはApplicationが想定している以外のデータが存在する可能性があります。
たとえば、
user_settings
network
calibration
production
のように用途をNamespaceで分けておけば、
ユーザー設定だけ初期化する
Calibration値は残す
といった制御ができます。
Factory Resetで「何を消して、何を残すか」は、製品仕様として決めておくことが重要です。
Firmwareを更新してもNVSの値は残る?
通常、Firmwareを書き換えただけでNVS Partitionが自動的に消去されるわけではありません。
そのためOTAなどでFirmwareを更新した後も、NVSに保存された設定値を引き継ぐ設計ができます。
これは便利ですが、別の問題もあります。
たとえばVersion 1のFirmwareで、
temp
mode
を保存していたとします。
Version 2では、
temp
mode
language
sensor_type
へ設定項目が増えるかもしれません。
さらに将来、
mode : uint8_t
だったものを別の形式へ変更する可能性もあります。
すると、
古いFirmwareが保存したデータを、新しいFirmwareがどう解釈するか
を考える必要があります。
設定データにもVersionを持たせる
長期間更新する製品では、設定データにVersion情報を持たせる方法があります。
たとえば、
config_version = 2
temp = 25
mode = 1
としておきます。
起動時に、
NVS読出し
↓
config_version確認
↓
現在Versionと同じ?
├─ YES → そのまま使用
│
└─ NO
↓
Migration処理
とします。
これによってFirmware Update後に設定項目が変わっても、古い設定を新しい形式へ変換できます。
これはNVS固有の機能というより、Application側で設計するデータ管理方法です。
しかし製品を長期間Updateするなら非常に重要になります。
Calibration値とユーザー設定は分ける
もう一つ実務でおすすめしたいのが、保存データを種類ごとに分けることです。
たとえば、
Namespace : "user"
├─ temp
├─ volume
└─ mode
Namespace : "cal"
├─ offset
└─ gain
Namespace : "system"
├─ config_version
└─ initialized
という構成です。
こうしておけばFactory Resetするときに、
user → 消す
cal → 残す
といった処理ができます。
工場で書き込んだCalibration値までユーザー操作で消えてしまう設計は避けたいところです。
データの用途と寿命を考えてNamespaceを設計すると、後々のFirmware変更にも対応しやすくなります。
保存直後に電源を切って大丈夫?
NVS自体には突然のPower Lossを考慮した仕組みがありますが、製品としてはもう一段考えます。
たとえばユーザーが設定を変更するたびに、
ボタン操作
↓
即Flash Write
↓
ボタン操作
↓
即Flash Write
とすると、操作回数だけ書き込みが発生します。
そこで、
設定変更
↓
RAM上で更新
↓
一定時間変更なし
↓
設定確定
↓
NVSへ保存
という方法も考えられます。
ただし保存を遅らせすぎれば、その間に電源が切れたとき変更内容が失われます。
つまり、
Flash寿命を優先して保存回数を減らす
ことと、
設定変更を確実に残す
ことにはTrade-offがあります。
製品仕様に応じて保存タイミングを決める必要があります。
毎秒保存するとどうなる?
仮に設定値を毎秒Flashへ書き込むとします。
1日では、
60 × 60 × 24
= 86,400回
です。
1年では約3,150万回の保存要求になります。
NVSにはWear Levelingがあるため、「同じFlash Cellを3,150万回直接Eraseする」という単純な話ではありません。
しかし、そもそも設定値を毎秒不揮発化する必要がないのであれば、Firmware側で不要な書き込みを発生させないことが第一です。
たとえば累積稼働時間のような値を保存したい場合にも、毎秒Flashへ保存するのではなく、
RAM上で積算
↓
一定間隔
または
正常Shutdown時
↓
NVSへ保存
など、要求仕様に応じた方法を検討します。
NVSへ保存する値・しない値を最初に決める
Firmware設計時には、変数ごとに、
電源OFF後も必要?
を考えると整理しやすくなります。
たとえば、
| データ | NVS保存 |
|---|---|
| ユーザー設定温度 | ○ |
| Wi-Fi設定 | ○ |
| Calibration値 | ○ |
| 初期設定完了Flag | ○ |
| 現在の一時的なADC値 | × |
| 通信中の一時Buffer | × |
| 毎周期更新する内部変数 | × |
| 大量の連続Log | △ 別方式を検討 |
「後で必要になるかもしれないから全部保存する」のではなく、電源断後に復元する必要があるものだけを選ぶのが基本です。
製品FirmwareでのNVS設計チェックリスト
最後に、実際にNVSを使うときの確認項目をまとめます。
| 確認項目 | 内容 |
|---|---|
| 保存対象 | 本当に電源OFF後も必要か |
| データ量 | NVSに適した小さなデータか |
| Namespace | 用途ごとに整理されているか |
| Default値 | 初回起動時の値を定義したか |
| 書き込み頻度 | 不要なFlash Writeがないか |
| 保存タイミング | 設定確定時など明確になっているか |
| Error処理 | 読み書き失敗を考慮しているか |
| Factory Reset | 消すデータ・残すデータを決めたか |
| Calibration | ユーザー設定と分離したか |
| Version管理 | Firmware更新後の互換性を考えたか |
| Migration | 設定形式変更時の処理を考えたか |
| 電源断 | 保存途中・保存前の電源断を考慮したか |
APIを呼んで値を保存するだけなら簡単です。
しかし製品では、数年後のFirmware Updateまで考えて保存データを設計することが重要になります。
まとめ|ESP32の設定保存にはNVSを活用する
ESP32で電源OFF後も設定値を保持したい場合、NVSを利用できます。
Arduino-ESP32ではPreferences Libraryを使うことで、NVSを比較的簡単に扱えます。
ESP-IDFではNVS APIを直接使用し、
nvs_open()
nvs_get_xxx()
nvs_set_xxx()
nvs_commit()
nvs_close()
という流れでデータを管理できます。
重要なのは、
Flashに保存できるから、何でも保存する
という考え方にしないことです。
NVSは、
設定値
Calibration値
動作モード
初期化Flag
など、小さく、頻繁には変更されないデータの保存に適しています。
大量のデータや頻繁なLogには、用途に応じてLittleFSなど別の保存方法を検討します。
また製品Firmwareでは、
書き込み頻度
Factory Reset
設定Version
Firmware Update時のMigration
まで考えておくことで、後からFirmwareを変更したときにも対応しやすくなります。
NVSは単なる「電源を切っても値が残る機能」ではありません。
製品の設定データを長期間どう管理するか
という視点で使うと、より実用的な仕組みになります。
今回の記事からは、既存の**「ESP32の開発環境を比較|Arduino IDE・PlatformIO・ESP-IDFはどれを選ぶ?」**への内部リンクも自然に設定できます。PreferencesとESP-IDF NVS APIの違いを読んで「そもそもどの開発環境を選ぶ?」となった読者をつなげられます。