diff --git a/Cargo.lock b/Cargo.lock index cc23789..73bb3ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -781,6 +781,7 @@ dependencies = [ "hal-api", "hal-builder", "quote", + "regex", "serde_json", "syn", ] diff --git a/boards/nucleo_l4r5zi.dts b/boards/nucleo_l4r5zi.dts index ce19d57..a5f9fdc 100644 --- a/boards/nucleo_l4r5zi.dts +++ b/boards/nucleo_l4r5zi.dts @@ -211,10 +211,9 @@ osiris_udc0: &usbotg_fs { #address-cells = <1>; #size-cells = <1>; - /* Reserve last 16KiB for property storage */ - storage_partition: partition@1fb000 { - label = "storage"; - reg = <0x001fb000 DT_SIZE_K(16)>; + flash_partition: partition@0 { + label = "flash"; + reg = <0x0 DT_SIZE_K(2048)>; }; }; }; diff --git a/machine/api/src/flash_addr.rs b/machine/api/src/flash_addr.rs new file mode 100644 index 0000000..34df388 --- /dev/null +++ b/machine/api/src/flash_addr.rs @@ -0,0 +1,458 @@ +use core::marker::PhantomData; + +/// Forward `fmt`/`Hash` to the inner `usize`. `Debug` is left to each type so +/// the output reads as `FlashAddress(0x…)` rather than the bare integer. +macro_rules! forward_usize_traits { + ($t:ident) => { + impl core::fmt::Display for $t { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.0, f) + } + } + impl core::fmt::LowerHex for $t { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::LowerHex::fmt(&self.0, f) + } + } + impl core::fmt::UpperHex for $t { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::UpperHex::fmt(&self.0, f) + } + } + impl core::fmt::Octal for $t { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Octal::fmt(&self.0, f) + } + } + impl core::fmt::Binary for $t { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Binary::fmt(&self.0, f) + } + } + impl core::hash::Hash for $t { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } + } + }; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidArgument, + OutOfBounds, + Misaligned, + NotErased, + Busy, + /// Chip-level lock (`FLASH->CR.LOCK`) is set / unlock failed. + Locked, + DoubleUnlock, + InvalidPage, + TimedOut, + /// Internal sentinel: something the generic layer expected from the HAL + /// (e.g. `page_size() > 0`) was missing. Not for hardware-reported failures. + Io, + NotFound, + /// The DT marked this partition `read-only`; mutating operations + /// (erase/program/write) refuse to touch it. + ReadOnly, + /// Hardware refused the access because the cells are write- or + /// read-protected (option-byte WRP, RDP, …). + Protected, + /// Hardware programming operation failed for a sequencing/alignment/size + /// reason — typically "tried to program already-programmed cells" but + /// also covers fast-program failures. + ProgrammingFailed, + /// ECC double-bit error detected during read. + EccError, +} + +pub type Result = core::result::Result; + +/// Chip-side queries that the address newtypes need to validate inputs. +/// Implementors are typically zero-sized backend marker types. +pub trait Flash { + fn flash_base() -> usize; + fn total_size() -> usize; + /// Erase granularity (bytes); erase ops must be page-aligned. + fn page_size() -> usize; + fn page_count() -> usize; + /// Program granularity (bytes); `program`/`write` addresses and lengths + /// must be a multiple of this. + fn write_unit_bytes() -> usize; +} + +// --------------------------------------------------------------------------- +// FlashAddress +// --------------------------------------------------------------------------- + +/// An absolute address in the primary flash bank. Construction validates the +/// address falls within `[F::flash_base(), F::flash_base() + F::total_size())`. +pub struct FlashAddress(usize, PhantomData); + +impl FlashAddress { + pub fn new(address: usize) -> Result { + let base = F::flash_base(); + let end = base + .checked_add(F::total_size()) + .ok_or(Error::OutOfBounds)?; + if address < base || address >= end { + return Err(Error::OutOfBounds); + } + Ok(Self(address, PhantomData)) + } + + pub fn as_usize(self) -> usize { + self.0 + } + + pub fn flash_offset(self) -> FlashOffset { + FlashOffset(self.0 - F::flash_base(), PhantomData) + } + + pub fn page_index(self) -> usize { + (self.0 - F::flash_base()) / F::page_size() + } + + /// Add `bytes`, returning `Err(OutOfBounds)` if the result lands outside + /// the flash bank or overflows `usize`. + pub fn checked_add(self, bytes: usize) -> Result { + let next = self.0.checked_add(bytes).ok_or(Error::OutOfBounds)?; + Self::new(next) + } + + /// Subtract `bytes`, returning `Err(OutOfBounds)` if the result drops + /// below `flash_base()` or underflows. + pub fn checked_sub(self, bytes: usize) -> Result { + let prev = self.0.checked_sub(bytes).ok_or(Error::OutOfBounds)?; + Self::new(prev) + } +} + +impl FlashAddress { + /// Distance in bytes from `other` to `self` (i.e. `self - other`). Returns + /// `Err(OutOfBounds)` if `other > self`. Accepts any `Into`, + /// so you can pass a `FlashPageStart` or `FlashOffset` directly. + pub fn distance_from(self, other: impl Into>) -> Result { + let other: FlashAddress = other.into(); + self.0.checked_sub(other.0).ok_or(Error::OutOfBounds) + } +} + +impl Clone for FlashAddress { + fn clone(&self) -> Self { + *self + } +} +impl Copy for FlashAddress {} +impl PartialEq for FlashAddress { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} +impl Eq for FlashAddress {} +impl PartialOrd for FlashAddress { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for FlashAddress { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.cmp(&other.0) + } +} +impl core::fmt::Debug for FlashAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "FlashAddress({:#x})", self.0) + } +} +forward_usize_traits!(FlashAddress); + +impl From> for FlashAddress { + fn from(p: FlashPageStart) -> Self { + Self(p.0, PhantomData) + } +} + +impl From> for FlashAddress { + fn from(o: FlashOffset) -> Self { + Self(F::flash_base() + o.0, PhantomData) + } +} + +impl From> for FlashOffset { + fn from(a: FlashAddress) -> Self { + Self(a.0 - F::flash_base(), PhantomData) + } +} + +impl From> for FlashOffset { + fn from(p: FlashPageStart) -> Self { + Self(p.0 - F::flash_base(), PhantomData) + } +} + +impl TryFrom> for FlashPageStart { + type Error = Error; + fn try_from(a: FlashAddress) -> Result { + if (a.0 - F::flash_base()) % F::page_size() != 0 { + return Err(Error::Misaligned); + } + Ok(Self(a.0, PhantomData)) + } +} + +impl TryFrom> for FlashPageStart { + type Error = Error; + fn try_from(o: FlashOffset) -> Result { + if o.0 % F::page_size() != 0 { + return Err(Error::Misaligned); + } + Ok(Self(F::flash_base() + o.0, PhantomData)) + } +} + +impl From> for usize { + fn from(a: FlashAddress) -> Self { + a.0 + } +} + +impl From> for usize { + fn from(o: FlashOffset) -> Self { + o.0 + } +} + +impl From> for usize { + fn from(p: FlashPageStart) -> Self { + p.0 + } +} + +// --------------------------------------------------------------------------- +// FlashOffset +// --------------------------------------------------------------------------- + +/// Byte offset from `F::flash_base()`. Construction validates the offset is +/// within `[0, F::total_size())`. +pub struct FlashOffset(usize, PhantomData); + +impl FlashOffset { + pub fn new(offset: usize) -> Result { + if offset >= F::total_size() { + return Err(Error::OutOfBounds); + } + Ok(Self(offset, PhantomData)) + } + + pub fn as_usize(self) -> usize { + self.0 + } + + pub fn page_index(self) -> usize { + self.0 / F::page_size() + } + + /// Add `bytes`, returning `Err(OutOfBounds)` if the result is `>= total_size()`. + pub fn checked_add(self, bytes: usize) -> Result { + let next = self.0.checked_add(bytes).ok_or(Error::OutOfBounds)?; + Self::new(next) + } + + /// Subtract `bytes`, returning `Err(OutOfBounds)` if the result underflows. + pub fn checked_sub(self, bytes: usize) -> Result { + let prev = self.0.checked_sub(bytes).ok_or(Error::OutOfBounds)?; + Ok(Self(prev, PhantomData)) + } +} + +impl FlashOffset { + /// Distance in bytes from `other` to `self` (i.e. `self - other`). Returns + /// `Err(OutOfBounds)` if `other > self`. Accepts any `Into`, + /// so you can pass a `FlashAddress` or `FlashPageStart` directly. + pub fn distance_from(self, other: impl Into>) -> Result { + let other: FlashOffset = other.into(); + self.0.checked_sub(other.0).ok_or(Error::OutOfBounds) + } +} + +impl Clone for FlashOffset { + fn clone(&self) -> Self { + *self + } +} +impl Copy for FlashOffset {} +impl PartialEq for FlashOffset { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} +impl Eq for FlashOffset {} +impl PartialOrd for FlashOffset { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for FlashOffset { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.cmp(&other.0) + } +} +impl core::fmt::Debug for FlashOffset { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "FlashOffset({:#x})", self.0) + } +} +forward_usize_traits!(FlashOffset); + +// --------------------------------------------------------------------------- +// FlashPageStart +// --------------------------------------------------------------------------- + +/// An absolute flash address aligned to a page boundary. Construction +/// validates both that the address is in flash and that it is page-aligned, +/// so any value of this type is a valid argument to a page-erase op. +pub struct FlashPageStart(usize, PhantomData); + +impl FlashPageStart { + pub fn new(address: usize) -> Result { + let base = F::flash_base(); + let end = base + .checked_add(F::total_size()) + .ok_or(Error::OutOfBounds)?; + if address < base || address >= end { + return Err(Error::OutOfBounds); + } + if (address - base) % F::page_size() != 0 { + return Err(Error::Misaligned); + } + Ok(Self(address, PhantomData)) + } + + pub fn from_page_index(page_index: usize) -> Result { + if page_index >= F::page_count() { + return Err(Error::InvalidPage); + } + Ok(Self( + F::flash_base() + page_index * F::page_size(), + PhantomData, + )) + } + + pub fn as_usize(self) -> usize { + self.0 + } + + pub fn page_index(self) -> usize { + (self.0 - F::flash_base()) / F::page_size() + } + + pub fn next(self) -> Result { + Self::from_page_index(self.page_index() + 1) + } + + pub fn prev(self) -> Result { + let idx = self.page_index().checked_sub(1).ok_or(Error::InvalidPage)?; + Self::from_page_index(idx) + } + + pub fn add_pages(self, n: usize) -> Result { + let idx = self.page_index().checked_add(n).ok_or(Error::InvalidPage)?; + Self::from_page_index(idx) + } + + pub fn sub_pages(self, n: usize) -> Result { + let idx = self.page_index().checked_sub(n).ok_or(Error::InvalidPage)?; + Self::from_page_index(idx) + } + + /// Iterator over `count` consecutive pages starting at `self`. + /// + /// The whole range is validated up front: if any page in `[self, + /// self + count)` would fall outside flash, this returns + /// `Err(InvalidPage)` and no iteration happens. Once you have the + /// `PageIter`, iterating it is infallible. + pub fn iter_pages(self, count: usize) -> Result> { + if count > 0 { + // last page of the range must be in flash + self.add_pages(count - 1)?; + } + Ok(PageIter { + next: self, + remaining: count, + }) + } +} + +/// Iterator yielded by [`FlashPageStart::iter_pages`]. The underlying range +/// is pre-validated, so this iterator never produces invalid pages. +pub struct PageIter { + next: FlashPageStart, + remaining: usize, +} + +impl Iterator for PageIter { + type Item = FlashPageStart; + fn next(&mut self) -> Option { + if self.remaining == 0 { + return None; + } + let current = self.next; + self.remaining -= 1; + if self.remaining > 0 { + // pre-validated: the next page exists. + self.next = current.next().expect("PageIter pre-validates the range"); + } + Some(current) + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for PageIter { + fn len(&self) -> usize { + self.remaining + } +} + +impl FlashPageStart { + /// Distance in *pages* from `other` to `self` (i.e. `self - other`). + /// Returns `Err(InvalidPage)` if `other > self`. + pub fn pages_from(self, other: Self) -> Result { + self.page_index() + .checked_sub(other.page_index()) + .ok_or(Error::InvalidPage) + } +} + +impl Clone for FlashPageStart { + fn clone(&self) -> Self { + *self + } +} +impl Copy for FlashPageStart {} +impl PartialEq for FlashPageStart { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} +impl Eq for FlashPageStart {} +impl PartialOrd for FlashPageStart { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for FlashPageStart { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.cmp(&other.0) + } +} +impl core::fmt::Debug for FlashPageStart { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "FlashPageStart({:#x})", self.0) + } +} +forward_usize_traits!(FlashPageStart); diff --git a/machine/api/src/lib.rs b/machine/api/src/lib.rs index 7f763ca..a86f799 100644 --- a/machine/api/src/lib.rs +++ b/machine/api/src/lib.rs @@ -2,6 +2,8 @@ use core::fmt; use core::fmt::Display; + +pub mod flash_addr; pub mod mem; pub mod stack; diff --git a/machine/cortex-m/Cargo.toml b/machine/cortex-m/Cargo.toml index 5871c24..bcde991 100644 --- a/machine/cortex-m/Cargo.toml +++ b/machine/cortex-m/Cargo.toml @@ -21,6 +21,7 @@ anyhow = "1.0.99" serde_json = "1.0.145" quote = "1.0.26" syn = { version = "2.0.36", features = ["full"] } +regex = "1.11" [features] panic-exit = [] diff --git a/machine/cortex-m/build.rs b/machine/cortex-m/build.rs index c33f61c..5f6e88e 100644 --- a/machine/cortex-m/build.rs +++ b/machine/cortex-m/build.rs @@ -55,7 +55,7 @@ fn check_cortex_m() -> bool { /// This function scans all environment variables and forwards any that start /// with "OSIRIS_" to the CMake build system. Boolean-like values are normalized: /// - "0", "false", "off" -> "0" -/// - "1", "true", "on" -> "1" +/// - "1", "true", "on" -> "1" /// - Other values are passed through unchanged /// /// # Arguments @@ -176,12 +176,52 @@ fn forward_fpu_config(config: &mut Config) -> Result<()> { /// - Header file cannot be found or parsed /// - Binding generation fails /// - Output file cannot be written -fn generate_bindings(out: &Path, hal: &Path) -> Result<()> { - let bindgen = bindgen::Builder::default() - .header(hal.join("interface").join("export.h").to_str().unwrap()) +#[derive(Debug)] +struct MacroFilter { + patterns: Vec, +} + +impl bindgen::callbacks::ParseCallbacks for MacroFilter { + fn will_parse_macro(&self, name: &str) -> bindgen::callbacks::MacroParsingBehavior { + if self.patterns.iter().any(|r| r.is_match(name)) { + bindgen::callbacks::MacroParsingBehavior::Default + } else { + bindgen::callbacks::MacroParsingBehavior::Ignore + } + } +} + +fn generate_bindings(out: &Path, hal: &Path, soc: &[(&str, &str)]) -> Result<()> { + let header = hal.join("interface").join("export.h"); + let src = std::fs::read_to_string(&header)?; + + let mut patterns = Vec::new(); + let mut builder = bindgen::Builder::default() + .header(header.to_str().unwrap()) + .clang_arg(format!("-I{}", hal.join("hal").display())) + .clang_arg(format!("-I{}", hal.join("device").display())) + .clang_arg("-Icmsis") + .allowlist_file(".*export\\.h") + .clang_macro_fallback() .use_core() - .wrap_unsafe_ops(true) - .generate()?; + .wrap_unsafe_ops(true); + + for (vendor, name) in soc { + if *vendor == "st" { + builder = builder.clang_arg(format!("-D{}xx", name.to_uppercase())); + } + builder = builder.clang_arg(format!("-D{}", name.to_uppercase())); + } + + for line in src.lines() { + if let Some(pat) = line.trim_start().strip_prefix("// bindgen-export:") { + let pat = pat.trim(); + builder = builder.allowlist_var(pat); + patterns.push(regex::Regex::new(&format!("^{pat}$"))?); + } + } + builder = builder.parse_callbacks(Box::new(MacroFilter { patterns })); + let bindgen = builder.generate()?; bindgen.write_to_file(out.join("bindings.rs"))?; @@ -285,7 +325,7 @@ mod vector_table { /// This function orchestrates the entire build process: /// /// 1. **Environment Setup**: Reads configuration from environment variables -/// 2. **Binding Generation**: Creates Rust FFI bindings from C headers +/// 2. **Binding Generation**: Creates Rust FFI bindings from C headers /// 3. **Core Configuration**: Sets up ARM core-specific cfg flags /// 4. **Host Detection**: Skips hardware builds when targeting host /// 5. **HAL Compilation**: Builds hardware abstraction layer via CMake @@ -322,11 +362,13 @@ fn main() { panic!("Failed to generate device tree scripts: {e}"); } - for (vendor, name) in hal_builder::dt::soc(&dt) { + let soc = hal_builder::dt::soc(&dt); + + for &(vendor, name) in &soc { let hal = Path::new(vendor).join(name); if hal.exists() { - fail_on_error(generate_bindings(&out, &hal)); + fail_on_error(generate_bindings(&out, &hal, &soc)); let vector_code = vector_table::generate(); if let Err(e) = fs::write(PathBuf::from(&out).join("vector_table.rs"), vector_code) { @@ -343,7 +385,7 @@ fn main() { libhal_config.define("OUT_DIR", &out); libhal_config.cflag(format!("-I{}", out.display())); - for (vendor, name) in hal_builder::dt::soc(&dt) { + for &(vendor, name) in &soc { if vendor == "st" { libhal_config.cflag(format!("-D{}xx", name.to_uppercase())); } diff --git a/machine/cortex-m/src/native.rs b/machine/cortex-m/src/native.rs index bd10c1e..a124420 100644 --- a/machine/cortex-m/src/native.rs +++ b/machine/cortex-m/src/native.rs @@ -6,6 +6,7 @@ pub mod asm; pub mod can; pub mod debug; pub mod excep; +pub mod flash; pub mod i2c; pub mod panic; pub mod sched; diff --git a/machine/cortex-m/src/native/flash.rs b/machine/cortex-m/src/native/flash.rs new file mode 100644 index 0000000..87d907b --- /dev/null +++ b/machine/cortex-m/src/native/flash.rs @@ -0,0 +1,296 @@ +use super::bindings; +use super::device_tree; + +pub use hal_api::flash_addr::{Error, Result}; + +// --------------------------------------------------------------------------- +// Chip-level queries +// --------------------------------------------------------------------------- + +pub fn flash_base() -> usize { + device_tree::FLASH_BASE +} + +pub fn total_size() -> usize { + unsafe { bindings::flash_size() as usize } +} + +pub fn is_dual_bank() -> bool { + unsafe { bindings::flash_is_dual_bank() } +} + +pub fn page_size() -> usize { + unsafe { bindings::flash_page_size() as usize } +} + +pub fn page_count() -> usize { + unsafe { bindings::flash_page_count() as usize } +} + +/// Smallest unit the chip will program in one shot. STM32L4/L4+/G4 program +/// at doubleword granularity (8 bytes). Other STM32 families differ +/// (F4: 1/2/4/8, H7: 32). +pub fn write_unit_bytes() -> usize { + core::mem::size_of::() +} + +// --------------------------------------------------------------------------- +// Flash trait impl + type aliases +// --------------------------------------------------------------------------- + +/// Zero-sized backend marker that plugs the chip queries into the shared +/// newtype constructors in `hal_api::flash`. +pub struct ArmFlash; + +impl hal_api::flash_addr::Flash for ArmFlash { + fn flash_base() -> usize { + flash_base() + } + fn total_size() -> usize { + total_size() + } + fn page_size() -> usize { + page_size() + } + fn page_count() -> usize { + page_count() + } + fn write_unit_bytes() -> usize { + write_unit_bytes() + } +} + +pub type FlashAddress = hal_api::flash_addr::FlashAddress; +pub type FlashOffset = hal_api::flash_addr::FlashOffset; +pub type FlashPageStart = hal_api::flash_addr::FlashPageStart; + +// --------------------------------------------------------------------------- +// Whole-chip operations +// --------------------------------------------------------------------------- + +fn from_c_rc(rc: u32) -> Result<()> { + if rc == bindings::FLASH_OK { + return Ok(()); + } + + if rc & bindings::ERR_FLASH_TIMEOUT != 0 { + return Err(Error::TimedOut); + } + if rc & bindings::ERR_FLASH_BUSY != 0 { + return Err(Error::Busy); + } + if rc & bindings::ERR_FLASH_DOUBLE_UNLOCK != 0 { + return Err(Error::DoubleUnlock); + } + if rc & bindings::ERR_FLASH_NOT_UNLOCKED != 0 { + return Err(Error::Locked); + } + if rc & bindings::ERR_FLASH_INVALID_PAGE != 0 { + return Err(Error::InvalidPage); + } + if rc & bindings::ERR_FLASH_INVALID_BANK != 0 { + return Err(Error::InvalidPage); + } + if rc & bindings::ERR_FLASH_ILLEGAL != 0 { + return Err(Error::InvalidArgument); + } + + if rc & bindings::HAL_FLASH_ERROR_PROG != 0 { + return Err(Error::NotErased); + } + if rc & (bindings::HAL_FLASH_ERROR_WRP | bindings::HAL_FLASH_ERROR_RD) != 0 { + return Err(Error::Protected); + } + if rc & bindings::HAL_FLASH_ERROR_ECCD != 0 { + return Err(Error::EccError); + } + if rc + & (bindings::HAL_FLASH_ERROR_PGA + | bindings::HAL_FLASH_ERROR_PGS + | bindings::HAL_FLASH_ERROR_SIZ + | bindings::HAL_FLASH_ERROR_MIS + | bindings::HAL_FLASH_ERROR_FAST + | bindings::HAL_FLASH_ERROR_OPTV) + != 0 + { + return Err(Error::ProgrammingFailed); + } + + Err(Error::Io) +} + +fn with_unlock Result<()>>(f: F, lock_wait_ms: u32) -> Result<()> { + let unlock_rc = unsafe { bindings::flash_unlock() }; + from_c_rc(unlock_rc)?; + let result = f(); + let lock_rc = unsafe { bindings::flash_lock() }; + if lock_rc != bindings::FLASH_OK { + // chip may still be busy from the op — wait briefly and retry once. + let _ = unsafe { bindings::flash_wait_for_last_operation(lock_wait_ms) }; + let _ = unsafe { bindings::flash_lock() }; + } + result +} + +/// Erase the page that starts at `page_start`. The bank is derived +/// automatically inside the C primitive. +pub fn erase_page(page_start: FlashPageStart, timeout_ms: u32, lock_wait_ms: u32) -> Result<()> { + with_unlock( + || { + let rc = unsafe { bindings::flash_erase(page_start.page_index() as u32, timeout_ms) }; + from_c_rc(rc) + }, + lock_wait_ms, + ) +} + +/// Program 64-bit doublewords starting at `start` (must be 8-byte-aligned). +/// The target range must already be erased. +/// +/// Accepts any type that converts into `FlashAddress` — pass a `FlashAddress`, +/// `FlashPageStart`, or `FlashOffset`. +pub fn program>( + start: A, + data: &[u64], + timeout_ms: u32, + lock_wait_ms: u32, +) -> Result<()> { + if data.is_empty() { + return Ok(()); + } + let start: FlashAddress = start.into(); + if start.as_usize() & 0x7 != 0 { + return Err(Error::Misaligned); + } + let bytes = data.len() * core::mem::size_of::(); + let end = start + .as_usize() + .checked_add(bytes) + .ok_or(Error::OutOfBounds)?; + let flash_end = flash_base() + .checked_add(total_size()) + .ok_or(Error::OutOfBounds)?; + if end > flash_end { + return Err(Error::OutOfBounds); + } + + with_unlock( + || { + let rc = unsafe { + bindings::flash_program( + start.as_usize() as u32, + data.as_ptr(), + data.len() as u32, + timeout_ms, + ) + }; + from_c_rc(rc) + }, + lock_wait_ms, + ) +} + +/// Read `buf.len()` bytes starting at `start`. Flash is memory-mapped, so +/// this is a volatile memcpy. Accepts any `Into`. +pub fn read>(start: A, buf: &mut [u8]) -> Result<()> { + if buf.is_empty() { + return Ok(()); + } + let start: FlashAddress = start.into(); + let end = start + .as_usize() + .checked_add(buf.len()) + .ok_or(Error::OutOfBounds)?; + let flash_end = flash_base() + .checked_add(total_size()) + .ok_or(Error::OutOfBounds)?; + if end > flash_end { + return Err(Error::OutOfBounds); + } + unsafe { + let src = start.as_usize() as *const u8; + for i in 0..buf.len() { + buf[i] = core::ptr::read_volatile(src.add(i)); + } + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// DT-driven partition handle +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +pub struct Region(&'static device_tree::FlashPartitionRegistryEntry); + +impl Region { + /// Validate that the DT-derived entry fits within the chip's actual flash size. + fn from_entry(entry: &'static device_tree::FlashPartitionRegistryEntry) -> Result { + let offset_end = entry + .offset + .checked_add(entry.len) + .ok_or(Error::OutOfBounds)?; + if offset_end > total_size() { + return Err(Error::OutOfBounds); + } + flash_base() + .checked_add(entry.offset) + .and_then(|s| s.checked_add(entry.len)) + .ok_or(Error::OutOfBounds)?; + Ok(Self(entry)) + } + + pub fn get(compatible: &str, ordinal: usize) -> Result { + let entry = device_tree::flash_partition_by_compatible(compatible, ordinal) + .ok_or(Error::NotFound)?; + Self::from_entry(entry) + } + + pub fn get_by_label(label: &str) -> Result { + let entry = device_tree::flash_partition_by_label(label).ok_or(Error::NotFound)?; + Self::from_entry(entry) + } + + /// Find the partition that contains `addr`. Returns `Err(NotFound)` if + /// `addr` doesn't fall in any declared partition. Accepts any + /// `Into`. + pub fn get_by_address(addr: impl Into) -> Result { + let addr: FlashAddress = addr.into(); + let (entry, _) = + device_tree::flash_partition_by_address(addr.as_usize()).ok_or(Error::NotFound)?; + Self::from_entry(entry) + } + + pub fn label(&self) -> &'static str { + self.0.label + } + + pub fn compatible(&self) -> &'static str { + self.0.compatible + } + + pub fn read_only(&self) -> bool { + self.0.read_only + } + + /// Absolute flash address at which this partition starts. + pub fn start_address(&self) -> FlashAddress { + // safe by DT contract: partitions live inside the parent flash node's + // reg, so flash_base + offset is always in flash bounds. + FlashAddress::new(device_tree::FLASH_BASE + self.0.offset) + .expect("DT partition outside flash bounds") + } + + /// Byte offset of this partition from `flash_base()`. + pub fn flash_offset(&self) -> FlashOffset { + FlashOffset::new(self.0.offset).expect("DT partition offset >= total_size") + } + + pub fn len(&self) -> usize { + self.0.len + } + + pub fn is_empty(&self) -> bool { + self.0.len == 0 + } +} diff --git a/machine/cortex-m/src/stub.rs b/machine/cortex-m/src/stub.rs index d37b00c..e8ea4ea 100644 --- a/machine/cortex-m/src/stub.rs +++ b/machine/cortex-m/src/stub.rs @@ -4,6 +4,7 @@ pub use hal_api::*; pub mod asm; pub mod can; pub mod device_tree; +pub mod flash; pub mod i2c; pub mod sched; pub mod spi; diff --git a/machine/cortex-m/src/stub/flash.rs b/machine/cortex-m/src/stub/flash.rs new file mode 100644 index 0000000..6ab55dd --- /dev/null +++ b/machine/cortex-m/src/stub/flash.rs @@ -0,0 +1,103 @@ +pub use hal_api::flash_addr::{Error, Result}; + +pub fn flash_base() -> usize { + 0x0800_0000 +} +pub fn total_size() -> usize { + 0 +} +pub fn is_dual_bank() -> bool { + false +} +pub fn page_size() -> usize { + 8 * 1024 +} +pub fn page_count() -> usize { + 0 +} +pub fn write_unit_bytes() -> usize { + core::mem::size_of::() +} + +pub struct TestingFlash; + +impl hal_api::flash_addr::Flash for TestingFlash { + fn flash_base() -> usize { + flash_base() + } + fn total_size() -> usize { + total_size() + } + fn page_size() -> usize { + page_size() + } + fn page_count() -> usize { + page_count() + } + fn write_unit_bytes() -> usize { + write_unit_bytes() + } +} + +pub type FlashAddress = hal_api::flash_addr::FlashAddress; +pub type FlashOffset = hal_api::flash_addr::FlashOffset; +pub type FlashPageStart = hal_api::flash_addr::FlashPageStart; + +pub fn erase_page( + _page_start: FlashPageStart, + _timeout_ms: u32, + _lock_wait_ms: u32, +) -> Result<()> { + Err(Error::Io) +} + +pub fn program>( + _start: A, + _data: &[u64], + _timeout_ms: u32, + _lock_wait_ms: u32, +) -> Result<()> { + Err(Error::Io) +} + +pub fn read>(_start: A, _buf: &mut [u8]) -> Result<()> { + Err(Error::Io) +} + +#[derive(Clone, Copy)] +pub struct Region; + +impl Region { + pub fn get(_compatible: &str, _ordinal: usize) -> Result { + Err(Error::NotFound) + } + pub fn get_by_label(_label: &str) -> Result { + Err(Error::NotFound) + } + pub fn get_by_address(_addr: impl Into) -> Result { + Err(Error::NotFound) + } + pub fn label(&self) -> &'static str { + "" + } + pub fn compatible(&self) -> &'static str { + "" + } + pub fn read_only(&self) -> bool { + false + } + pub fn start_address(&self) -> FlashAddress { + // host stub: total_size() is 0, so no valid FlashAddress exists. + // Production code shouldn't hit this path; panic if it does. + unreachable!("flash region accessed in host testing stub") + } + pub fn flash_offset(&self) -> FlashOffset { + unreachable!("flash region accessed in host testing stub") + } + pub fn len(&self) -> usize { + 0 + } + pub fn is_empty(&self) -> bool { + true + } +} diff --git a/machine/cortex-m/st/stm32l4/interface/export.h b/machine/cortex-m/st/stm32l4/interface/export.h index f1f8532..d82fb30 100644 --- a/machine/cortex-m/st/stm32l4/interface/export.h +++ b/machine/cortex-m/st/stm32l4/interface/export.h @@ -1,4 +1,36 @@ #pragma once +#include "stm32l4xx_hal_def.h" +#include "stm32l4xx_hal_flash.h" +#include +#include + +// bindgen-export: HAL_FLASH_ERROR_.* +// bindgen-export: FLASH_FLAG_.* + +#define __FLASH_ERRORS \ + (FLASH_FLAG_SR_ERRORS | FLASH_FLAG_ALL_ERRORS | HAL_OK | HAL_BUSY | \ + HAL_TIMEOUT) + +#define FLASH_OK 0 +_Static_assert((FLASH_OK & __FLASH_ERRORS) == 0); +_Static_assert(FLASH_OK == FLASH_ERROR_NONE); +#define ERR_FLASH_DOUBLE_UNLOCK (1 << 10) +_Static_assert((ERR_FLASH_DOUBLE_UNLOCK & __FLASH_ERRORS) == 0); +#define ERR_FLASH_NOT_UNLOCKED (1 << 11) +_Static_assert((ERR_FLASH_NOT_UNLOCKED & __FLASH_ERRORS) == 0); +#define ERR_FLASH_BUSY (1 << 12) +_Static_assert((ERR_FLASH_BUSY & __FLASH_ERRORS) == 0); +#define ERR_FLASH_ILLEGAL (1 << 13) +_Static_assert((ERR_FLASH_ILLEGAL & __FLASH_ERRORS) == 0); +#define ERR_FLASH_INVALID_PAGE (1 << 16) +_Static_assert((ERR_FLASH_INVALID_PAGE & __FLASH_ERRORS) == 0); +#define ERR_FLASH_INVALID_BANK (1 << 18) +_Static_assert((ERR_FLASH_INVALID_BANK & __FLASH_ERRORS) == 0); +#define ERR_FLASH_TIMEOUT (1 << 19) +_Static_assert((ERR_FLASH_TIMEOUT & __FLASH_ERRORS) == 0); + +// bindgen-export: FLASH_OK +// bindgen-export: ERR_FLASH_.* #include @@ -219,6 +251,53 @@ long dwt_read(void); float dwt_read_ns(void); float dwt_cycles_to_ns(long cycles); +// flash.c + +// True if the flash controller's BSY bit is set. +bool flash_is_busy(void); + +// Total flash size in bytes. +int flash_size(void); + +// True if the device is configured for dual-bank mode (affects page size +// and how page numbers map to banks). +bool flash_is_dual_bank(void); + +// Page size in bytes: 4 KiB in dual-bank mode, 8 KiB in single-bank mode. +int flash_page_size(void); + +// Total number of flash pages. +int flash_page_count(void); + +// Block until the last flash operation completes, or until timeout_ms +// milliseconds elapse. +uint32_t flash_wait_for_last_operation(uint32_t timeout_ms); + +// Unlock the flash control register for erase/program. Fails with +// ERR_FLASH_BUSY if an operation is in flight, or ERR_FLASH_DOUBLE_UNLOCK if +// the flash is already unlocked. +uint32_t flash_unlock(void); + +// Re-lock the flash control register. Caller must ensure no operation is in +// flight; returns ERR_FLASH_BUSY otherwise. +uint32_t flash_lock(void); + +// Erase the flash page identified by `page_index` (a page number, not an +// address). Bank is derived automatically in dual-bank mode. timeout_ms +// bounds the wait for the erase to finish. Returns FLASH_OK, ERR_FLASH_BUSY, +// ERR_FLASH_INVALID_PAGE, or a HAL_FLASH_ERROR_* code. +uint32_t flash_erase(uint32_t page_index, uint32_t timeout_ms); + +// Program `length` doublewords (uint64_t) starting at `start_address`. +// `data` must point to at least `length` elements. Flash must be unlocked +// and the target region must already be erased. +// +// timeout_ms is the total budget for the whole sequence, not per doubleword; +// each iteration is given the remaining time. Returns FLASH_OK, +// ERR_FLASH_BUSY, ERR_FLASH_TIMEOUT, or a HAL_FLASH_ERROR_* code. +uint32_t flash_program(uint32_t start_address, const uint64_t *data, + uint32_t length, uint32_t timeout_ms); + // clock.c void SystemClock_Config(void); diff --git a/machine/cortex-m/st/stm32l4/interface/flash.c b/machine/cortex-m/st/stm32l4/interface/flash.c new file mode 100644 index 0000000..15a9f28 --- /dev/null +++ b/machine/cortex-m/st/stm32l4/interface/flash.c @@ -0,0 +1,199 @@ +#include "export.h" +#include "lib.h" +#include "stm32l4xx.h" +#include "stm32l4xx_hal.h" +#include "stm32l4xx_hal_cortex.h" +#include "stm32l4xx_hal_def.h" +#include "stm32l4xx_hal_flash.h" +#include "stm32l4xx_hal_flash_ex.h" +#include "stm32l4xx_hal_gpio.h" +#include "stm32l4xx_hal_rcc.h" +#include "stm32l4xx_hal_rcc_ex.h" +#include "stm32l4xx_ll_utils.h" +#include +#include +#include + +#define ONE_MEGABYTE (1024 * 1024) + +// Translate the result of FLASH_WaitForLastOperation / HAL_FLASH_Program into +// an osiris flash error code. Masks HAL_FLASH_GetError() so non-error status +// bits (PEMPTY etc.) don't propagate. +static uint32_t status_to_err(HAL_StatusTypeDef status) { + if (status == HAL_OK) { + return FLASH_OK; + } + if (status == HAL_TIMEOUT) { + return ERR_FLASH_TIMEOUT; + } + uint32_t err = HAL_FLASH_GetError() & FLASH_FLAG_ALL_ERRORS; + return err ? err : FLASH_OK; +} + +// Whether the flash busy bit is set - true if busy, false otherwise +bool flash_is_busy(void) { return __HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY) != 0U; } + +int flash_size(void) { + uint32_t flash_size_kb = LL_GetFlashSize(); + return flash_size_kb * 1024; +} + +// Check if dual-bank mode is enabled +bool flash_is_dual_bank(void) { + // Manual: "For 1-Mbyte and 512-Kbyte Flash memory devices, do not care about + // DBANK" + uint32_t optr_mask = FLASH_OPTR_DB1M; +#if defined(FLASH_OPTR_DUALBANK) + // L47x..L4A6 + if (flash_size() > ONE_MEGABYTE) { + optr_mask = FLASH_OPTR_DUALBANK; + } +#elif defined(FLASH_OPTR_DBANK) + // L4+ + if (flash_size() > ONE_MEGABYTE) { + optr_mask = FLASH_OPTR_DBANK; + } +#endif + // For devices <1MB, just read DB1M + return (READ_BIT(FLASH->OPTR, optr_mask) != 0U); +} + +int flash_page_size() { + if (flash_is_dual_bank()) { + return 4 * 1024; // 4KB page size in dual-bank mode + } else { + return 8 * 1024; // 8KB page size in single-bank mode + } +} + +int flash_page_count() { return flash_size() / flash_page_size(); } + +// Block until the most recently issued flash operation finishes, or until +// timeout_ms milliseconds have elapsed (measured against HAL_GetTick). +// Returns the underlying HAL status (HAL_OK / HAL_TIMEOUT / HAL_ERROR). +uint32_t flash_wait_for_last_operation(uint32_t timeout_ms) { + return FLASH_WaitForLastOperation(timeout_ms); +} + +// Unlock the flash for writing. +uint32_t flash_unlock(void) { + if (flash_is_busy()) { + return ERR_FLASH_BUSY; + } + // Check if the flash is already unlocked. + // Note that the HAL unlock function also checks this, + // but we want to prevent usage of the flash by multiple call sites at the + // same time, so we prefer to have one of them fail. + if (READ_BIT(FLASH->CR, FLASH_CR_LOCK) == 0U) { + return ERR_FLASH_DOUBLE_UNLOCK; + } + + HAL_StatusTypeDef status = HAL_FLASH_Unlock(); + if (status != HAL_OK) { + return ERR_FLASH_NOT_UNLOCKED; + } + + return HAL_OK; +} + +// Note: must wait for flash to not be busy before calling this function. +uint32_t flash_lock(void) { + if (flash_is_busy()) { + return ERR_FLASH_BUSY; + } + // Note: always returns HAL_OK. + return HAL_FLASH_Lock(); +} + +// Erase the flash page identified by `page_index` (a page number, not an +// address). The bank is derived automatically from the index. +uint32_t flash_erase(uint32_t page_index, uint32_t timeout_ms) { + if (flash_is_busy()) { + return ERR_FLASH_BUSY; + } + if (page_index >= flash_page_count()) { + return ERR_FLASH_INVALID_PAGE; + } + + // Check which bank we are on + uint32_t bank = FLASH_BANK_1; +#ifdef FLASH_BANK_2 + const uint32_t half_page_count = flash_page_count() / 2; + if (flash_is_dual_bank() && page_index >= half_page_count) { + bank = FLASH_BANK_2; + page_index -= half_page_count; + } +#endif + + // HAL's low-level FLASH_PageErase sets FLASH_CR.PER/PNB/BKER and starts the + // erase but doesn't clear them on exit — the public wrapper (which we + // bypass) does. Leaving PER set makes any subsequent HAL_FLASH_Program + // raise PGS because the hardware sees PER|PG simultaneously. + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); + FLASH_PageErase(page_index, bank); + HAL_StatusTypeDef status = FLASH_WaitForLastOperation(timeout_ms); + CLEAR_BIT(FLASH->CR, FLASH_CR_PER); + + return status_to_err(status); +} + +// Program `length` doublewords (uint64_t) at `flash_address` (an absolute +// flash address; must be 8-byte-aligned). The target range must already be +// erased. +uint32_t flash_program(uint32_t flash_address, const uint64_t *data, + uint32_t length, uint32_t timeout_ms) { + uint32_t total_bytes = length * (uint32_t)sizeof(uint64_t); + uint32_t end_address = flash_address + total_bytes; + if (flash_address < FLASH_BASE || end_address < flash_address || + end_address > FLASH_BASE + (uint32_t)flash_size()) { + return ERR_FLASH_ILLEGAL; + } + if ((flash_address & 0x7U) != 0U) { + return ERR_FLASH_ILLEGAL; + } + + if (flash_is_busy()) { + return ERR_FLASH_BUSY; + } + + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); + + uint32_t tickstart = HAL_GetTick(); + + for (uint32_t dw_written = 0; dw_written < length; dw_written++) { + uint32_t elapsed = HAL_GetTick() - tickstart; + if (elapsed >= timeout_ms) { + return ERR_FLASH_TIMEOUT; + } + // disable interrupts, as flash_program must write two 32-bit words without + // interruptions. Later restore exact state + + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + HAL_StatusTypeDef status = HAL_FLASH_Program( + FLASH_TYPEPROGRAM_DOUBLEWORD, + flash_address + (dw_written * sizeof(uint64_t)), data[dw_written]); + __set_PRIMASK(primask); + + if (status != HAL_OK) { + uint32_t err = status_to_err(status); + if (err != FLASH_OK) { + return err; + } + } + + elapsed = HAL_GetTick() - tickstart; + if (elapsed >= timeout_ms) { + return ERR_FLASH_TIMEOUT; + } + status = FLASH_WaitForLastOperation(timeout_ms - elapsed); + if (status != HAL_OK) { + uint32_t err = status_to_err(status); + if (err != FLASH_OK) { + return err; + } + } + } + + return FLASH_OK; +} diff --git a/src/drivers.rs b/src/drivers.rs index fe3e561..da2147c 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -1,4 +1,5 @@ pub mod can; +pub mod flash; pub mod i2c; pub mod spi; diff --git a/src/drivers/flash.rs b/src/drivers/flash.rs new file mode 100644 index 0000000..72e488d --- /dev/null +++ b/src/drivers/flash.rs @@ -0,0 +1,320 @@ +use crate::error::Result; +use crate::hal; + +pub use hal::flash::{FlashAddress, FlashOffset, FlashPageStart}; + +/// Re-exports of the HAL-level flash primitives. Their `Result` type carries +/// the variant-rich `hal::flash::Error` rather than the kernel's `PosixError`, +/// for callers that need to discriminate between `NotErased`, `Protected`, +/// `EccError`, etc. +pub mod raw { + use crate::hal; + pub use hal::flash::{ + Error, FlashAddress, FlashOffset, FlashPageStart, Result, erase_page, flash_base, + is_dual_bank, page_count, page_size, program, read, total_size, write_unit_bytes, + }; +} + +#[derive(Clone, Copy, Debug)] +pub struct Config { + /// Timeout for a single page erase, in milliseconds. + pub erase_timeout_ms: u32, + /// Total timeout for a program operation across all doublewords, in milliseconds. + pub program_timeout_ms: u32, + /// Timeout for the post-op chip-busy wait if `flash_lock()` finds the chip + /// still busy. Recovery-path only — operations don't normally hit this. + pub lock_timeout_ms: u32, +} + +impl Default for Config { + fn default() -> Self { + Self { + erase_timeout_ms: 5000, + program_timeout_ms: 1000, + lock_timeout_ms: 100, + } + } +} + +#[derive(Clone, Copy)] +pub struct Region { + desc: hal::flash::Region, + config: Config, +} + +impl Region { + pub fn open(compatible: &str, ordinal: usize, config: Config) -> Result { + Ok(Self { + desc: hal::flash::Region::get(compatible, ordinal)?, + config, + }) + } + + pub fn open_by_label(label: &str, config: Config) -> Result { + Ok(Self { + desc: hal::flash::Region::get_by_label(label)?, + config, + }) + } + + /// Find the partition containing `addr` and open it. The caller already + /// has the absolute address, so this returns only the `Region` — pass + /// `addr` directly to `read`/`program`/`write`. + /// + /// Returns `Err(NotFound)` if `addr` doesn't fall in any declared partition. + pub fn open_by_address(addr: impl Into, config: Config) -> Result { + Ok(Self { + desc: hal::flash::Region::get_by_address(addr)?, + config, + }) + } + + pub fn config(&self) -> Config { + self.config + } + + pub fn label(&self) -> &'static str { + self.desc.label() + } + + pub fn compatible(&self) -> &'static str { + self.desc.compatible() + } + + pub fn read_only(&self) -> bool { + self.desc.read_only() + } + + /// Absolute flash address at which this partition starts. + pub fn start_address(&self) -> FlashAddress { + self.desc.start_address() + } + + /// Byte offset of this partition from `flash_base()`. + pub fn flash_offset(&self) -> FlashOffset { + self.desc.flash_offset() + } + + pub fn len(&self) -> usize { + self.desc.len() + } + + pub fn is_empty(&self) -> bool { + self.desc.is_empty() + } + + pub fn page_size(&self) -> usize { + hal::flash::page_size() + } + + pub fn page_count(&self) -> usize { + let ps = self.page_size(); + if ps == 0 { 0 } else { self.len() / ps } + } + + pub fn write_unit_bytes(&self) -> usize { + hal::flash::write_unit_bytes() + } + + /// Read `buf.len()` bytes starting at `addr`. Accepts any + /// `Into` (so a `FlashOffset` or `FlashPageStart` works + /// too). Errors with `OutOfBounds` if `[addr, addr + buf.len())` leaves + /// this partition. + pub fn read(&self, addr: impl Into, buf: &mut [u8]) -> Result<()> { + if buf.is_empty() { + return Ok(()); + } + let addr: FlashAddress = addr.into(); + self.check_range(addr, buf.len())?; + hal::flash::read(addr, buf)?; + Ok(()) + } + + /// Erase `len_bytes` bytes starting at `start`. `len_bytes` must be a + /// multiple of the chip's current page size, and `start` must be + /// page-aligned for that same page size. Both are revalidated here + /// against `hal::flash::page_size()` (which can change at runtime on + /// parts that support bank reconfiguration), so a `FlashPageStart` + /// validated at an earlier page size will be rejected. + /// + /// Returns `Err(ReadOnly)` if the DT marks this partition read-only. + pub fn erase(&self, start: FlashPageStart, len_bytes: usize) -> Result<()> { + if len_bytes == 0 { + return Ok(()); + } + if self.read_only() { + return Err(kerr!(EROFS, "flash partition is read-only; erase refused")); + } + let ps = self.page_size(); + if ps == 0 { + return Err(kerr!(EIO, "flash HAL reported page_size == 0")); + } + if start.as_usize() % ps != 0 || len_bytes % ps != 0 { + return Err(kerr!( + EINVAL, + "flash erase: start or length not page-aligned" + )); + } + self.check_range(start.into(), len_bytes)?; + for page in start.iter_pages(len_bytes / ps)? { + hal::flash::erase_page( + page, + self.config.erase_timeout_ms, + self.config.lock_timeout_ms, + )?; + } + Ok(()) + } + + pub fn erase_all(&self) -> Result<()> { + let start = FlashPageStart::try_from(self.start_address())?; + self.erase(start, self.len()) + } + + /// Program bytes starting at `addr`. `addr` and `data.len()` must both be + /// multiples of `hal::flash::write_unit_bytes()`. The target range must + /// already be erased; flash reads as `0xFF` after erase. Returns + /// `Err(ReadOnly)` if the DT marks this partition read-only. + pub fn program(&self, addr: impl Into, data: &[u8]) -> Result<()> { + if data.is_empty() { + return Ok(()); + } + if self.read_only() { + return Err(kerr!( + EROFS, + "flash partition is read-only; program refused" + )); + } + let addr: FlashAddress = addr.into(); + let unit = self.write_unit_bytes(); + if unit != core::mem::size_of::() { + // Repacking below hardcodes 8-byte chunks; a non-u64 HAL would + // out-of-bounds index. Generalize before lifting this check. + debug_assert_eq!(unit, core::mem::size_of::()); + return Err(kerr!( + EIO, + "flash write_unit_bytes != 8; kernel program() not generalized yet" + )); + } + if addr.as_usize() % unit != 0 || data.len() % unit != 0 { + return Err(kerr!( + EINVAL, + "flash program: address or length not write-unit-aligned" + )); + } + self.check_range(addr, data.len())?; + + const BATCH: usize = 32; + let mut buf = [0u64; BATCH]; + let mut written = 0; + while written < data.len() { + let take = core::cmp::min(BATCH * unit, data.len() - written); + let n = take / unit; + for i in 0..n { + let off = written + i * unit; + buf[i] = u64::from_le_bytes([ + data[off], + data[off + 1], + data[off + 2], + data[off + 3], + data[off + 4], + data[off + 5], + data[off + 6], + data[off + 7], + ]); + } + hal::flash::program( + addr.checked_add(written)?, + &buf[..n], + self.config.program_timeout_ms, + self.config.lock_timeout_ms, + )?; + written += take; + } + Ok(()) + } + + /// Erase any pages overlapping `[addr, addr + data.len())`, then program + /// `data` at `addr` (padding the trailing partial write-unit with `0xFF` + /// if `data.len()` isn't a multiple of `hal::flash::write_unit_bytes()`). + /// Bytes outside `data` *within the erased pages* are left as `0xFF` — + /// this is **not** read-modify-write. Use the explicit `read` → modify → + /// `erase` → `program` sequence if you need to preserve other content in + /// the affected pages. + /// + /// Accepts any `Into`; `addr` must be write-unit-aligned. + pub fn write(&self, addr: impl Into, data: &[u8]) -> Result<()> { + if data.is_empty() { + return Ok(()); + } + if self.read_only() { + return Err(kerr!(EROFS, "flash partition is read-only; write refused")); + } + let addr: FlashAddress = addr.into(); + let unit = self.write_unit_bytes(); + if unit != core::mem::size_of::() { + // Tail buffer below is sized to u64; see program() for the same constraint. + debug_assert_eq!(unit, core::mem::size_of::()); + return Err(kerr!( + EIO, + "flash write_unit_bytes != 8; kernel write() not generalized yet" + )); + } + if addr.as_usize() % unit != 0 { + return Err(kerr!(EINVAL, "flash write: address not write-unit-aligned")); + } + self.check_range(addr, data.len())?; + let ps = self.page_size(); + if ps == 0 { + return Err(kerr!(EIO, "flash HAL reported page_size == 0")); + } + + let addr_u = addr.as_usize(); + let first_page = (addr_u / ps) * ps; + let last_page_end = addr_u + .checked_add(data.len()) + .and_then(|end| end.checked_add(ps - 1)) + .map(|x| (x / ps) * ps) + .ok_or_else(|| { + kerr!( + ERANGE, + "flash write: end-of-write page rounding overflowed usize" + ) + })?; + let erase_len = last_page_end - first_page; + self.erase(FlashPageStart::new(first_page)?, erase_len)?; + + // Program whole write-units directly from `data`. If the tail is a + // partial unit, copy into a stack buffer and pad with 0xFF — that + // matches the post-erase state, so the padded bytes program as blank. + let aligned = (data.len() / unit) * unit; + if aligned > 0 { + self.program(addr, &data[..aligned])?; + } + let remaining = data.len() - aligned; + if remaining > 0 { + let mut tail = [0xFFu8; core::mem::size_of::()]; + tail[..remaining].copy_from_slice(&data[aligned..]); + self.program(addr.checked_add(aligned)?, &tail)?; + } + Ok(()) + } + + fn check_range(&self, addr: FlashAddress, len: usize) -> Result<()> { + let region_start = self.start_address().as_usize(); + let region_end = region_start + .checked_add(self.len()) + .ok_or_else(|| kerr!(ERANGE, "flash region end overflows usize"))?; + let start = addr.as_usize(); + if start < region_start { + return Err(kerr!(ERANGE, "flash access starts before region")); + } + let end = start + .checked_add(len) + .ok_or_else(|| kerr!(ERANGE, "flash access end overflows usize"))?; + if end > region_end { + return Err(kerr!(ERANGE, "flash access extends past region end")); + } + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index c5b7f28..67c1cae 100644 --- a/src/error.rs +++ b/src/error.rs @@ -197,6 +197,24 @@ impl From for Error { } } +impl From for Error { + fn from(e: hal::flash::Error) -> Self { + use hal::flash::Error as F; + let kind = match e { + F::NotFound => PosixError::ENOENT, + F::InvalidArgument | F::Misaligned | F::InvalidPage => PosixError::EINVAL, + F::OutOfBounds => PosixError::ERANGE, + F::Busy | F::Locked => PosixError::EBUSY, + F::DoubleUnlock => PosixError::EALREADY, + F::TimedOut => PosixError::ETIMEDOUT, + F::ReadOnly => PosixError::EROFS, + F::Protected => PosixError::EACCES, + F::NotErased | F::ProgrammingFailed | F::EccError | F::Io => PosixError::EIO, + }; + Self::new(kind) + } +} + impl PartialEq for Error { fn eq(&self, other: &Self) -> bool { self.kind == other.kind diff --git a/src/uapi.rs b/src/uapi.rs index d43576d..8179fda 100644 --- a/src/uapi.rs +++ b/src/uapi.rs @@ -1,4 +1,5 @@ pub mod can; +pub mod flash; pub mod i2c; pub mod print; pub mod sched; diff --git a/src/uapi/flash.rs b/src/uapi/flash.rs new file mode 100644 index 0000000..c0902bc --- /dev/null +++ b/src/uapi/flash.rs @@ -0,0 +1,16 @@ +use crate::error::Result; + +pub use crate::drivers::flash::raw; +pub use crate::drivers::flash::{Config, FlashAddress, FlashOffset, FlashPageStart, Region}; + +pub fn open(compatible: &str, ordinal: usize, config: Config) -> Result { + Region::open(compatible, ordinal, config) +} + +pub fn open_by_label(label: &str, config: Config) -> Result { + Region::open_by_label(label, config) +} + +pub fn open_by_address(addr: impl Into, config: Config) -> Result { + Region::open_by_address(addr, config) +} diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index 8d3d75a..07ee75b 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -3,6 +3,7 @@ use proc_macro2::TokenStream; use quote::quote; mod can; +mod flash; mod i2c; mod spi; @@ -18,6 +19,8 @@ pub fn generate_rust(dt: &DeviceTree) -> String { i2c::emit_query_api(), spi::emit_registry(dt), spi::emit_query_api(), + flash::emit_registry(dt), + flash::emit_query_api(), can::emit_registry(dt), can::emit_query_api(), emit_aliases_module(dt), diff --git a/xtasks/crates/dtgen/src/codegen/flash.rs b/xtasks/crates/dtgen/src/codegen/flash.rs new file mode 100644 index 0000000..9735af4 --- /dev/null +++ b/xtasks/crates/dtgen/src/codegen/flash.rs @@ -0,0 +1,251 @@ +//! Flash partition registry codegen. + +use super::*; + +#[derive(Clone)] +struct Partition { + node: usize, + flash_node: usize, + label: String, + compatible: String, + offset: usize, + len: usize, + read_only: bool, +} + +/// Returns `(node_idx, base, size)` of the first `flash@*` node with a +/// valid `reg`, if any. All partitions are assumed to live in this flash; +/// multi-flash systems would need an extension here. +fn find_primary_flash(dt: &DeviceTree) -> Option<(usize, usize, usize)> { + for (idx, n) in dt.nodes.iter().enumerate() { + if !n.name.starts_with("flash@") { + continue; + } + let Some((base, size)) = n.reg else { + continue; + }; + let (Ok(base), Ok(size)) = (usize::try_from(base), usize::try_from(size)) else { + continue; + }; + return Some((idx, base, size)); + } + None +} + +fn collect(dt: &DeviceTree, primary_idx: Option) -> Vec { + let mut out: Vec = Vec::new(); + + for (flash_idx, flash_node) in dt.nodes.iter().enumerate() { + if !flash_node.name.starts_with("flash@") { + continue; + } + let is_primary = primary_idx == Some(flash_idx); + + // Find a child `partitions` node whose compatible includes "fixed-partitions". + for &part_block_idx in &flash_node.children { + let part_block = &dt.nodes[part_block_idx]; + if !part_block + .compatible + .iter() + .any(|c| c == "fixed-partitions") + { + continue; + } + + for &part_idx in &part_block.children { + let part = &dt.nodes[part_idx]; + if !part.name.starts_with("partition@") { + continue; + } + + if !is_primary { + eprintln!( + "cargo::warning=flash partition {} sits under non-primary flash node {}; skipping (multi-flash systems not yet supported)", + part.name, flash_node.name + ); + continue; + } + + let Some((offset_u64, len_u64)) = part.reg else { + eprintln!( + "cargo::warning=flash partition node {} missing reg property; skipping", + part.name + ); + continue; + }; + let (Ok(offset), Ok(len)) = (usize::try_from(offset_u64), usize::try_from(len_u64)) + else { + eprintln!( + "cargo::warning=flash partition {} reg out of usize range; skipping", + part.name + ); + continue; + }; + + let label = match part.extra.get("label") { + Some(PropValue::Str(s)) => s.clone(), + _ => { + eprintln!( + "cargo::warning=flash partition {} missing label; skipping", + part.name + ); + continue; + } + }; + + let compatible = part + .compatible + .first() + .cloned() + .unwrap_or_else(|| "fixed-partitions".to_string()); + + let read_only = part.extra.contains_key("read-only"); + + out.push(Partition { + node: part_idx, + flash_node: flash_idx, + label, + compatible, + offset, + len, + read_only, + }); + } + } + } + + out +} + +/// True iff any `partition@*` node exists anywhere in the DT. +fn any_partition_in_dt(dt: &DeviceTree) -> bool { + dt.nodes.iter().any(|n| n.name.starts_with("partition@")) +} + +pub fn emit_registry(dt: &DeviceTree) -> TokenStream { + let primary = find_primary_flash(dt); + let (flash_base, flash_total_size) = primary + .map(|(_, base, size)| (base, size)) + .unwrap_or((0, 0)); + let parts = collect(dt, primary.map(|(idx, _, _)| idx)); + + if primary.is_none() { + if any_partition_in_dt(dt) { + eprintln!( + "cargo::error=no `flash@*` node with a valid `reg` property in device tree, but partition nodes are present — FLASH_BASE would be 0 and partition addresses would be bogus" + ); + std::process::exit(1); + } else { + eprintln!( + "cargo::warning=no `flash@*` node found in device tree; FLASH_BASE/FLASH_TOTAL_SIZE emitted as 0" + ); + } + } + + for p in &parts { + let offset_end = p.offset.checked_add(p.len); + let abs_end = flash_base + .checked_add(p.offset) + .and_then(|s| s.checked_add(p.len)); + let in_range = offset_end.map(|e| e <= flash_total_size).unwrap_or(false); + if abs_end.is_none() || !in_range { + eprintln!( + "cargo::error=flash partition '{}' (offset={:#x}, len={:#x}) overflows or exceeds flash size {:#x}", + p.label, p.offset, p.len, flash_total_size + ); + std::process::exit(1); + } + } + + let entries = parts.iter().map(|p| { + let node = p.node; + let flash_node = p.flash_node; + let label = p.label.as_str(); + let compatible = p.compatible.as_str(); + let offset = p.offset; + let len = p.len; + let read_only = p.read_only; + quote! { + FlashPartitionRegistryEntry { + node: #node, + flash_node: #flash_node, + label: #label, + compatible: #compatible, + offset: #offset, + len: #len, + read_only: #read_only, + }, + } + }); + + quote! { + /// Absolute base address of the primary flash bank, sourced from + /// the parent `flash@*` node's `reg` cell. Zero if no flash node + /// is present in the device tree. + pub const FLASH_BASE: usize = #flash_base; + + /// Total size of the primary flash, in bytes, from the same `reg`. + /// Note that this is the static DT value; runtime queries (e.g. + /// dual-bank vs single-bank page sizes) live in the chip HAL. + pub const FLASH_TOTAL_SIZE: usize = #flash_total_size; + + #[derive(Debug, Clone, Copy)] + #[repr(C)] + pub struct FlashPartitionRegistryEntry { + pub node: usize, + pub flash_node: usize, + pub label: &'static str, + pub compatible: &'static str, + /// Offset within the flash, relative to `FLASH_BASE`. + pub offset: usize, + pub len: usize, + pub read_only: bool, + } + + pub const FLASH_PARTITION_REGISTRY: &[FlashPartitionRegistryEntry] = &[ + #(#entries)* + ]; + } +} + +pub fn emit_query_api() -> TokenStream { + quote! { + pub fn flash_partition_by_compatible( + compatible: &str, + ord: usize, + ) -> Option<&'static FlashPartitionRegistryEntry> { + let mut matches = 0usize; + for p in FLASH_PARTITION_REGISTRY { + if p.compatible == compatible { + if matches == ord { + return Some(p); + } + matches += 1; + } + } + None + } + + pub fn flash_partition_by_label( + label: &str, + ) -> Option<&'static FlashPartitionRegistryEntry> { + FLASH_PARTITION_REGISTRY.iter().find(|p| p.label == label) + } + + /// Find the partition that contains the given absolute flash + /// address and return it together with the offset of that + /// address from the partition's start. + pub fn flash_partition_by_address( + address: usize, + ) -> Option<(&'static FlashPartitionRegistryEntry, usize)> { + for p in FLASH_PARTITION_REGISTRY { + let start = FLASH_BASE + p.offset; + let end = start + p.len; + if address >= start && address < end { + return Some((p, address - start)); + } + } + None + } + } +}