Chapter 11
NVS 系ストレージ — ESP-IDF NVS・Zephyr NVS・キー値の共通形
この章のゴール.
ESP-IDF NVS と Zephyr NVS を読み解け、 10 章の NVM3 と合わせて「追記型キー値ストアの共通パターン」を身につけること。 対象が変わっても「エントリ・世代・最新版」という同じ見方が使えることを確認する。
この章で使う既出の用語(定義は各リンク先). CRC(03 章 8 節)、オフセット(03 章 1 節)、固定長(03 章 5 節)、QEMU(04 章 6 節)、マップ(04 章 2 節)、テーブル(07 章 6 節)、文字列(07 章 6 節)、Storage(08 章 2 節)、NVM3(10 章 1 節)、ページ(10 章 4 節)
1. 「NVS」という名前の重複に注意
NVS(Non-Volatile Storage)は一般名詞で、複数の別物がこの名を使う。
| 名前 | 提供元 | 対象チップ |
|---|---|---|
| ESP-IDF NVS | Espressif | ESP32 系 |
| Zephyr NVS | Zephyr RTOS | 汎用(Nordic、STM32、NXP…) |
| Zephyr NVS の後継 ZMS | Zephyr(新しい版) | 汎用 |
構造は違うが、追記型のキー値ストアという思想は 10 章の NVM3 と共通である。まず ESP-IDF NVS を詳しく、次に Zephyr NVS、最後に共通パターンをまとめる。
2. ESP-IDF NVS の構造
ESP32 のフラッシュは、先頭付近のパーティションテーブル(オフセット 0x8000、マジック 0xAA 0x50)で領域が分かれる。その中の nvs タイプのパーティションが NVS である。
esptool.py read_flash 0x9000 0x6000 nvs.bin # パーティションテーブルで示された nvs 領域
# パーティションテーブル自体を読む
esptool.py read_flash 0x8000 0xC00 pt.bin
gen_esp32part.py pt.bin # 人が読める表にNVS 領域は 4 KB のページに分かれ、各ページは次の構成。
| 部分 | 大きさ | 内容 |
|---|---|---|
| ページヘッダ | 32 バイト | 状態(active / full / freeing)、シーケンス番号、CRC |
| エントリ状態ビットマップ | 32 バイト | 各エントリが「空 / 書込済 / 消去済」のどれか(2 ビット × 126) |
| エントリ | 32 バイト × 126 | 実際のキー値データ |
各エントリ(32 バイト)の形:
| オフセット | 内容 |
|---|---|
| 0x00 | Namespace インデックス(キーをグループ分けする番号) |
| 0x01 | Type(u8/i8/…/u32/i64/string/blob) |
| 0x02 | Span(このエントリが占める 32 バイト単位の数。長い値は複数エントリ) |
| 0x03 | ChunkIndex(blob を分割したときの断片番号) |
| 0x04 | CRC32 |
| 0x08 | Key(最大 15 文字の NUL 終端文字列 + 予約) |
| 0x18 | Value(小さい値はここに直接。大きい値は後続エントリを指す) |
読み方:
- ページヘッダのシーケンス番号で、ページの新旧順を決める
- エントリ状態ビットマップで「書込済」のエントリだけを見る(消去済みは飛ばす)
- 各エントリの Namespace・Key・Type・Value を読む
- 同じ Namespace + Key が複数あれば、新しいページ・後ろのエントリが最新(また追記型)
- Type が string / blob なら、Span 分の後続エントリをつないで値を組み立てる
公式ツール
ESP-IDF 5.x には解析ツールがある(版により名前が違う)。
python nvs_tool.py nvs.bin # キー値を一覧(ESP-IDF 5.x)
# 生成側(参考)
python nvs_partition_gen.py generate in.csv out.bin 0x6000esp-idf-nvs-partition-gen とコミュニティ製の nvs_read / パーサも使える。
3. Zephyr NVS の構造
Zephyr NVS は、10・11 章の中で最もシンプルな追記型で、16 ビットの ID をキーにする(キーは文字列ではなく番号。NVM3 に近い)。
構造の要点:
- フラッシュ領域をセクタ(消去ブロック単位)に分ける
- 各書き込みは、データを領域の前方から、メタデータ(Allocation Table Entry、ATE)を領域の後方から、互いに向かって伸ばす(両端から詰める)
- ATE は「ID・データのオフセット・長さ・CRC」を持つ固定長の記録。後方に新しい ATE ほど手前に積まれる
- 同じ ID の値を更新すると、新しい ATE が追記され、古い ATE は残る(最新の ATE が有効)
- セクタが満杯になると、有効な最新データだけ次のセクタへコピーして古いセクタを消す
Zephyr のソース(subsys/fs/nvs/nvs.c)に ATE 構造体(struct nvs_ate)の定義があり、これが仕様書になる。 後継の ZMS(Zephyr Memory Storage)は、消去操作を必要としないメモリ(RRAM・MRAM)や大容量フラッシュも扱えるように設計が拡張されている。
4. 追記型キー値ストアの共通パターン
10 章の NVM3、この章の ESP-IDF NVS と Zephyr NVS は、細部は違っても同じ 5 つの部品を持つ。一度この型を覚えると、初めて見る独自ストレージも読める。
| 部品 | NVM3 | ESP-IDF NVS | Zephyr NVS |
|---|---|---|---|
| 区画を分ける単位 | ページ(消去ブロック) | 4 KB ページ | セクタ |
| 世代の順序 | 消去カウンタ | ページのシーケンス番号 | ATE の並び順(後方から) |
| キー | 20 ビット番号 | Namespace + 文字列 | 16 ビット ID |
| エントリの状態 | 型 / 削除マーカ | 状態ビットマップ | ATE の有無・CRC |
| 最新版の決定 | 後に書かれたもの | 後に書かれたもの | 後の ATE |
解析の共通手順:
- 区画を消去ブロック(ページ / セクタ)に区切る
- 各ブロックの世代番号を読み、新旧を並べる
- 各ブロックから有効なエントリを拾う(状態フラグ・削除マーカを見て、消去済みは飛ばす)
- 全エントリを集め、同じキーは最新世代のものを採用
- 値の型に従って中身を解釈する(03 章)
5. 履歴とセキュリティ
10 章と同じく、追記型なので過去の値が残る。
- 障害解析: あるキーが「いつ、どの値から、どの値へ」変わったかを、世代順に並べれば追える
- セキュリティ: 消したはずの Wi-Fi パスワードや鍵が、古いエントリに平文で残っていないか(13 章)
6. マウントできる実装もある
ESP-IDF NVS には FUSE 経由のツールが、Zephyr は QEMU / ネイティブ実行でストレージを再現する手段があるが、解析目的ならダンプを直接パースするのが速くて確実である。専用ツール(nvs_tool.py)→ 手動パースの順に試す。
7. 手を動かす
3 つのストアを同じ型で読む
この章のポイント
- 「NVS」は複数の別物(ESP-IDF・Zephyr)。だが追記型キー値ストアという思想は NVM3 と共通
- ESP-IDF NVS: 4 KB ページ + 状態ビットマップ + 32 バイトエントリ(Namespace + 文字列キー + Type + Value)
- Zephyr NVS: 16 ビット ID、データは前方・ATE は後方から両端で詰める
- 共通の 5 部品(区画・世代・キー・状態・最新版)と 5 手順で、初見のストアも読める
- ここでも古い値が履歴として残る。ソース(
nvs.c)が仕様書