Skip to main content

device_envoy_esp/
flash_block.rs

1//! A device abstraction for type-safe persistent storage in flash memory.
2//!
3//! This module provides a generic flash block storage system that allows storing any
4//! `serde`-compatible type in ESP's internal flash memory.
5//!
6//! See [`FlashBlockEsp`] for details and usage examples.
7#![cfg_attr(not(target_os = "none"), allow(dead_code))]
8
9#[cfg(target_os = "none")]
10use embassy_sync::blocking_mutex::Mutex;
11#[cfg(target_os = "none")]
12use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
13#[cfg(target_os = "none")]
14use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
15#[cfg(target_os = "none")]
16use portable_atomic::{AtomicU32, Ordering};
17#[cfg(target_os = "none")]
18use serde::{Deserialize, Serialize};
19#[cfg(target_os = "none")]
20use static_cell::StaticCell;
21
22#[cfg(target_os = "none")]
23use crate::{Error, Result};
24#[cfg(target_os = "none")]
25use device_envoy_core::flash_block::{
26    self as core_flash, Error as FlashBlockError, FlashBlock as CoreFlashBlock, FlashDevice,
27};
28
29pub use device_envoy_core::flash_block::FlashBlock;
30
31#[cfg(target_os = "none")]
32const FLASH_BLOCK_SIZE: usize = <esp_storage::FlashStorage<'static> as NorFlash>::ERASE_SIZE;
33#[cfg(target_os = "none")]
34const FLASH_BLOCK_SIZE_U32: u32 = FLASH_BLOCK_SIZE as u32;
35#[cfg(target_os = "none")]
36const DEFAULT_FLASH_REGION_BYTES: u32 = 16 * FLASH_BLOCK_SIZE_U32;
37
38// Local adapter — wraps esp_storage::FlashStorage so core's FlashDevice trait can be
39// implemented for a type defined in this crate (required by the orphan rule).
40#[cfg(target_os = "none")]
41struct EspFlashAdapter<'a>(&'a mut esp_storage::FlashStorage<'static>);
42
43#[cfg(target_os = "none")]
44impl FlashDevice for EspFlashAdapter<'_> {
45    type Error = esp_storage::FlashStorageError;
46
47    fn read(
48        &mut self,
49        offset: u32,
50        bytes: &mut [u8],
51    ) -> Result<(), esp_storage::FlashStorageError> {
52        ReadNorFlash::read(self.0, offset, bytes)
53    }
54
55    fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), esp_storage::FlashStorageError> {
56        NorFlash::write(self.0, offset, bytes)
57    }
58
59    fn erase(&mut self, from: u32, to: u32) -> Result<(), esp_storage::FlashStorageError> {
60        NorFlash::erase(self.0, from, to)
61    }
62}
63
64#[cfg(target_os = "none")]
65fn convert_flash_block_error(e: FlashBlockError<esp_storage::FlashStorageError>) -> Error {
66    match e {
67        FlashBlockError::Io(err) => Error::FlashStorage(err),
68        FlashBlockError::FormatError => Error::FormatError,
69        FlashBlockError::StorageCorrupted => Error::StorageCorrupted,
70    }
71}
72
73#[cfg(target_os = "none")]
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75enum FlashRegionRequest {
76    Tail { byte_len: u32 },
77}
78
79#[cfg(target_os = "none")]
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81struct ResolvedFlashRegion {
82    start_offset: u32,
83    block_count: u32,
84}
85
86#[cfg(target_os = "none")]
87impl FlashRegionRequest {
88    fn resolve(self, flash_capacity: u32) -> Result<ResolvedFlashRegion> {
89        let Self::Tail { byte_len } = self;
90        if byte_len == 0 || byte_len > flash_capacity {
91            return Err(Error::InvalidFlashRegion);
92        }
93        let start_offset = flash_capacity - byte_len;
94
95        if start_offset % FLASH_BLOCK_SIZE_U32 != 0 || byte_len % FLASH_BLOCK_SIZE_U32 != 0 {
96            return Err(Error::InvalidFlashRegion);
97        }
98        let end_offset = start_offset
99            .checked_add(byte_len)
100            .ok_or(Error::InvalidFlashRegion)?;
101        if end_offset > flash_capacity {
102            return Err(Error::InvalidFlashRegion);
103        }
104        Ok(ResolvedFlashRegion {
105            start_offset,
106            block_count: byte_len / FLASH_BLOCK_SIZE_U32,
107        })
108    }
109}
110
111#[cfg(target_os = "none")]
112struct FlashManager {
113    flash_storage:
114        Mutex<CriticalSectionRawMutex, core::cell::RefCell<esp_storage::FlashStorage<'static>>>,
115    next_block: AtomicU32,
116    requested_region: FlashRegionRequest,
117    resolved_region: ResolvedFlashRegion,
118}
119
120#[cfg(target_os = "none")]
121impl FlashManager {
122    fn new(
123        flash: esp_hal::peripherals::FLASH<'static>,
124        requested_region: FlashRegionRequest,
125    ) -> Result<Self> {
126        let flash_storage = esp_storage::FlashStorage::new(flash);
127        let flash_capacity = ReadNorFlash::capacity(&flash_storage) as u32;
128        let resolved_region = requested_region.resolve(flash_capacity)?;
129        Ok(Self {
130            flash_storage: Mutex::new(core::cell::RefCell::new(flash_storage)),
131            next_block: AtomicU32::new(0),
132            requested_region,
133            resolved_region,
134        })
135    }
136
137    fn with_flash<R>(
138        &self,
139        f: impl FnOnce(&mut esp_storage::FlashStorage<'static>) -> Result<R>,
140    ) -> Result<R> {
141        self.flash_storage.lock(|flash_storage| {
142            let mut flash_storage_ref = flash_storage.borrow_mut();
143            f(&mut flash_storage_ref)
144        })
145    }
146
147    fn reserve<const N: usize>(&'static self) -> Result<[FlashBlockEsp; N]> {
148        let start_block = self.next_block.fetch_add(N as u32, Ordering::SeqCst);
149        let end_block = start_block
150            .checked_add(N as u32)
151            .ok_or(Error::IndexOutOfBounds)?;
152        if end_block > self.resolved_region.block_count {
153            self.next_block.fetch_sub(N as u32, Ordering::SeqCst);
154            return Err(Error::IndexOutOfBounds);
155        }
156
157        Ok(core::array::from_fn(|block_index| FlashBlockEsp {
158            manager: self,
159            block_id: start_block + block_index as u32,
160        }))
161    }
162
163    fn block_offset(&self, block_id: u32) -> Result<u32> {
164        if block_id >= self.resolved_region.block_count {
165            return Err(Error::IndexOutOfBounds);
166        }
167        let reverse_index = self.resolved_region.block_count - 1 - block_id;
168        Ok(self.resolved_region.start_offset + reverse_index * FLASH_BLOCK_SIZE_U32)
169    }
170}
171
172#[cfg(target_os = "none")]
173struct FlashBlockEspStatic {
174    manager_cell: StaticCell<FlashManager>,
175    manager_ref: Mutex<CriticalSectionRawMutex, core::cell::RefCell<Option<&'static FlashManager>>>,
176}
177
178#[cfg(target_os = "none")]
179impl FlashBlockEspStatic {
180    const fn new() -> Self {
181        Self {
182            manager_cell: StaticCell::new(),
183            manager_ref: Mutex::new(core::cell::RefCell::new(None)),
184        }
185    }
186
187    fn manager(
188        &'static self,
189        flash: esp_hal::peripherals::FLASH<'static>,
190        requested_region: FlashRegionRequest,
191    ) -> Result<&'static FlashManager> {
192        self.manager_ref.lock(|manager_slot| {
193            let mut manager_slot = manager_slot.borrow_mut();
194            if let Some(manager) = *manager_slot {
195                if manager.requested_region != requested_region {
196                    return Err(Error::FlashRegionMismatch);
197                }
198                return Ok(manager);
199            }
200
201            let manager_ref = self
202                .manager_cell
203                .init(FlashManager::new(flash, requested_region)?);
204            *manager_slot = Some(manager_ref);
205            Ok(manager_ref)
206        })
207    }
208}
209
210#[cfg(target_os = "none")]
211/// A device abstraction for type-safe persistent storage in flash memory.
212///
213/// `FlashBlockEsp` provides a generic flash-block storage system for ESP,
214/// allowing you to store any `serde`-compatible type in the device's internal flash.
215///
216/// Use [`FlashBlockEsp::new_array`] to allocate one or more blocks. Block operations like
217/// [`load`](FlashBlock::load), [`save`](FlashBlock::save), and
218/// [`clear`](FlashBlock::clear) are provided by [`FlashBlock`], so bring the trait into
219/// scope:
220///
221/// `use device_envoy_esp::flash_block::FlashBlock as _;`
222///
223/// # Features
224///
225/// - **Type safety**: Hash-based type checking prevents reading data written under a
226///   different Rust type name. The hash is derived from the full type path
227///   (for example, `app1::BootCounter`). **Trying to read a different type
228///   returns `Ok(None)`**. Structural changes (adding or removing fields) do not
229///   change the hash, but may cause deserialization to fail and return an error.
230/// - **Postcard serialization**: A compact, `no_std`-friendly binary format.
231///
232/// # Block allocation
233///
234/// Conceptually, flash is treated as an array of fixed-size erase blocks counted from
235/// the end of the configured region backward. Your code can split that array using
236/// destructuring assignment and hand individual blocks to subsystems that need
237/// persistent storage.
238///
239/// ⚠️ **Warning**: ESP firmware and user data share the same flash device.
240/// Allocating too many blocks can overwrite your firmware.
241///
242/// # Example
243///
244/// ```rust,no_run
245/// # #![no_std]
246/// # #![no_main]
247/// use device_envoy_esp::{Result, init_and_start, flash_block::{FlashBlockEsp, FlashBlock as _}};
248///
249/// #[derive(serde::Serialize, serde::Deserialize, Clone)]
250/// struct WifiPersistedState {
251///     ssid: heapless::String<32>,
252///     password: heapless::String<64>,
253///     timezone_offset_minutes: i32,
254/// }
255///
256/// # async fn example() -> Result<Infallible> {
257/// init_and_start!(p);
258/// let [mut wifi_persisted_state_flash_block, mut fields_flash_block] =
259///     FlashBlockEsp::new_array::<2>(p.FLASH)?;
260///
261/// let wifi_persisted_state = wifi_persisted_state_flash_block.load::<WifiPersistedState>()?;
262/// if wifi_persisted_state.is_none() {
263///     let wifi_persisted_state = WifiPersistedState {
264///         ssid: heapless::String::new(),
265///         password: heapless::String::new(),
266///         timezone_offset_minutes: 0,
267///     };
268///     wifi_persisted_state_flash_block.save(&wifi_persisted_state)?;
269/// }
270///
271/// fields_flash_block.clear()?;
272/// # core::future::pending().await
273/// # }
274/// ```
275
276#[cfg(target_os = "none")]
277#[derive(Clone, Copy)]
278pub struct FlashBlockEsp {
279    manager: &'static FlashManager,
280    block_id: u32,
281}
282
283#[cfg(target_os = "none")]
284impl CoreFlashBlock for FlashBlockEsp {
285    type Error = Error;
286
287    fn load<T>(&mut self) -> Result<Option<T>>
288    where
289        T: Serialize + for<'de> Deserialize<'de>,
290    {
291        let block_offset = self.manager.block_offset(self.block_id)?;
292        self.manager.with_flash(|flash_storage| {
293            let mut adapter = EspFlashAdapter(flash_storage);
294            core_flash::load_block::<{ FLASH_BLOCK_SIZE }, T, _>(&mut adapter, block_offset)
295                .map_err(convert_flash_block_error)
296        })
297    }
298
299    fn save<T>(&mut self, value: &T) -> Result<()>
300    where
301        T: Serialize + for<'de> Deserialize<'de>,
302    {
303        let block_offset = self.manager.block_offset(self.block_id)?;
304        self.manager.with_flash(|flash_storage| {
305            let mut adapter = EspFlashAdapter(flash_storage);
306            core_flash::save_block::<{ FLASH_BLOCK_SIZE }, _, _>(&mut adapter, block_offset, value)
307                .map_err(convert_flash_block_error)
308        })
309    }
310
311    fn clear(&mut self) -> Result<()> {
312        let block_offset = self.manager.block_offset(self.block_id)?;
313        self.manager.with_flash(|flash_storage| {
314            let mut adapter = EspFlashAdapter(flash_storage);
315            core_flash::clear_block::<{ FLASH_BLOCK_SIZE }, _>(&mut adapter, block_offset)
316                .map_err(convert_flash_block_error)
317        })
318    }
319}
320
321#[cfg(target_os = "none")]
322impl FlashBlockEsp {
323    /// Reserve `N` blocks in the default tail region.
324    pub fn new_array<const N: usize>(
325        flash: esp_hal::peripherals::FLASH<'static>,
326    ) -> Result<[FlashBlockEsp; N]> {
327        Self::new_array_with_request(
328            flash,
329            FlashRegionRequest::Tail {
330                byte_len: DEFAULT_FLASH_REGION_BYTES,
331            },
332        )
333    }
334
335    fn new_array_with_request<const N: usize>(
336        flash: esp_hal::peripherals::FLASH<'static>,
337        requested_region: FlashRegionRequest,
338    ) -> Result<[FlashBlockEsp; N]> {
339        static FLASH_BLOCK_ESP_STATIC: FlashBlockEspStatic = FlashBlockEspStatic::new();
340        let manager = FLASH_BLOCK_ESP_STATIC.manager(flash, requested_region)?;
341        manager.reserve::<N>()
342    }
343}