speck-core 0.2.0

Secure runtime package manager for MMU-less microcontrollers
Documentation
//! NOR Flash abstraction with wear leveling and transaction support
//! 
//! Models typical small SPI NOR flash chips:
//! - 4KB pages (erase granularity)
//! - 256B write granularity (program page size)
//! - Limited erase cycles (~100k)

use alloc::vec::Vec;
use core::ops::Range;
use crate::error::{Error, Result};

/// Default page size (4KB)
pub const DEFAULT_PAGE_SIZE: usize = 4096;

/// Default number of pages (16 = 64KB)
pub const DEFAULT_NUM_PAGES: usize = 16;

/// Program page size (256 bytes)
pub const PROGRAM_PAGE_SIZE: usize = 256;

/// Page identifier (0-based)
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PageId(pub usize);

impl PageId {
    /// Convert to byte offset
    pub fn to_offset(&self, page_size: usize) -> usize {
        self.0 * page_size
    }
    
    /// Check if valid for given flash size
    pub fn is_valid(&self, num_pages: usize) -> bool {
        self.0 < num_pages
    }
}

/// Flash configuration
#[derive(Clone, Debug)]
pub struct FlashConfig {
    /// Page size in bytes (erase unit)
    pub page_size: usize,
    /// Number of pages
    pub num_pages: usize,
    /// Program page size (write unit)
    pub program_size: usize,
    /// Maximum erase cycles per page
    pub max_erase_cycles: u32,
}

impl Default for FlashConfig {
    fn default() -> Self {
        Self {
            page_size: DEFAULT_PAGE_SIZE,
            num_pages: DEFAULT_NUM_PAGES,
            program_size: PROGRAM_PAGE_SIZE,
            max_erase_cycles: 100_000,
        }
    }
}

/// Simulated NOR Flash storage
#[derive(Debug)]
pub struct Flash {
    config: FlashConfig,
    data: Vec<u8>,
    erase_counts: Vec<u32>,
    erased: Vec<bool>, // Tracks which pages are in erased state (0xFF)
}

impl Flash {
    /// Create new flash simulation with default config (64KB)
    pub fn new() -> Self {
        Self::with_config(FlashConfig::default())
    }
    
    /// Create with custom configuration
    pub fn with_config(config: FlashConfig) -> Self {
        let num_pages = config.num_pages;
        let size = config.page_size * num_pages;
        Self {
            config,
            data: vec![0xFF; size],
            erase_counts: vec![0; num_pages],
            erased: vec![true; num_pages],
        }
    }
    
    /// Get total capacity
    pub fn capacity(&self) -> usize {
        self.config.page_size * self.config.num_pages
    }
    
    /// Get page size
    pub fn page_size(&self) -> usize {
        self.config.page_size
    }
    
    /// Erase a page (sets all bytes to 0xFF)
    /// 
    /// # Errors
    /// Returns error if page index is out of range or max erase cycles exceeded
    pub fn erase_page(&mut self, page: PageId) -> Result<()> {
        if !page.is_valid(self.config.num_pages) {
            return Err(Error::flash(format!(
                "page {} out of range (0-{})", 
                page.0, self.config.num_pages - 1
            )));
        }
        
        if self.erase_counts[page.0] >= self.config.max_erase_cycles {
            return Err(Error::flash(format!(
                "page {} exceeded max erase cycles ({})",
                page.0, self.config.max_erase_cycles
            )));
        }
        
        let start = page.to_offset(self.config.page_size);
        self.data[start..start + self.config.page_size].fill(0xFF);
        self.erase_counts[page.0] += 1;
        self.erased[page.0] = true;
        
        Ok(())
    }
    
    /// Write data to flash
    /// 
    /// Flash can only change bits from 1 to 0, not 0 to 1.
    /// Attempting to set a bit from 0 to 1 returns an error.
    pub fn write(&mut self, offset: usize, data: &[u8]) -> Result<()> {
        if offset.saturating_add(data.len()) > self.capacity() {
            return Err(Error::flash(format!(
                "write out of bounds: offset={}, len={}, capacity={}",
                offset, data.len(), self.capacity()
            )));
        }
        
        // Check for write violations (0 -> 1 transitions)
        for (i, &byte) in data.iter().enumerate() {
            let current = self.data[offset + i];
            if byte & !current != 0 {
                let page = (offset + i) / self.config.page_size;
                return Err(Error::flash(format!(
                    "write violation at offset {}: attempting 0->1 transition (page {} not erased?)",
                    offset + i, page
                )));
            }
        }
        
        self.data[offset..offset + data.len()].copy_from_slice(data);
        
        // Mark affected pages as modified (not fully erased)
        let start_page = offset / self.config.page_size;
        let end_page = (offset + data.len()) / self.config.page_size;
        for p in start_page..=end_page {
            if p < self.config.num_pages {
                self.erased[p] = false;
            }
        }
        
        Ok(())
    }
    
    /// Read data from flash
    pub fn read(&self, offset: usize, len: usize) -> Result<&[u8]> {
        if offset.saturating_add(len) > self.capacity() {
            return Err(Error::flash("read out of bounds"));
        }
        Ok(&self.data[offset..offset + len])
    }
    
    /// Get erase count for a page
    pub fn get_erase_count(&self, page: PageId) -> Option<u32> {
        self.erase_counts.get(page.0).copied()
    }
    
    /// Check if page is in erased state
    pub fn is_erased(&self, page: PageId) -> bool {
        self.erased.get(page.0).copied().unwrap_or(false)
    }
    
    /// Find pages with lowest erase counts (for wear leveling)
    pub fn find_wear_leveled_pages(&self, count: usize) -> Vec<PageId> {
        let mut indexed: Vec<_> = self.erase_counts.iter()
            .enumerate()
            .map(|(i, &count)| (count, i))
            .collect();
        indexed.sort();
        indexed.iter().take(count).map(|(_, i)| PageId(*i)).collect()
    }
    
    /// Get total erase cycles across all pages
    pub fn total_erase_cycles(&self) -> u64 {
        self.erase_counts.iter().map(|&c| c as u64).sum()
    }
    
    /// Dump flash contents as hex (for debugging)
    #[cfg(feature = "std")]
    pub fn dump(&self, start: usize, len: usize) -> String {
        use std::format;
        let end = (start + len).min(self.capacity());
        let mut result = String::new();
        for chunk in self.data[start..end].chunks(16) {
            for byte in chunk {
                result.push_str(&format!("{:02x} ", byte));
            }
            result.push('\n');
        }
        result
    }
}

impl Default for Flash {
    fn default() -> Self {
        Self::new()
    }
}