Skip to main content

device_envoy/
flash_array.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 Raspberry Pi Pico's internal flash memory.
5//!
6//! See [`FlashArray`] for details and usage examples.
7
8use core::array;
9use core::cell::RefCell;
10use crc32fast::Hasher;
11use defmt::{error, info};
12use embassy_rp::Peri;
13use embassy_rp::flash::{Blocking, ERASE_SIZE, Flash as EmbassyFlash};
14use embassy_rp::peripherals::FLASH;
15use embassy_sync::blocking_mutex::Mutex;
16use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
17use portable_atomic::{AtomicU32, Ordering};
18use serde::{Deserialize, Serialize};
19use static_cell::StaticCell;
20
21use crate::{Error, Result};
22
23// Internal flash size for Raspberry Pi Pico 2 (4 MB).
24#[cfg(feature = "pico2")]
25const INTERNAL_FLASH_SIZE: usize = 4 * 1024 * 1024;
26
27// Internal flash size for Raspberry Pi Pico 1 W (2 MB).
28#[cfg(all(not(feature = "pico2"), feature = "pico1"))]
29const INTERNAL_FLASH_SIZE: usize = 2 * 1024 * 1024;
30
31// Internal flash size fallback (2 MB).
32#[cfg(all(not(feature = "pico2"), not(feature = "pico1")))]
33pub const INTERNAL_FLASH_SIZE: usize = 2 * 1024 * 1024;
34
35const MAGIC: u32 = 0x424C_4B53; // 'BLKS'
36const HEADER_SIZE: usize = 4 + 4 + 2; // Magic + TypeHash + PayloadLen
37const CRC_SIZE: usize = 4;
38const MAX_PAYLOAD_SIZE: usize = ERASE_SIZE - HEADER_SIZE - CRC_SIZE; // 3900 bytes
39const TOTAL_BLOCKS: u32 = (INTERNAL_FLASH_SIZE / ERASE_SIZE) as u32;
40
41/// Shared flash manager that owns the hardware driver and allocation cursor.
42struct FlashManager {
43    flash: Mutex<
44        CriticalSectionRawMutex,
45        RefCell<EmbassyFlash<'static, FLASH, Blocking, INTERNAL_FLASH_SIZE>>,
46    >,
47    next_block: AtomicU32,
48}
49
50impl FlashManager {
51    fn new(peripheral: Peri<'static, FLASH>) -> Self {
52        Self {
53            flash: Mutex::new(core::cell::RefCell::new(EmbassyFlash::new_blocking(
54                peripheral,
55            ))),
56            next_block: AtomicU32::new(0),
57        }
58    }
59
60    fn with_flash<R>(
61        &self,
62        f: impl FnOnce(&mut EmbassyFlash<'static, FLASH, Blocking, INTERNAL_FLASH_SIZE>) -> Result<R>,
63    ) -> Result<R> {
64        self.flash.lock(|flash| {
65            let mut flash_ref = flash.borrow_mut();
66            f(&mut *flash_ref)
67        })
68    }
69
70    fn reserve<const N: usize>(&'static self) -> Result<[FlashBlock; N]> {
71        let start = self.next_block.fetch_add(N as u32, Ordering::SeqCst);
72        let end = start.checked_add(N as u32).ok_or(Error::IndexOutOfBounds)?;
73        if end > TOTAL_BLOCKS {
74            // rollback
75            self.next_block.fetch_sub(N as u32, Ordering::SeqCst);
76            return Err(Error::IndexOutOfBounds);
77        }
78        Ok(array::from_fn(|idx| FlashBlock {
79            manager: self,
80            block: start + idx as u32,
81        }))
82    }
83}
84
85/// Type of a [`FlashArray`] block, with methods such as [`load`](Self::load), [`save`](Self::save), and [`clear`](Self::clear).
86///
87/// See [`FlashArray`] for usage examples.
88pub struct FlashBlock {
89    manager: &'static FlashManager,
90    block: u32,
91}
92
93impl FlashBlock {
94    /// Load data stored in this block.
95    ///
96    /// See [`FlashArray`] for usage examples.
97    pub fn load<T>(&mut self) -> Result<Option<T>>
98    where
99        T: Serialize + for<'de> Deserialize<'de>,
100    {
101        load_block(self.manager, self.block)
102    }
103
104    /// Save data to this block.
105    ///
106    /// See [`FlashArray`] for usage examples.
107    pub fn save<T>(&mut self, value: &T) -> Result<()>
108    where
109        T: Serialize + for<'de> Deserialize<'de>,
110    {
111        save_block(self.manager, self.block, value)
112    }
113
114    /// Clear this block.
115    pub fn clear(&mut self) -> Result<()> {
116        clear_block(self.manager, self.block)
117    }
118}
119
120/// Static resources for [`FlashArray`].
121pub(crate) struct FlashArrayStatic {
122    manager_cell: StaticCell<FlashManager>,
123    manager_ref: Mutex<CriticalSectionRawMutex, core::cell::RefCell<Option<&'static FlashManager>>>,
124}
125
126impl FlashArrayStatic {
127    #[must_use]
128    const fn new() -> Self {
129        Self {
130            manager_cell: StaticCell::new(),
131            manager_ref: Mutex::new(core::cell::RefCell::new(None)),
132        }
133    }
134
135    fn manager(&'static self, peripheral: Peri<'static, FLASH>) -> &'static FlashManager {
136        self.manager_ref.lock(|slot_cell| {
137            let mut slot = slot_cell.borrow_mut();
138            if slot.is_none() {
139                let manager_mut = self.manager_cell.init(FlashManager::new(peripheral));
140                let manager_ref: &'static FlashManager = manager_mut;
141                *slot = Some(manager_ref);
142            }
143            slot.expect("manager initialized")
144        })
145    }
146}
147
148/// A device abstraction for type-safe persistent storage in flash memory.
149///
150/// This struct provides a generic flash-block storage system for Raspberry Pi Pico,
151/// allowing you to store any `serde`-compatible type in the device’s internal flash.
152///
153/// You choose the number of storage blocks at compile time. Each block holds up to
154/// 3900 bytes of postcard-serialized data (a hardware-determined 4 KB flash block
155/// minus metadata space).
156///
157/// # Features
158///
159/// - **Type safety**: Hash-based type checking prevents reading data written under a
160///   different Rust type name. The hash is derived from the full type path
161///   (for example, `app1::BootCounter`). **Trying to read a different types
162///   returns `Ok(None)`**. Structural changes (adding or removing fields) do not
163///   change the hash, but may cause deserialization to fail and return an error.
164/// - **Postcard serialization**: A compact, `no_std`-friendly binary format.
165///
166/// # Block allocation
167///
168/// Conceptually, flash is treated as an array of fixed-size erase blocks counted from
169/// the end of memory backward. Your code can split that array using destructuring
170/// assignment and hand individual blocks to subsystems that need persistent storage.
171///
172/// ⚠️ **Warning**: Pico 1 and Pico 2 store firmware, vector tables, and user data in the
173/// same flash device. Allocating too many blocks can overwrite your firmware.
174
175///
176/// # Example
177///
178/// ```rust,no_run
179/// # #![no_std]
180/// # #![no_main]
181/// # use panic_probe as _;
182/// # use defmt_rtt as _;
183/// # use core::{convert::Infallible, future};
184/// use device_envoy::flash_array::FlashArray;
185/// # use defmt::info;
186///
187/// /// Boot counter (newtype) that wraps at 10.
188/// /// Stored with `postcard` (Serde).
189/// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
190/// struct BootCounter(u8);
191///
192/// impl BootCounter {
193///     const fn new(value: u8) -> Self {
194///         Self(value)
195///     }
196///
197///     fn increment(self) -> Self {
198///         Self((self.0 + 1) % 10)
199///     }
200/// }
201///
202/// async fn example() -> device_envoy::Result<Infallible> {
203///     let p = embassy_rp::init(Default::default());
204///
205///     // Create a flash array. You can destructure it however you like.
206///     let [mut boot_counter_flash_block] = FlashArray::<1>::new(p.FLASH)?;
207///
208///     // Read boot counter from flash then increment.
209///     // FlashArray includes a runtime type hash so values are only loaded
210///     // if the stored type matches the requested type; mismatches yield `None`.
211///     let boot_counter = boot_counter_flash_block
212///         .load()?
213///         .unwrap_or(BootCounter::new(0)) // Default to 0 type not present
214///         .increment();
215///
216///     // Write incremented counter back to flash.
217///     // This example writes once per power-up (fine for a demo; don't write in a tight loop).
218///     // Flash is typically good for ~100K erase cycles per block.
219///     boot_counter_flash_block.save(&boot_counter)?;
220///
221///     info!("Boot counter: {}", boot_counter.0);
222///     future::pending().await // Keep running
223/// }
224/// ```
225pub struct FlashArray<const N: usize>;
226
227impl<const N: usize> FlashArray<N> {
228    /// Reserve `N` contiguous blocks and return them as an array that you can destructure however you like.
229    ///
230    /// See [`FlashArray`] for usage examples.
231    pub fn new(peripheral: Peri<'static, FLASH>) -> Result<[FlashBlock; N]> {
232        static FLASH_STATIC: FlashArrayStatic = FlashArrayStatic::new();
233        let manager = FLASH_STATIC.manager(peripheral);
234        manager.reserve::<N>()
235    }
236}
237
238fn save_block<T>(manager: &'static FlashManager, block: u32, value: &T) -> Result<()>
239where
240    T: Serialize + for<'de> Deserialize<'de>,
241{
242    let mut payload_buffer = [0u8; MAX_PAYLOAD_SIZE];
243    let payload_len = postcard::to_slice(value, &mut payload_buffer)
244        .map_err(|_| {
245            error!(
246                "Flash: Serialization failed or data too large (max {} bytes)",
247                MAX_PAYLOAD_SIZE
248            );
249            Error::FormatError
250        })?
251        .len();
252
253    let mut buffer = [0xFFu8; ERASE_SIZE];
254    buffer[0..4].copy_from_slice(&MAGIC.to_le_bytes());
255    buffer[4..8].copy_from_slice(&compute_type_hash::<T>().to_le_bytes());
256    buffer[8..10].copy_from_slice(&(payload_len as u16).to_le_bytes());
257    buffer[HEADER_SIZE..HEADER_SIZE + payload_len].copy_from_slice(&payload_buffer[..payload_len]);
258
259    let crc_offset = HEADER_SIZE + payload_len;
260    let crc = compute_crc(&buffer[0..crc_offset]);
261    buffer[crc_offset..crc_offset + CRC_SIZE].copy_from_slice(&crc.to_le_bytes());
262
263    let offset = block_offset(block);
264    manager.with_flash(|flash| {
265        flash
266            .blocking_erase(offset, offset + ERASE_SIZE as u32)
267            .map_err(Error::Flash)?;
268        flash
269            .blocking_write(offset, &buffer)
270            .map_err(Error::Flash)?;
271        Ok(())
272    })?;
273
274    info!("Flash: Saved {} bytes to block {}", payload_len, block);
275    Ok(())
276}
277
278fn load_block<T>(manager: &'static FlashManager, block: u32) -> Result<Option<T>>
279where
280    T: Serialize + for<'de> Deserialize<'de>,
281{
282    let offset = block_offset(block);
283    let mut buffer = [0u8; ERASE_SIZE];
284
285    manager.with_flash(|flash| {
286        flash
287            .blocking_read(offset, &mut buffer)
288            .map_err(Error::Flash)?;
289        Ok(())
290    })?;
291
292    let magic = u32::from_le_bytes(buffer[0..4].try_into().unwrap());
293    if magic != MAGIC {
294        info!("Flash: No data at block {}", block);
295        return Ok(None);
296    }
297
298    let stored_type_hash = u32::from_le_bytes(buffer[4..8].try_into().unwrap());
299    let expected_type_hash = compute_type_hash::<T>();
300    if stored_type_hash != expected_type_hash {
301        info!(
302            "Flash: Type mismatch at block {} (expected hash {}, found {})",
303            block, expected_type_hash, stored_type_hash
304        );
305        return Ok(None);
306    }
307
308    let payload_len = u16::from_le_bytes(buffer[8..10].try_into().unwrap()) as usize;
309    if payload_len > MAX_PAYLOAD_SIZE {
310        error!(
311            "Flash: Invalid payload length {} at block {}",
312            payload_len, block
313        );
314        return Err(Error::StorageCorrupted);
315    }
316
317    let crc_offset = HEADER_SIZE + payload_len;
318    let stored_crc = u32::from_le_bytes(
319        buffer[crc_offset..crc_offset + CRC_SIZE]
320            .try_into()
321            .unwrap(),
322    );
323    let computed_crc = compute_crc(&buffer[0..crc_offset]);
324    if stored_crc != computed_crc {
325        error!(
326            "Flash: CRC mismatch at block {} (expected {}, found {})",
327            block, computed_crc, stored_crc
328        );
329        return Err(Error::StorageCorrupted);
330    }
331
332    let payload = &buffer[HEADER_SIZE..HEADER_SIZE + payload_len];
333    let value: T = postcard::from_bytes(payload).map_err(|_| {
334        error!("Flash: Deserialization failed at block {}", block);
335        Error::StorageCorrupted
336    })?;
337
338    info!("Flash: Loaded data from block {}", block);
339    Ok(Some(value))
340}
341
342fn clear_block(manager: &'static FlashManager, block: u32) -> Result<()> {
343    let offset = block_offset(block);
344    manager.with_flash(|flash| {
345        flash
346            .blocking_erase(offset, offset + ERASE_SIZE as u32)
347            .map_err(Error::Flash)?;
348        Ok(())
349    })?;
350    info!("Flash: Cleared block {}", block);
351    Ok(())
352}
353
354/// Blocks are allocated from the end of flash backwards.
355fn block_offset(block_id: u32) -> u32 {
356    let capacity = INTERNAL_FLASH_SIZE as u32;
357    capacity - (block_id + 1) * ERASE_SIZE as u32
358}
359
360/// Compute FNV-1a hash of the type name for type safety.
361fn compute_type_hash<T>() -> u32 {
362    const FNV_PRIME: u32 = 16_777_619;
363    const FNV_OFFSET: u32 = 2_166_136_261;
364
365    let type_name = core::any::type_name::<T>();
366    let mut hash = FNV_OFFSET;
367
368    for byte in type_name.bytes() {
369        hash ^= u32::from(byte);
370        hash = hash.wrapping_mul(FNV_PRIME);
371    }
372
373    hash
374}
375
376/// Compute CRC32 checksum.
377fn compute_crc(data: &[u8]) -> u32 {
378    let mut hasher = Hasher::new();
379    hasher.update(data);
380    hasher.finalize()
381}