ESP-IDFを使ってみる

Non-volatile storage library


esp32では、Flashの一部を不揮発性の記憶領域(NVS)として使うことができます。
こ ちらに簡単なサンプルが公開されています。
ビルドして実行すると以下の様に再起動した回数をNVS領域に設定して再起動します。
再起動した回数は「restart_counter」というキーでNVS領域に保存されます。


Non-volatile storage libraryの詳細はこ ちらに公開されています。
「idf.py erase_flash」でNVS領域を含むFlash全体を初期化することができます。



こ ちらにnvs blobのサンプルが公開されています。
ビルドして実行すると以下の様に再起動した回数をNVS領域に設定して再起動します。
再起動した回数は「restart_counter」というキーでNVS領域に保存されます。


このタイミングでGPIO0(Bootボタン)を長押しすると、起動してからのTick数を「run_time」というキーで
「blob value」としてNVS領域に設定して再起動します。


「run_time」のキーは可変サイズの領域で、1つのキーに対して、複数のデータを保存することができます。




こ ちらに「Creating Unique Factory Data Images」のやり方が紹介されています。
直訳すると「独自の工場データイメージの作成」ですが、要するに最初からシリアル番号などのデータを、
2番目以降のNVS領域に書き込んでおくことができます。

「Factory Data Images」を書き込めるのは2番目以降のNVSパーテイション(以降Specified NVS partition)です。
そこで、Custom partitation table CSV(partitions.csv)を準備します。
赤字の部分がSpecified NVS partitionで、パーテーション開始アドレスが0x10000、パーティションサイズが0x6000となります。
$ cat partitions.csv
# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
fctry   , data, nvs,     0x10000, 0x6000,
factory,  app,  factory, 0x20000, 1M,

Specified NVS partitionに書き込むデータはCSVファイルとして準備します。
CSVファイルのファイル名は何でも構いませんが、私は「nvs_config.csv」としました。
$ cat nvs_config.csv
key,type,encoding,value ----> 固定文字列
device_data,namespace,,  ----> namespace名
serial_no,data,string,164589345735  -----> NVSに書き込むデータ
mac_addr,data,string,0A:0B:0C:0D:0E:0F -----> NVSに書き込むデータ

CSVファイルの1行目は固定の文字列です。

CSVファイルの2行目はnamespaceの指定で、先頭に「namespaceの名前(紛らわしい)」を指定します。
「namespaceの名前(英語ではNamespace name)」は何でも構いませんが、私は「device_data」としました。

CSVファイルの3行目以降がNVSに書き込むデータで「Key,Type,Encording,Value」の順に指定します。
Keyは変数の名前です。
Typeにはfileかdataを指定することができます。
Encordingは変数の型です。
Valueが変数値となります。
これらの詳細はこ ちらに記載されています。

今回は「serial_no」と「mac_addr」の2つのKeyを文字列として書き込みます。

CSVファイルをnvs_partition_gen.pyを使ってイメージデータ(my_nvs.bin)に変換し、
変換したイメージデータをesptool.pyを使ってFlashに書き込みます。
この時、イメージサイズを0x6000、書き込み開始アドレスを0x10000として、Specified NVS partitionのパーティション位置とサイズを
Custom partitation table CSV(partitions.csv)と一致させる必要が有りま す。
$ python $IDF_PATH/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py generate nvs_config.csv my_nvs.bin 0x6000

Creating NVS binary with version: V2 - Multipage Blob Support Enabled

Created NVS binary: ===> /home/nop/rtos/nvs_partition_gen/my_nvs.bin

$ python $IDF_PATH/components/esptool_py/esptool/esptool.py  write_flash 0x10000 my_nvs.bin
esptool.py v2.7-dev
Found 3 serial ports
Serial port /dev/ttyUSB0
Connecting........___
Detecting chip type... ESP32
Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, Coding Scheme None
MAC: 30:ae:a4:24:bc:5c
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 24576 bytes to 258...
Wrote 24576 bytes (258 compressed) at 0x00010000 in 0.0 seconds (effective 7288.8 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...


最後にmenuconfigでCustom partitation table CSVにpartitions.csvを指定します。




sdkconfig.defaultsが有るとその内容に従ってsdkconfigが作られます。
以下のsdkconfig.defaultsが有ると、Menuconfig実行時にsdkconfigにこれらの値が反映されます。
$ cat sdkconfig.defaults
#
# Serial flasher config
#
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y

#
# Partition Table
#
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"

以下がFactory Data Imagesからデータを取り出すコードです。
注意する点としてbuflenにはバッファサイズを入れておく必要が有ります。
最初これが分からずにめちゃハマりました。
APIの詳細はこ ちらに有ります。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"

#define MFG_PARTITION_NAME "fctry"

void app_main()
{
    esp_err_t err;

    // Initialize specified NVS partition
    err = nvs_flash_init_partition(MFG_PARTITION_NAME);
    if (err != ESP_OK) {
       printf("Error (%s) nvs_flash_init_partition!\n", esp_err_to_name(err));
    }

    // Open specified NVS partition
    printf("Opening Non-Volatile Storage (NVS) handle... ");
    nvs_handle fctry_handle;
    err = nvs_open_from_partition(MFG_PARTITION_NAME, "device_data",
               NVS_READWRITE, &fctry_handle);
    if (err != ESP_OK) {
        printf("Error (%s) nvs_open_from_partition!\n", esp_err_to_name(err));
    } else {
        printf("Done\n");

        char buf[64];
        size_t buflen;

        // Read serialserial_no
        printf("Reading serial_no from NVS ... \n");
        buflen = sizeof(buf);
        err = nvs_get_str(fctry_handle, "serial_no", buf, &buflen);
        switch (err) {
            case ESP_OK:
                printf("Done\n");
                printf("buflen=%d serial_no=[%s]\n", buflen, buf);
                break;
            case ESP_ERR_NVS_NOT_FOUND:
                printf("The value is not initialized yet!\n");
                break;
            default :
                printf("Error (%s) reading!\n", esp_err_to_name(err));
        }

        // Read mac_addr
        printf("Reading mac_addr from NVS ... \n");
        buflen = sizeof(buf);
        err = nvs_get_str(fctry_handle, "mac_addr", buf, &buflen);
        switch (err) {
            case ESP_OK:
                printf("Done\n");
                printf("bulen=%d mac_addr=[%s]\n", buflen, buf);
                break;
            case ESP_ERR_NVS_NOT_FOUND:
                printf("The value is not initialized yet!\n");
                break;
            default :
                printf("Error (%s) reading!\n", esp_err_to_name(err));
        }

        // Close
        nvs_close(fctry_handle);
    }

    printf("\n");
}

ビルドして実行するとserial_noとmac_addrをSpecified NVS partitionから取り出します。
Opening Non-Volatile Storage (NVS) handle... Done
Reading serial_no from NVS ...
Done
buflen=13 serial_no=[164589345735]
Reading mac_addr from NVS ...
Done
bulen=18 mac_addr=[0A:0B:0C:0D:0E:0F]



NVS partitionには複数のnamespaceを設定することができます。
以下の例では[device_data]と[current_data]の2つのnamespaceを設定しています。
[device_data]のserial_numberと、[current_data]のserial_numberは別の値として扱うこと ができま す。
$ cat nvs_config.csv
key,type,encoding,value
device_data,namespace,, ----> 1番目のnamespace名
serial_no,data,string,164589345735
mac_addr,data,string,0A:0B:0C:0D:0E:0F
current_data,namespace,, ----> 2番目のnamespace名
serial_no,data,string,372818934632
mac_addr,data,string,1A:1B:1C:1D:1E:1F

注意点として空白行はデータの終わりとみなされます。
以下の様に空白行が有ると、そこでデータの終わりとみなされ、それ以降のデータを無視します。
$ cat nvs_config.csv
key,type,encoding,value
device_data,namespace,, ----> 1番目のnamespace名
serial_no,data,string,164589345735
mac_addr,data,string,0A:0B:0C:0D:0E:0F
                         ----> 空白行はデータの終端とみなされる
current_data,namespace,, ----> 2番目のnamespace名
serial_no,data,string,372818934632
mac_addr,data,string,1A:1B:1C:1D:1E:1F

namespaceを変えて実行してみます。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"

#define MFG_PARTITION_NAME "fctry"

void readNVS(char * namespace) {
    // Open specified NVS partition
    printf("Opening Non-Volatile Storage (namespace=[%s]) handle... ", namespace);
    nvs_handle nvs_handle;
    esp_err_t err = nvs_open_from_partition(MFG_PARTITION_NAME, namespace,
               NVS_READWRITE, &nvs_handle);
    if (err != ESP_OK) {
        printf("Error (%s) nvs_open_from_partition!\n", esp_err_to_name(err));
    } else {
        printf("Done\n");

        char buf[64];
        size_t buflen;

        // Read serialserial_no
        printf("Reading serial_no from NVS ... \n");
        buflen = sizeof(buf);
        err = nvs_get_str(nvs_handle, "serial_no", buf, &buflen);
        switch (err) {
            case ESP_OK:
                printf("Done\n");
                printf("buflen=%d serial_no=[%s]\n", buflen, buf);
                break;
            case ESP_ERR_NVS_NOT_FOUND:
                printf("The value is not initialized yet!\n");
                break;
            default :
                printf("Error (%s) reading!\n", esp_err_to_name(err));
        }

        // Read mac_addr
        printf("Reading mac_addr from NVS ... \n");
        buflen = sizeof(buf);
        err = nvs_get_str(nvs_handle, "mac_addr", buf, &buflen);
        switch (err) {
            case ESP_OK:
                printf("Done\n");
                printf("bulen=%d mac_addr=[%s]\n", buflen, buf);
                break;
            case ESP_ERR_NVS_NOT_FOUND:
                printf("The value is not initialized yet!\n");
                break;
            default :
                printf("Error (%s) reading!\n", esp_err_to_name(err));
        }

        // Close
        nvs_close(nvs_handle);
        printf("\n");
    }
}

void app_main()
{
    // Initialize specified NVS partition
    esp_err_t err = nvs_flash_init_partition(MFG_PARTITION_NAME);
    if (err != ESP_OK) {
        printf("Error (%s) nvs_flash_init_partition!\n", esp_err_to_name(err));
    }

    readNVS("device_data");
    readNVS("current_data");
}

ビルドして実行すると[device_data]に格納されているデータと、[current_data]に格納されているデータは違うことが分 かります。
Opening Non-Volatile Storage (namespace=[device_data]) handle... Done
Reading serial_no from NVS ...
Done
buflen=13 serial_no=[164589345735]
Reading mac_addr from NVS ...
Done
bulen=18 mac_addr=[0A:0B:0C:0D:0E:0F]

Opening Non-Volatile Storage (namespace=[current_data]) handle... Done
Reading serial_no from NVS ...
Done
buflen=13 serial_no=[372818934632]
Reading mac_addr from NVS ...
Done
bulen=18 mac_addr=[1A:1B:1C:1D:1E:1F]

NVSパーティションは以下のツリー構造になっています。
Partition--+--Namespace1--+--Key1-Value1,2,3...
           |              +--Key2-Value1,2,3...
           |              +--Key3-Value1,2,3...
           |              +--Key4-Value1,2,3...
           +--Namespace2--+--Key1-Value1,2,3...
                          +--Key2-Value1,2,3...
                          +--Key3-Value1,2,3...
                          +--Key4-Value1,2,3...

NVS Partition Generator Utilityの使い方がこ ちらに公開されています。
fileも直接指定できるみたいですが、使い方が分かりません。



NVSには以下の2種類が有ります。
・Default NVS partition
このNVSパーティションは、パーティションテーブルで「nvs」とラベル付けされたパーティションです。
赤字の部分がDefault NVSパーティションで、特別な操作をしなくてもデフォルトで作成されるパーテーションです。
0x6000バイト(24Kバイト)が確保されます。
I (62) boot: ## Label            Usage          Type ST Offset   Length
I (70) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (77) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (85) boot:  2 fctry            WiFi data        01 02 00010000 00006000
I (92) boot:  3 factory          factory app      00 00 00020000 00100000

Wifiに接続すると接続情報やPHYキャリブレーション情報がここに格納されます。
こ ちらのサンプルでnvsパーテイションに格納されている情報を見ることができます。
ルータのSSIDやパスワードはblob形式で格納されていますが、暗号化されているようです。
esp32> nvs_list nvs
namespace 'storage', key 'restart_counter', type 'i32'
namespace 'nvs.net80211', key 'ap.sndchan', type 'u8'
namespace 'nvs.net80211', key 'opmode', type 'u8'
namespace 'nvs.net80211', key 'bssid.set', type 'u8'
namespace 'nvs.net80211', key 'sta.lis_intval', type 'u16'
namespace 'nvs.net80211', key 'sta.scan_method', type 'u8'
namespace 'nvs.net80211', key 'sta.sort_method', type 'u8'
namespace 'nvs.net80211', key 'sta.minrssi', type 'i8'
namespace 'nvs.net80211', key 'sta.minauth', type 'u8'
namespace 'nvs.net80211', key 'sta.pmf_e', type 'u8'
namespace 'nvs.net80211', key 'sta.pmf_r', type 'u8'
namespace 'nvs.net80211', key 'sta.rrm_e', type 'u8'
namespace 'nvs.net80211', key 'sta.btm_e', type 'u8'
namespace 'nvs.net80211', key 'sta.mbo_e', type 'u8'
namespace 'phy', key 'cal_mac', type 'blob'
namespace 'storage', key 'bmpText', type 'str'
namespace 'storage', key 'bmpError', type 'str'
namespace 'bt_config.conf', key 'bt_cfg_key0', type 'blob'
namespace 'nvs.net80211', key 'sta.ft', type 'u8'
namespace 'nvs.net80211', key 'sta.owe', type 'u8'
namespace 'nvs.net80211', key 'sta.bss_retry', type 'u8'
namespace 'nvs.net80211', key 'sta.trans_d', type 'u8'
namespace 'nvs.net80211', key 'sta.sae_h2e', type 'u8'
namespace 'nvs.net80211', key 'sta.sae_pk_mode', type 'u8'
namespace 'nvs.net80211', key 'sta.sae_h2e_id', type 'blob'
namespace 'storage', key 'bmpFile', type 'str'
namespace 'nvs.net80211', key 'sta.apinfo', type 'blob'
namespace 'phy', key 'cal_version', type 'u32'
namespace 'phy', key 'cal_data', type 'blob'
namespace 'nvs.net80211', key 'sta.ssid', type 'blob'
namespace 'nvs.net80211', key 'sta.pswd', type 'blob'
namespace 'nvs.net80211', key 'sta.apsw', type 'blob'
namespace 'nvs.net80211', key 'sta.chan', type 'u8'
esp32> nvs_namespace nvs.net80211
I (68560) cmd_nvs: Namespace set to 'nvs.net80211'
esp32> nvs_get sta.ssid blob
0e000000617465726d2d6435613465652d67000000000000000000000000000000000000

・Specified NVS partition
このNVSパーティションは、パーティションテーブルで「nvs」以外にラベル付けされたパーティションです。
赤字の部分がSpecified NVSパーティションで、menuconfigのPartition TableでCustom partition tableを指定する必要が有ります。
I (62) boot: ## Label            Usage          Type ST Offset   Length
I (70) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (77) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (85) boot:  2 fctry            WiFi data        01 02 00010000 00006000
I (92) boot:  3 factory          factory app      00 00 00020000 00100000

Default NVS partitionを扱うときは、nvs_flash_init()/nvs_open()で初期化/オープンしますが、
Specified NVS partitionを扱うときは、nvs_flash_init_partition()/nvs_open_from_partition()で初期化 /オープン します。
NVSへのデータの書き込み、検索はどちらも同じAPIを使います。



こ ちらにarduino for esp32のEEPROMライブラリが公開されています。
ソースを確認しましたが、Default NVS partitionを使ってEEPROM機能をエミュレーションしています。
[esp32 eeprom]でググると幾つかヒットします。
esp32のSoCにeeprom領域が存在しているように書かれている記事も有りますが、esp32のSoCにはeeprom領域は有りませ ん。
Arduino互換ライブラリが、Flashメモリの一部をNVS領域として確保し、NVS領域を使った疑似EEPROMを提供しています。
なお、ESP32Cシリーズなどは、SoCにFlashメモリがビルトインされている物が有るので、SoC内のFlashメモリ領域に構築した NVSとなり、
メチャクチャ省略するとSoC上のNVSとなります。

続く....