コンテンツにスキップ

embedded-storage — Crate 詳細

embedded-storage

Stable no_std

組み込み向けの不揮発ストレージ抽象化 crate。EEPROM、FRAM、内蔵 Flash、外付け NOR Flash などを、ドライバ非依存の trait 境界で扱うための共通インターフェースを提供します。

A storage abstraction layer for embedded systems. It defines traits for non-volatile storage such as EEPROM, NOR flash, NAND flash, and internal or external flash devices.

概要

embedded-storage は、組み込みシステムで使う不揮発ストレージを抽象化するための trait 定義 crate です。公式 README では、この crate が EEPROM、NOR Flash、NAND Flash など、内部・外部を問わない複数種類の不揮発ストレージに対して実装できる trait 群を定義すると説明されています。したがって、この crate の役割は「特定チップの Flash ドライバ」ではなく、「上位ロジックと下位ドライバの間に置く共通インターフェース」です。

Rust 組み込み開発では、MCU 内蔵 Flash、外付け SPI NOR Flash、I2C EEPROM、FRAM など、保存先ごとに API や制約が大きく異なります。アプリケーション側が個別ドライバ API に直接依存すると、設定保存、ファームウェア更新、ブートローダ、テストコードなどが特定ハードウェアに密結合します。embedded-storage の trait を境界にすると、実機では HAL やドライバ実装を渡し、テストでは RAM ベースの mock を渡す、といった差し替えが可能になります。

API の構成

crate 直下には ReadStorageStorageRegion があり、nor_flash モジュールには NOR Flash 向けの ReadNorFlashNorFlashMultiwriteNorFlashNorFlashErrorNorFlashErrorKindErrorType、および check_read / check_write / check_erase が用意されています。docs.rs の item list でも、これらの trait・enum・helper 関数が公開 API として列挙されています。

ReadStorage / Storage は、比較的「普通のバイト列ストレージ」として扱えるデバイスを上位から透過的に扱うための trait です。Storage::write の説明では、必要なページ消去が自動的に行われる可能性があり、その結果 RMW、つまり read-modify-write 操作による性能影響があり得ると説明されています。そのため、単純な設定保存 API としては扱いやすい一方、実時間性や消去回数を厳密に管理したい層では、下位の NOR Flash trait を直接使う設計も検討対象になります。

nor_flash::ReadNorFlash / nor_flash::NorFlash は、NOR Flash の物理的制約をより直接的に表現する API です。GitHub 上の nor_flash.rs では、ReadNorFlashREAD_SIZEread()capacity() を持つこと、NorFlashErrorKindNotAlignedOutOfBoundsOther を持つことが確認できます。NorFlash 側では、write と erase の最小単位を associated const として表現し、上位コードがアライメント制約を意識できるようにしています。

NOR Flash で特に重要な考え方

NOR Flash は一般に、消去するとビットが 1 の状態、実装上は多くの場合 0xFF に戻り、書き込みでは 1 から 0 方向へビットを変化させます。0 から 1 に戻すには erase が必要です。この性質により、設定保存やログ追記では「どの単位で消去するか」「同じ word に再書き込みできるか」「電源断時にどの範囲が不定になるか」を設計時に考える必要があります。

MultiwriteNorFlash は、通常の NorFlash よりも write 制約が緩い実装を表す marker trait です。同じ erase block 内で追記的な 1→0 更新を許容できるデバイスやドライバでは、この性質を上位に伝えることで、ログ構造や wear leveling 設計の自由度が上がります。ただし、multiwrite が利用できるかどうかは実装依存であり、全ての Flash に対して前提にしてはいけません。

エラー設計

NOR Flash 実装は実装固有のエラー型を持てますが、NorFlashError trait により NorFlashErrorKind へ写像できます。これにより、上位コードは「アライメント不正」「範囲外」「実装固有エラー」のような大分類を見て処理方針を分けられます。例えば、NotAligned は呼び出し側バグとして扱う、OutOfBounds は領域設計ミスとして扱う、Other はデバイス異常やドライバ固有エラーとしてログに残す、という方針を立てやすくなります。

使いどころ

代表的な用途は、設定値保存、校正値保存、ブートローダのスロット管理、OTA 更新、簡易 key-value store、Flash-backed filesystem、Flash ドライバの共通テストです。特に、STM32、nRF52、ESP32、RP2040 など複数ターゲットを扱うプロジェクトでは、上位ロジックを embedded-storage の trait に寄せておくと、ターゲット固有の HAL や Flash ドライバとの差し替えが容易になります。

注意点

この crate はストレージ抽象を提供しますが、実デバイスの耐久性、erase block サイズ、write alignment、電源断時の保証、割り込み中の Flash 操作可否、XIP 実行中の Flash 書き換え制約などは隠蔽しません。実機設計では、対象 MCU や外付け Flash のデータシート、HAL 実装、RTOS / async executor との相互作用を別途確認する必要があります。

また、embedded-storage は同期 API です。erase に時間がかかるデバイスでは、同期呼び出しがタスク実行を長時間占有する可能性があります。Embassy など async 環境でストレージ処理を設計する場合は、関連 crate の embedded-storage-async や、専用タスクに Flash 操作を集約する設計を検討するとよいです。

バージョン
0.3.1
ライセンス
MIT OR Apache-2.0
メンテナンス
活発に開発中

コード例

EEPROMやFRAMのように、上位からread/write可能なストレージとして扱う例です。実機ドライバではなくRAM配列でmock実装しているため、設定保存ロジックのホストテストにも応用できます。

Storage traitを使った設定値の保存と読み出し
#![no_std]
use embedded_storage::{ReadStorage, Storage};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RamStorageError {
OutOfBounds,
}
pub struct RamStorage<const N: usize> {
buf: [u8; N],
}
impl<const N: usize> RamStorage<N> {
pub const fn new(fill: u8) -> Self {
Self { buf: [fill; N] }
}
fn range(&self, offset: u32, len: usize) -> Result<core::ops::Range<usize>, RamStorageError> {
let start = offset as usize;
let end = start.checked_add(len).ok_or(RamStorageError::OutOfBounds)?;
if end > N {
return Err(RamStorageError::OutOfBounds);
}
Ok(start..end)
}
}
impl<const N: usize> ReadStorage for RamStorage<N> {
type Error = RamStorageError;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
let range = self.range(offset, bytes.len())?;
bytes.copy_from_slice(&self.buf[range]);
Ok(())
}
fn capacity(&self) -> usize {
N
}
}
impl<const N: usize> Storage for RamStorage<N> {
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
let range = self.range(offset, bytes.len())?;
self.buf[range].copy_from_slice(bytes);
Ok(())
}
}
pub fn save_and_load_setting() -> Result<(), RamStorageError> {
let mut storage = RamStorage::<32>::new(0x00);
let setting = [0x12, 0x34, 0x56, 0x78];
let mut readback = [0u8; 4];
storage.write(8, &setting)?;
storage.read(8, &mut readback)?;
assert_eq!(readback, setting);
Ok(())
}

NOR Flashの基本制約であるerase単位、write単位、1から0方向への書き込みをRAM上で模擬する例です。実機Flashドライバの共通テストや、ブートローダ処理の抽象化を考えるときの出発点になります。

NorFlash traitを使ったerase → write → readの検証
#![no_std]
use embedded_storage::nor_flash::{
ErrorType, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MockNorFlashError {
NotAligned,
OutOfBounds,
WriteToNonErased,
}
impl NorFlashError for MockNorFlashError {
fn kind(&self) -> NorFlashErrorKind {
match self {
Self::NotAligned => NorFlashErrorKind::NotAligned,
Self::OutOfBounds => NorFlashErrorKind::OutOfBounds,
Self::WriteToNonErased => NorFlashErrorKind::Other,
}
}
}
pub struct MockNorFlash<const N: usize> {
buf: [u8; N],
}
impl<const N: usize> MockNorFlash<N> {
pub const fn new() -> Self {
Self { buf: [0xFF; N] }
}
fn range(&self, offset: u32, len: usize) -> Result<core::ops::Range<usize>, MockNorFlashError> {
let start = offset as usize;
let end = start.checked_add(len).ok_or(MockNorFlashError::OutOfBounds)?;
if end > N {
return Err(MockNorFlashError::OutOfBounds);
}
Ok(start..end)
}
}
impl<const N: usize> ErrorType for MockNorFlash<N> {
type Error = MockNorFlashError;
}
impl<const N: usize> ReadNorFlash for MockNorFlash<N> {
const READ_SIZE: usize = 1;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
let range = self.range(offset, bytes.len())?;
bytes.copy_from_slice(&self.buf[range]);
Ok(())
}
fn capacity(&self) -> usize {
N
}
}
impl<const N: usize> NorFlash for MockNorFlash<N> {
const WRITE_SIZE: usize = 4;
const ERASE_SIZE: usize = 16;
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
let from = from as usize;
let to = to as usize;
if from > to || to > N {
return Err(MockNorFlashError::OutOfBounds);
}
if from % Self::ERASE_SIZE != 0 || to % Self::ERASE_SIZE != 0 {
return Err(MockNorFlashError::NotAligned);
}
for byte in &mut self.buf[from..to] {
*byte = 0xFF;
}
Ok(())
}
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
if offset as usize % Self::WRITE_SIZE != 0 || bytes.len() % Self::WRITE_SIZE != 0 {
return Err(MockNorFlashError::NotAligned);
}
let range = self.range(offset, bytes.len())?;
for (dst, src) in self.buf[range].iter_mut().zip(bytes.iter()) {
// NOR Flash is normally programmed by changing bits from 1 to 0.
// If a requested bit would need to change from 0 to 1, erase is required first.
if (*dst & *src) != *src {
return Err(MockNorFlashError::WriteToNonErased);
}
*dst &= *src;
}
Ok(())
}
}
pub fn erase_write_read_roundtrip() -> Result<(), MockNorFlashError> {
let mut flash = MockNorFlash::<64>::new();
let data = [
0xAA, 0x55, 0x00, 0xFF,
0x10, 0x20, 0x30, 0x40,
0xFE, 0xDC, 0xBA, 0x98,
0x01, 0x23, 0x45, 0x67,
];
let mut readback = [0u8; 16];
flash.erase(0, 16)?;
flash.write(0, &data)?;
flash.read(0, &mut readback)?;
assert_eq!(readback, data);
Ok(())
}

関連 Crates