5
5

More than 1 year has passed since last update.

ESP32でNVS領域を使うメモ

Last updated at Posted at 2021-08-27

ESP32でNVS領域を使うメモ

ESP-IDFのドキュメントを参考に、Arduino IDE環境で試しています。Arduino IDE環境はESP-IDFより古いバージョンのライブラリが使われているため、使えない関数などもあります。

nvs 領域について

nvs(不揮発性ストレージ)はプログラムなどと同じメインのフラッシュメモリ上に確保され、電源を切っても保存される記憶領域で、パーティションテーブルでType=data, Subtype=nvsで定義されています。

デフォルトのパーティションテーブルでは、0x5000 = 20KByteが確保されています。
設定データなどの保存に便利に使えます。

nvsライブラリでは、15文字までの英数字の名前と値がセットで記録されています。またパーティション、名前空間(NameSpace)もあります。
データは、 パーティション名/名前空間/キー でユニークになっています。

パーティションの対応について

以下の説明のほとんどで、デフォルトの'nvs'ではなく、ユーザ定義の別パーティションを対象にする _partition()関数が用意されています。
別パーティションを対象にすることができるようですが、未検証のため、ここでは扱いません。

パーティションテーブル

パーティションテーブルについては、下記にまとめました。

nvsの利用法

1. nvs_flash_init()でパーティションを初期化

デフォルトパーティションを使用開始します。
成功すれば ESP_OK が返ります。NVSストレージに空ページが無い場合は ESP_ERR_NVS_NO_FREE_PAGES、パーティションテーブルにSubtype=nvsのパーティションが見つからない場合はESP_ERR_NOT_FOUNDが返ります。

この「初期化」はいわゆるフォーマット動作ではなく使用開始処理です。基本的には、 多くの環境ではシステムで呼び出し済みで省略可です。
ただし、nvs_flash_deinit()やnvs_flash_erase()を実行した後は、明示的に呼び出しが必要です。
もし呼び出しなしの場合、WiFi接続ライブラリなどの内部でnvsを利用しているものがエラーになります。

2. nvs_open()で名前空間を指定し、ハンドラを得る

名前空間(namespace)を指定して領域をオープンします。名前空間は15文字までの英数字です。

nvs_open_sample.c
nvs_handle nvsHandle;
esp_err_t ret = nvs_open("mynvs",NVS_READWRITE,&nvsHandle);
if( err != ESP_OK ) { /* ERROR */ }

オープンモードは、NVS_READONLY(読み込み専用)か、NVS_READWRITE(読み書き)のどちらかです。
openで取得されたハンドラを使って、その後の読み書き(NVS_READONLYの場合は読み込みのみ)を行います。

3. ハンドラを使ってnvs_set_*() でデータを書き込む

データの名前を指定して、書き込みします。
データの書き込みを行う場合、書き込みモードでopenする必要があります。

nvs_write_sample.c
ret = nvs_set_i32(nvsHandle,"MyValueName",123);
if( ret != ESP_OK ){
  /* 読み出しエラー */
  Serial.println(esp_err_to_name(ret));
}

もし同じ名前のデータが存在していた場合は上書きされます。書き込むデータの型が違っていたとしても、古いデータはなくなり、新しいデータが新しいデータ型で保存されます。

書き込むデータの型に合わせて多数の関数が用意されています。

set系関数は以下のとおり。

nvs_flash/nvs.h
esp_err_t nvs_set_i8(nvs_handlehandle, const char *key, int8_t value)
esp_err_t nvs_set_u8(nvs_handlehandle, const char *key, uint8_t value)
esp_err_t nvs_set_i16(nvs_handlehandle, const char *key, int16_t value)
esp_err_t nvs_set_u16(nvs_handlehandle, const char *key, uint16_t value)
esp_err_t nvs_set_i32(nvs_handlehandle, const char *key, int32_t value)
esp_err_t nvs_set_u32(nvs_handlehandle, const char *key, uint32_t value)
esp_err_t nvs_set_i64(nvs_handlehandle, const char *key, int64_t value)
esp_err_t nvs_set_u64(nvs_handlehandle, const char *key, uint64_t value)
esp_err_t nvs_set_str(nvs_handlehandle, const char *key, const char *value)
esp_err_t nvs_set_blob(nvs_handlehandle, const char *key, const void *value, size_t *length)

blobはバイナリデータを保存するための関数です。
また、nvsの実装上の都合から、現時点では、長いstrやblobの利用は非推奨です。
その様な利用を想定する場合、Wear LevellingライブラリのFATファイルシステムを使うことを推奨しています。

ただ、FAT-FSを使うのも複雑だと思うので、blob,strの制限(現時点では1984byte,strの場合は'\0'を含む)以下のデータであれば、書き換えを最小限にして使用すれば利用可能と思います。

注意が必要なのは、「同じサイズでの書き換えはなら大丈夫」ではありません。現時点でのnvsライブラリはデータの書き換えは「データの消去」「別の領域にデータを書き込み」という実装になっています。

4. ハンドラを使ってnvs_get_*() でデータ読み出す

読み出すデータ変数を用意し、ポインタ渡しで読み込みます。

nvs_read_sample.c
int32_t i32Value;
ret = nvs_get_i32(nvsHandle,"MyValueName",&i32Value);
if( ret != ESP_OK ){
  /* 読み出しエラー */
  Serial.println(esp_err_to_name(ret));
}

読み込むデータの型に合わせて多数の関数が用意されています。書き込みをしたときと同じ型で読み込まないとエラー(ESP_ERR_NVS_NOT_FOUND)になります。
型の比較は厳格で、例えば、uint64で書き込まれたデータをint64で読みこもうとしてもエラーになります。

get系関数は以下のとおり。

nvs_flash/nvs.h
esp_err_t nvs_get_i8(nvs_handlehandle, const char *key, int8_t *out_value)
esp_err_t nvs_get_u8(nvs_handlehandle, const char *key, uint8_t *out_value)
esp_err_t nvs_get_i16(nvs_handlehandle, const char *key, int16_t *out_value)
esp_err_t nvs_get_u16(nvs_handlehandle, const char *key, uint16_t *out_value)
esp_err_t nvs_get_i32(nvs_handlehandle, const char *key, int32_t *out_value)
esp_err_t nvs_get_u32(nvs_handlehandle, const char *key, uint32_t *out_value)
esp_err_t nvs_get_i64(nvs_handlehandle, const char *key, int64_t *out_value)
esp_err_t nvs_get_u64(nvs_handlehandle, const char *key, uint64_t *out_value)
esp_err_t nvs_get_str(nvs_handlehandle, const char *key, char *out_value, size_t *length)
esp_err_t nvs_get_blob(nvs_handlehandle, const char *key, void *out_value, size_t *length)

キーは最大15文字までの英数字です。

この中で、strとblobはデータ長さが読み出し前にはわからないため、長さを問い合わせる方法が用意されています。
out_valueを0にしてstr、blobを実行すると、読み出しに必要なデータ容量がlengthに返ってきます。
strの場合、lengthは行端の'\0'を含んだバイト数が返ります。

nvs_strget_sample.c
/* まず保存されているデータの長さを得る */
size_t length;
esp_err_t err = nvs_get_str(nvsHandler,"myStringName",0,&length);
if( err != ESP_OK ){ /* ERROR */ }
/* 長さ分のメモリを確保して読み出す */
char *stringBody = (char *)malloc(length);
if( stringBody == null ){ /* ERROR */}
err = nvs_get_str(nvsHandler, "myStringName",stringBody ,&length);
if( err != ESP_OK ){ /* ERROR */ }

5. nvs_erase_key() / nvs_erase_all() でデータを消去する

nvs_erase_key(handle,keyName)で、指定のキーの値を消去します。
また、nvs_erase_all(handle)で、handleを得たopenの名前空間の全データを消去します。
書き込みモードでopenしている必要があります。

6. nvs_commit()で物理デバイスに書き込み、nvs_close()で終了

nvs_set,get,erase関数は、メモリ上での操作のみでデータをキャッシュしており、このままでは電源をOFFにしたときにデータの変更はされません。
フラッシュに物理的に書き込むためにnvs_commit()を実行します。もちろんnvs_commit()は、フラッシュメモリの寿命を長くするために、回数を最小限に抑える必要があります。

nvs_commit()後に、ハンドラをnvs_close()して開放します。

情報取得

nvsデータの情報を取得する方法がいくつかあります。

nvs.h
esp_err_t nvs_get_stats(const char* part_name, nvs_stats_t* nvs_stats);
esp_err_t nvs_get_used_entry_count(nvs_handle handle, size_t* used_entries);
typedef struct {
    size_t used_entries;      /**< Amount of used entries. */
    size_t free_entries;      /**< Amount of free entries. */
    size_t total_entries;     /**< Amount all available entries. */
    size_t namespace_count;   /**< Amount name space. */
} nvs_stats_t;

nvs_get_stats()は、指定パーティション内のエントリ(キーと名前空間)の使用状況を取得できます。
nvs_get_used_entry_count()は、openされている名前空間での使用済みエントリの数を取得できます。

なお、以下の関数で、キー名などを取得できるのですが、2021/8現在、Arduino IDE環境のライブラリではこれらの関数は未実装です。

nvs.h
nvs_iterator_t nvs_entry_find(const char *part_name, const char *namespace_name, nvs_type_t type);
nvs_iterator_t nvs_entry_next(nvs_iterator_t iterator);
void nvs_entry_info(nvs_iterator_t iterator, nvs_entry_info_t *out_info);
void nvs_release_iterator(nvs_iterator_t iterator);

消去

前述の通り、nvsは、パーティション、名前空間、キーという3段階の階層でデータを保持しています。
名前空間全体の消去は、nvs_erase_all()、一つのキーの消去はnvs_erase_key()で行いますが(前述)、パーティション全体の消去はnvs_flash_erase()で行います。

nvs_flash_erase()

パーティション全体の消去を行います。パーティション化のすべての名前空間、キーと値が消去されます。

erase_sample.h
 nvs_flash_erase();

フラッシュ全体をダンプしてみましたが、0xffで上書き消去されるようです。

nvs_erase_all(),nvs_erase_key()はともにキーに無効フラグを書き込むだけで、フラッシュメモリ全体を吸い出すとデータが視認できてしまいます。
これはnvs_set_*()でデータを上書きしたときも同じで、実装上、古いデータに無効フラグを立て、新しいデータを別領域に書き込みます。
もしセキュリティー上の必要などでデータを完全に消去したい場合は、nvs_flash_ersce()で全体の初期化をする必要があります。

nvsのシステムからの利用

nvsはシステムのライブラリによっても使用されるため、ユーザプログラムで使っていない場合でもデータが保存されていることがあります。
特にWifi接続すると、SSIDやパスワードがnvs領域に保存されるようです。
また前述の通り、明示的に消去を行わない限り、データは残ったままか、消去済みのデータでもデータ自体がメモリ上に残っていることが多くあります。
プログラムを安定動作させるために、あるいはセキュリティのために、時々nvs_flash_erase()すると良いと思います。

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5