ESP32でNVS領域を使うメモ

# ESP32でNVS領域を使うメモ

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

+ [ESP-IDFのドキュメント](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/nvs_flash.html)

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

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

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

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

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

* [ESP32のPartition Tableの書き方](https://qiita.com/yomori/items/743bc84c961bb921d8cb)

## 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は、パーティション、名前空間、キーという３段階の階層でデータを保持しています。
名前空間全体の消去は、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()すると良いと思います。