ことの発端
以前から稀に発生していたのですが、たしかにNVSに書き込んだデータが読み出してみても何もなかったり、すでに書き込まれているデータが化けたりということが起きていました。その当時は再現性があまりなくて
「???……なんかミスったかな?」
くらいにしか思っていませんでしたが、直近の案件でたしかに確実に現象を再現させることができるようになったので、ちゃんとマジメに調べてみようと思ったのが発端です。
昔、NVSの記事を書いた
これですね
もはやいつの時代のものかすら分からない(笑)のですが、かなり古いSDKバージョンであると思われます。当時のソースコードが載せてありますが、古いAPIでstorageというラベル名を使っていたようです。
パーティションの概念自体は当時からあったと思います、たぶん……。
ちなみに最近のNVSのサンプルソースコードは以下のような記述になっています。
#include <zephyr/kernel.h>
#include <zephyr/sys/reboot.h>
#include <zephyr/device.h>
#include <string.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/fs/nvs.h>
static struct nvs_fs fs;
#define NVS_PARTITION storage_partition
#define NVS_PARTITION_DEVICE FIXED_PARTITION_DEVICE(NVS_PARTITION)
#define NVS_PARTITION_OFFSET FIXED_PARTITION_OFFSET(NVS_PARTITION)
...
fs.flash_device = NVS_PARTITION_DEVICE;
fs.offset = NVS_PARTITION_OFFSET;
となっており、ここで使っているstorage_partitionというのはDTSノードラベルになります。何のノードラベルかと言うと&flash0で定義しているノードラベルで、nRF52840だとnrf52840_partition.dtsiで定義されています。
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 0x0000C000>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000C000 0x00076000>;
};
slot1_partition: partition@82000 {
label = "image-1";
reg = <0x00082000 0x00076000>;
};
/*
* The flash starting at 0x000f8000 and ending at
* 0x000fffff is reserved for use by the application.
*/
/*
* Storage partition will be used by FCB/LittleFS/NVS
* if enabled.
*/
storage_partition: partition@f8000 {
label = "storage";
reg = <0x000f8000 0x00008000>;
};
};
};
パーティションとは
Windows PCを深いところまで触っている人は知っていると思いますが、一つの物理記憶領域を複数の記憶領域に分けて(パーティション)、それぞれが独立した物理記憶領域のように扱うことを言います。一つの物理記憶領域であるにも関わらずAというパーティションからはBというパーティションは見ることもできなければ存在すら知ることもできません。
ZephyrのフラッシュROM領域にも同じ概念が導入されており、アプリケーション領域やNVS記憶領域などのパーティションを定義することができます。
というところまで理解したところで
さっきのNVSサンプルのMemory Reportを見てみましょう。きっとdtsiに定義されたとおりパーティションができてい……ふぁっ?!
なんだよappパーティションって……さっきのdtsiに書いてあったboot_partitionなるものも見当たらないし、いったいどういうことなんだってばよ……。
sysbuildとパーティションマネージャー
SDK 3.0以降ではsysbuildが必須になりました。それに伴ってパーティションマネージャーは常に動作するという仕様になっています。そして、マルチイメージビルド時代からパーティションマネージャーが使われる場合はDTSに定義されているパーティションは無視され、パーティションマネージャーがパーティションを決定するという仕組みがあります。
つまりSDK 3.0以降においては先ほど紹介したnrf52840_partition.dtsiに記述されているパーティション定義はどう転んでも使われないということになるわけですが、だからと言って&flash0の定義がないとビルドエラーになってしまうためやむなく残っているという悲しい役割を持っています(笑)。
nvs_storageとは
おそらくパーティションを理解する中でこれが一番最初につまづく仕様じゃないかと思いますが、Kconfigの中には有効にした時点で自動生成されるパーティションというものがいくつもあります。もちろんここで取り上げている
CONFIG_NVS=y
もその一つです。他にもCONFIG_BOOTLOADER_MCUBOOT=y(sysbuildではSB_CONFIG_BOOTLOADER_MCUBOOT=y)にするとmcubootというパーティションを生成します。パーティションを自動生成するKconfigを複数使用すると、それぞれが自動生成するパーティションをパーティションマネージャーが適切に(?)配置していき、最後に残った領域をapp領域としてアプリケーションを書き込む領域に割り当てます。
これが先ほどのMemory Reportに表示されていたappパーティションです。
え?ちょっと待って、nvs_storageだと……?
あれ、さっきのMemory Reportにはなんて書いてありましたっけ?そうですね、nvs_storageです。ん~、nvs_storageね……nvs_……あれ、ソースコードに書いてあったパーティションってそんな名前だったっけ?
#define NVS_PARTITION storage_partition
ふぁ~っ(2回目)
どういうことなのでしょう?ソースコード上ではstorage_partitionと書いていますが、そんなパーティションはありません。なのになぜこれでビルドが通るのでしょうか?
以下、AIによる回答です。
知識ソースによると、flash_map_pm.h は複数のファイルシステム(NVS、LittleFS等)に対して storage_partition というラベルを定義しており、これらが同じフラッシュ領域を指すように変換されます
なるほど、要するにstorage_partitionと書いておけばなんでも通用するというわけですね(フラグw)
なぜか生成されないnvs_storage
先に書いたように他にもパーティションを生成するKconfigはいくつかあります。最も使うのはOSの設定を保存するCONFIG_SETTINGS=yとBluetoothのボンディング情報などを保存するCONFIG_BT_SETTINGS=yではないでしょうか。他にもMatterデバイスでも使いますが、これらのKconfigはsettings_storageというパーティションを自動生成します。
ということでNVSサンプルにCONFIG_SETTINGSを追加してみます。
CONFIG_FLASH=y
CONFIG_NVS=y
CONFIG_LOG=y
CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_NVS_LOG_LEVEL_DBG=y
CONFIG_REBOOT=y
CONFIG_MPU_ALLOW_FLASH_WRITE=y
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_SETTINGS=y
CONFIG_FLASH_MAP=y
これでパーティションが二つでき……ふぁーっ、できてない?
settings_storageはnvs_storageよりも優先度が高いため、settings_storageパーティションを生成するKconfigが存在するとnvs_storageは生成されません。
おそらくflash_map_pm.hのラベル変換の制約ではないかと思っています
そしてドツボにハマる
いや、でも別にビルドエラー出ないしな……。エラーが出ないのなら別にこのままでいいじゃないか、って思いますよね。僕もそう思ってずっとこの定義で使っていたのです。そう思って、というより、これが問題のある実装だという認識すらなかったわけですよ。
その一方でたまに保存データが破損するという現象が発生していたわけです。そもそもこれが原因だと気が付くまでにずいぶんと時間がかかりました。
様々な設定を書き込むパーティション
settings_storageを使うCONFIG_SETTINGSやCONFIG_BT_SETTINGSって、これらのKconfigを使っても自分では何も記述しないんですよね。実際、SDKが全部処理をしてしまうので、これらのKconfigがあるのとないのとで自分が記述するコードにはほとんど差が出ません。
唯一、追加する記述として
settings_load();
があります。SDK上で記録されたボンディング情報などを読み出すAPIです。つまり実際には裏で色々と記録されているわけですが、自分自身のソースコードには何も記述しないので実感できません。ちなみにフラッシュROMへの記録はハッシュ方式です。
一方NVSは
以前の記事でも紹介したように、NVSパーティションに書き込むのはnvs_writeというAPIを使用します。nvs_writeはID方式でフラッシュROMに書き込みを行います。
同じパーティションに複数のファイルシステムが書き込む?
あれ、ちょっと待ってくださいよ!
つまりCONFIG_SETTINGSはハッシュ方式でsettings_storageに書き込みを実行し、CONFIG_NVSはnvs_writeを使ってID方式で同じsettings_storageに書き込みを実行するってことですよね?普通に考えたらそんなのダメなんじゃね?
ええ、もちろんダメでした……。これが冒頭にあった保存データが壊れる原因です。
設定はsettings_storageにしか書き込めない
OS設定やボンディング情報の保存はSDKがやっていることなのでこれをいじることはできません。その一方でnvs_writeを使ったフラッシュROMへの書き込みは自分自身のソースコード上に全て記述しているので、こちらをなんとかしてこのコンフリクトを解消する必要があります。
解消する方法はいくつかあるのですが、比較的簡単なのはこちらです。
settings_save()だと……?
サンプルプロジェクトには一切出てきませんが、settings_storageパーティションに対してハッシュ方式でアプリケーション独自の値を保存することができます。というか、この問題について調べていて初めてこんなAPIの存在を知りました!
アプリケーションが独自で保存したいデータはsettings_save_one()というAPIを使って単独で保存することができるようです。つまり、nvs_writeなんか使うのを止めてしまって、settings_save_oneで全てをハッシュ形式で統一して保存するということです。
パーティションを分けるのも手
いやだ、俺はどうしてもnvs_writeを使って保存データを読み書きをしたいんだという人はpm_static.ymlを使って独自パーティションを生成し、
#define NVS_PARTITION original_storage
と直接指定することでsettings_storageパーティションとoriginal_storageパーティションの両方を扱うことができます。この場合、SDKレベルで保存するデータとアプリケーション独自で保存するデータが完全に分離されるので中身を調べるには調べやすいですがそれ以上のメリットはあまりありません。
また、フラッシュの仕様でテンポラリページが1ページ必要なので、パーティションが2つあると2ページは使えない領域が発生することになります。
さらに追い打ちですが、DFUでパーティションの構成を変更することは推奨されていないので、もしすでにリリースされてしまっているのであればこの方式にすることはできません。諦めてsettings_save_one()を使う形に実装し直しましょう(笑)。
総括
こんなの誰が理解できてんねん!(怒
ちなみにDev Zoneには同様の質問が結構いっぱいあるようです。

