From fab6546a08397435191d0dc1e72f585e213f33d0 Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sun, 10 May 2026 21:12:59 +0000 Subject: [PATCH 1/7] Flash driver: HAL --- Cargo.lock | 1 + machine/cortex-m/Cargo.toml | 2 + machine/cortex-m/build.rs | 56 +++++- .../cortex-m/st/stm32l4/interface/export.h | 79 +++++++++ machine/cortex-m/st/stm32l4/interface/flash.c | 166 ++++++++++++++++++ 5 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 machine/cortex-m/st/stm32l4/interface/flash.c 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/machine/cortex-m/Cargo.toml b/machine/cortex-m/Cargo.toml index 5871c24..d5e3979 100644 --- a/machine/cortex-m/Cargo.toml +++ b/machine/cortex-m/Cargo.toml @@ -21,6 +21,8 @@ anyhow = "1.0.99" serde_json = "1.0.145" quote = "1.0.26" syn = { version = "2.0.36", features = ["full"] } +regex = "1.11" +dtgen = { workspace = true } [features] panic-exit = [] diff --git a/machine/cortex-m/build.rs b/machine/cortex-m/build.rs index c33f61c..0d75ba3 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 @@ -326,7 +366,7 @@ fn main() { let hal = Path::new(vendor).join(name); if hal.exists() { - fail_on_error(generate_bindings(&out, &hal)); + fail_on_error(generate_bindings(&out, &hal, &hal_builder::dt::soc(&dt))); let vector_code = vector_table::generate(); if let Err(e) = fs::write(PathBuf::from(&out).join("vector_table.rs"), vector_code) { diff --git a/machine/cortex-m/st/stm32l4/interface/export.h b/machine/cortex-m/st/stm32l4/interface/export.h index 7f71638..74f54a6 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 @@ -123,6 +155,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 one flash page by global page number (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, 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..a13186c --- /dev/null +++ b/machine/cortex-m/st/stm32l4/interface/flash.c @@ -0,0 +1,166 @@ +#include "export.h" +#include "lib.h" +#include "stm32l4xx_hal.c" +#include "stm32l4xx.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) + +// 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 a flash page given a page number. +// The bank will be automatically determined. +uint32_t flash_erase(uint32_t page, uint32_t timeout_ms) { + if (flash_is_busy()) { + return ERR_FLASH_BUSY; + } + if (page >= 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 >= half_page_count) { + bank = FLASH_BANK_2; + page -= half_page_count; + } +#endif + + FLASH_PageErase(page, bank); + + HAL_StatusTypeDef status = FLASH_WaitForLastOperation(timeout_ms); + if (status != HAL_OK) { + return HAL_FLASH_GetError(); + } + + return HAL_OK; +} + +uint32_t flash_program(uint32_t start_address, const uint64_t *data, + uint32_t length, uint32_t timeout_ms) { + // TODO: check if start_address and start_address + (length * 8) are within + // flash bounds - not sure how to do that with device trees. + + if (flash_is_busy()) { + // caller should wait until no longer busy + return ERR_FLASH_BUSY; + } + + 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, + start_address + (dw_written * sizeof(uint64_t)), data[dw_written]); + __set_PRIMASK(primask); + + if (status != HAL_OK) { + return HAL_FLASH_GetError(); + } + + elapsed = HAL_GetTick() - tickstart; + if (elapsed >= timeout_ms) { + return ERR_FLASH_TIMEOUT; + } + status = FLASH_WaitForLastOperation(timeout_ms - elapsed); + if (status != HAL_OK) { + return HAL_FLASH_GetError(); + } + } + + return FLASH_OK; +} From 8b17cd456fa5561695454e1cd722a0fdf93948e8 Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sun, 10 May 2026 21:19:07 +0000 Subject: [PATCH 2/7] Flash driver: kernel + uapi --- Cargo.lock | 1 + boards/nucleo_l4r5zi.dts | 7 +- machine/cortex-m/src/native.rs | 1 + machine/cortex-m/src/native/flash.rs | 215 ++++++++++++++++++ machine/cortex-m/src/stub.rs | 1 + machine/cortex-m/src/stub/flash.rs | 80 +++++++ .../cortex-m/st/stm32l4/interface/export.h | 10 +- machine/cortex-m/st/stm32l4/interface/flash.c | 26 ++- src/drivers.rs | 1 + src/drivers/flash.rs | 202 ++++++++++++++++ src/uapi.rs | 1 + src/uapi/flash.rs | 10 + xtasks/crates/dtgen/src/codegen.rs | 202 ++++++++++++++++ 13 files changed, 738 insertions(+), 19 deletions(-) create mode 100644 machine/cortex-m/src/native/flash.rs create mode 100644 machine/cortex-m/src/stub/flash.rs create mode 100644 src/drivers/flash.rs create mode 100644 src/uapi/flash.rs diff --git a/Cargo.lock b/Cargo.lock index 73bb3ec..0f986c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -778,6 +778,7 @@ dependencies = [ "cbindgen", "cmake", "critical-section", + "dtgen", "hal-api", "hal-builder", "quote", 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/cortex-m/src/native.rs b/machine/cortex-m/src/native.rs index 436b665..e6bb467 100644 --- a/machine/cortex-m/src/native.rs +++ b/machine/cortex-m/src/native.rs @@ -5,6 +5,7 @@ pub use hal_api::*; pub mod asm; 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..f03e8e4 --- /dev/null +++ b/machine/cortex-m/src/native/flash.rs @@ -0,0 +1,215 @@ +use super::bindings; +use super::device_tree; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidArgument, + OutOfBounds, + Misaligned, + NotErased, + Busy, + Locked, + DoubleUnlock, + InvalidPage, + TimedOut, + Io, + NotFound, +} + +pub type Result = core::result::Result; + +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); + } + + Err(Error::Io) +} + +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 } +} + +/// Absolute start address of the flash page identified by `page_index`. +pub fn page_address(page_index: usize) -> Option { + if page_index >= page_count() { + return None; + } + Some(flash_base() + page_index * page_size()) +} + +/// Page index containing the byte at absolute `address`. +pub fn page_index_for_address(address: usize) -> Option { + let base = flash_base(); + let size = total_size(); + if address < base || address >= base + size { + return None; + } + Some((address - base) / page_size()) +} + +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 flash page identified by `page_index` (i.e., a page number, not +/// an address). The bank is derived automatically inside the C primitive. +pub fn erase_page(page_index: usize, timeout_ms: u32, lock_wait_ms: u32) -> Result<()> { + if page_index >= page_count() { + return Err(Error::InvalidPage); + } + with_unlock( + || { + let rc = unsafe { bindings::flash_erase(page_index as u32, timeout_ms) }; + from_c_rc(rc) + }, + lock_wait_ms, + ) +} + +pub fn program(address: usize, data: &[u64], timeout_ms: u32, lock_wait_ms: u32) -> Result<()> { + if data.is_empty() { + return Ok(()); + } + if address & 0x7 != 0 { + return Err(Error::Misaligned); + } + let base = flash_base(); + let size = total_size(); + let bytes = data.len() * core::mem::size_of::(); + let end = address.checked_add(bytes).ok_or(Error::OutOfBounds)?; + if address < base || end > base + size { + return Err(Error::OutOfBounds); + } + + with_unlock( + || { + let rc = unsafe { + bindings::flash_program( + address as u32, + data.as_ptr(), + data.len() as u32, + timeout_ms, + ) + }; + from_c_rc(rc) + }, + lock_wait_ms, + ) +} + +pub fn read(address: usize, buf: &mut [u8]) -> Result<()> { + if buf.is_empty() { + return Ok(()); + } + let base = flash_base(); + let size = total_size(); + let end = address.checked_add(buf.len()).ok_or(Error::OutOfBounds)?; + if address < base || end > base + size { + return Err(Error::OutOfBounds); + } + // Flash is memory-mapped; copy via volatile reads to keep the compiler honest. + unsafe { + let src = address as *const u8; + for i in 0..buf.len() { + buf[i] = core::ptr::read_volatile(src.add(i)); + } + } + Ok(()) +} + +#[derive(Clone, Copy)] +pub struct Region(&'static device_tree::FlashPartitionRegistryEntry); + +impl Region { + pub fn get(compatible: &str, ordinal: usize) -> Result { + device_tree::flash_partition_by_compatible(compatible, ordinal) + .map(Self) + .ok_or(Error::NotFound) + } + + pub fn get_by_label(label: &str) -> Result { + device_tree::flash_partition_by_label(label) + .map(Self) + .ok_or(Error::NotFound) + } + + 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 + } + + pub fn start(&self) -> usize { + device_tree::FLASH_BASE + self.0.offset + } + + pub fn offset_in_flash(&self) -> usize { + self.0.offset + } + + 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 5a700a7..f0b809f 100644 --- a/machine/cortex-m/src/stub.rs +++ b/machine/cortex-m/src/stub.rs @@ -3,6 +3,7 @@ pub use hal_api::*; pub mod asm; 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..6cd0cc2 --- /dev/null +++ b/machine/cortex-m/src/stub/flash.rs @@ -0,0 +1,80 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidArgument, + OutOfBounds, + Misaligned, + NotErased, + Busy, + Locked, + DoubleUnlock, + InvalidPage, + TimedOut, + Io, + NotFound, +} + +pub type Result = core::result::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 page_address(_page_index: usize) -> Option { + None +} +pub fn page_index_for_address(_address: usize) -> Option { + None +} +pub fn erase_page(_page_index: usize, _timeout_ms: u32, _lock_wait_ms: u32) -> Result<()> { + Err(Error::Io) +} +pub fn program(_address: usize, _data: &[u64], _timeout_ms: u32, _lock_wait_ms: u32) -> Result<()> { + Err(Error::Io) +} +pub fn read(_address: usize, _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 label(&self) -> &'static str { + "" + } + pub fn compatible(&self) -> &'static str { + "" + } + pub fn read_only(&self) -> bool { + false + } + pub fn start(&self) -> usize { + 0 + } + pub fn offset_in_flash(&self) -> usize { + 0 + } + 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 74f54a6..23e0a8c 100644 --- a/machine/cortex-m/st/stm32l4/interface/export.h +++ b/machine/cortex-m/st/stm32l4/interface/export.h @@ -186,11 +186,11 @@ uint32_t flash_unlock(void); // flight; returns ERR_FLASH_BUSY otherwise. uint32_t flash_lock(void); -// Erase one flash page by global page number (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, uint32_t timeout_ms); +// 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 diff --git a/machine/cortex-m/st/stm32l4/interface/flash.c b/machine/cortex-m/st/stm32l4/interface/flash.c index a13186c..6c6446c 100644 --- a/machine/cortex-m/st/stm32l4/interface/flash.c +++ b/machine/cortex-m/st/stm32l4/interface/flash.c @@ -91,13 +91,13 @@ uint32_t flash_lock(void) { return HAL_FLASH_Lock(); } -// Erase a flash page given a page number. -// The bank will be automatically determined. -uint32_t flash_erase(uint32_t page, uint32_t timeout_ms) { +// 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 >= flash_page_count()) { + if (page_index >= flash_page_count()) { return ERR_FLASH_INVALID_PAGE; } @@ -105,13 +105,13 @@ uint32_t flash_erase(uint32_t page, uint32_t timeout_ms) { 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 >= half_page_count) { + if (flash_is_dual_bank() && page_index >= half_page_count) { bank = FLASH_BANK_2; - page -= half_page_count; + page_index -= half_page_count; } #endif - FLASH_PageErase(page, bank); + FLASH_PageErase(page_index, bank); HAL_StatusTypeDef status = FLASH_WaitForLastOperation(timeout_ms); if (status != HAL_OK) { @@ -123,11 +123,17 @@ uint32_t flash_erase(uint32_t page, uint32_t timeout_ms) { uint32_t flash_program(uint32_t start_address, const uint64_t *data, uint32_t length, uint32_t timeout_ms) { - // TODO: check if start_address and start_address + (length * 8) are within - // flash bounds - not sure how to do that with device trees. + uint32_t total_bytes = length * (uint32_t)sizeof(uint64_t); + uint32_t end_address = start_address + total_bytes; + if (start_address < FLASH_BASE || end_address < start_address || + end_address > FLASH_BASE + (uint32_t)flash_size()) { + return ERR_FLASH_ILLEGAL; + } + if ((start_address & 0x7U) != 0U) { + return ERR_FLASH_ILLEGAL; + } if (flash_is_busy()) { - // caller should wait until no longer busy return ERR_FLASH_BUSY; } diff --git a/src/drivers.rs b/src/drivers.rs index 86f7bc4..50310b3 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -1,3 +1,4 @@ +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..8c52181 --- /dev/null +++ b/src/drivers/flash.rs @@ -0,0 +1,202 @@ +pub use hal::flash::Error; +pub use hal::flash::Result; + +pub mod raw { + pub use hal::flash::{ + erase_page, flash_base, is_dual_bank, page_address, page_count, page_index_for_address, + page_size, program, read, total_size, + }; +} + +#[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, + }) + } + + 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() + } + + pub fn start(&self) -> usize { + self.desc.start() + } + + 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 read(&self, offset: usize, buf: &mut [u8]) -> Result<()> { + if buf.is_empty() { + return Ok(()); + } + self.check_range(offset, buf.len())?; + hal::flash::read(self.start() + offset, buf) + } + + /// Erase `len` bytes starting at `offset` within the partition. + /// Both `offset` and `len` must be multiples of `page_size()`. + pub fn erase(&self, offset: usize, len: usize) -> Result<()> { + if len == 0 { + return Ok(()); + } + self.check_range(offset, len)?; + let ps = self.page_size(); + if ps == 0 { + return Err(Error::Io); + } + if offset % ps != 0 || len % ps != 0 { + return Err(Error::Misaligned); + } + let first_page_index = hal::flash::page_index_for_address(self.start() + offset) + .ok_or(Error::OutOfBounds)?; + let page_count = len / ps; + for i in 0..page_count { + hal::flash::erase_page( + first_page_index + i, + self.config.erase_timeout_ms, + self.config.lock_timeout_ms, + )?; + } + Ok(()) + } + + pub fn erase_all(&self) -> Result<()> { + self.erase(0, self.len()) + } + + /// Program 64-bit doublewords at `offset` (which must be 8-byte-aligned). + /// The target range must already be erased; flash reads as `0xFF` after erase. + pub fn program(&self, offset: usize, data: &[u64]) -> Result<()> { + if data.is_empty() { + return Ok(()); + } + if offset % core::mem::size_of::() != 0 { + return Err(Error::Misaligned); + } + let bytes = data.len() * core::mem::size_of::(); + self.check_range(offset, bytes)?; + hal::flash::program( + self.start() + offset, + data, + self.config.program_timeout_ms, + self.config.lock_timeout_ms, + ) + } + + /// Erase any pages overlapping `[offset, offset + data.len())`, then program + /// `data` at `offset` (padding the trailing 8-byte chunk with `0xFF` if + /// `data.len()` isn't a multiple of 8). 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. + pub fn write(&self, offset: usize, data: &[u8]) -> Result<()> { + if data.is_empty() { + return Ok(()); + } + self.check_range(offset, data.len())?; + if offset % core::mem::size_of::() != 0 { + return Err(Error::Misaligned); + } + let ps = self.page_size(); + if ps == 0 { + return Err(Error::Io); + } + + let first = (offset / ps) * ps; + let last = offset + .checked_add(data.len()) + .and_then(|end| end.checked_add(ps - 1)) + .map(|x| (x / ps) * ps) + .ok_or(Error::OutOfBounds)?; + let erase_len = last - first; + if first + erase_len > self.len() { + return Err(Error::OutOfBounds); + } + self.erase(first, erase_len)?; + + // Program one doubleword at a time. `&[u8]` has no alignment guarantee, + // so build each u64 from explicit bytes; the trailing partial chunk is + // padded with 0xFF so the cell programs as "blank" past the data end. + let mut written = 0usize; + while written < data.len() { + let mut buf = [0xFFu8; 8]; + let take = core::cmp::min(8, data.len() - written); + buf[..take].copy_from_slice(&data[written..written + take]); + let word = u64::from_le_bytes(buf); + self.program(offset + written, &[word])?; + written += 8; + } + + Ok(()) + } + + fn check_range(&self, offset: usize, len: usize) -> Result<()> { + let end = offset.checked_add(len).ok_or(Error::OutOfBounds)?; + if end > self.len() { + return Err(Error::OutOfBounds); + } + Ok(()) + } +} diff --git a/src/uapi.rs b/src/uapi.rs index 5b4c3df..a0ee8f1 100644 --- a/src/uapi.rs +++ b/src/uapi.rs @@ -1,3 +1,4 @@ +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..5651bfd --- /dev/null +++ b/src/uapi/flash.rs @@ -0,0 +1,10 @@ +pub use crate::drivers::flash::raw; +pub use crate::drivers::flash::{Config, Error, Region, Result}; + +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) +} diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index ab65bb0..d361ef8 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -14,6 +14,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(), emit_aliases_module(dt), emit_memory_module(dt), emit_chosen_module(dt), @@ -1374,6 +1376,206 @@ mod spi { } } +// ------------------------------------------------------------------------------------------------ +// Flash partition registry +// ------------------------------------------------------------------------------------------------ + +mod flash { + use super::*; + + #[derive(Clone)] + struct Partition { + node: usize, + flash_node: usize, + label: String, + compatible: String, + offset: usize, + len: usize, + read_only: bool, + } + + /// Returns (base, size) of the first `flash@*` node found in the tree, 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) -> 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; + } + // We don't need the flash base here — it's emitted as a top-level + // FLASH_BASE constant; partitions store offsets relative to it. + + // 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; + } + + 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 + } + + pub fn emit_registry(dt: &DeviceTree) -> TokenStream { + let parts = collect(dt); + let (flash_base, flash_total_size) = find_primary_flash(dt) + .map(|(_, base, size)| (base, size)) + .unwrap_or((0, 0)); + + 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) + } + } + } +} + fn resolve_path(dt: &DeviceTree, path: &str) -> Option { if path == "/" { return Some(dt.root); From 3ee53103c3b4d2440f2348128242185f32c2f4f6 Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sun, 10 May 2026 21:19:13 +0000 Subject: [PATCH 3/7] Flash driver: type safety, read-only regions, address lookup --- machine/api/src/flash_addr.rs | 454 ++++++++++++++++++ machine/api/src/lib.rs | 2 + machine/cortex-m/src/native/flash.rs | 195 +++++--- machine/cortex-m/src/stub/flash.rs | 73 +-- machine/cortex-m/st/stm32l4/interface/flash.c | 15 +- src/drivers/flash.rs | 132 +++-- src/uapi/flash.rs | 14 +- xtasks/crates/dtgen/src/codegen.rs | 16 + 8 files changed, 747 insertions(+), 154 deletions(-) create mode 100644 machine/api/src/flash_addr.rs diff --git a/machine/api/src/flash_addr.rs b/machine/api/src/flash_addr.rs new file mode 100644 index 0000000..ffb5dfc --- /dev/null +++ b/machine/api/src/flash_addr.rs @@ -0,0 +1,454 @@ +//! Generic flash address arithmetic. +//! +//! Backends (`hal-arm`, `hal-testing`, …) implement [`Flash`] to plug their +//! chip-specific queries (`flash_base`, `total_size`, `page_size`, +//! `page_count`) into the validating constructors of the shared newtypes. +//! +//! Each backend then re-exports type aliases so user code calls +//! `hal::flash::FlashAddress::new(addr)` without seeing the generic. + +use core::marker::PhantomData; + +/// Forward `fmt` and `Hash` to the wrapped `usize`. Lets a newtype be used +/// with `{:x}` / `{:#X}` / `{:b}` / `{}` / `HashMap` keys without being a +/// `usize` itself. `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, + Io, + NotFound, + /// The DT marked this partition `read-only`; mutating operations + /// (erase/program/write) refuse to touch it. + ReadOnly, +} + +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; + fn page_size() -> usize; + fn page_count() -> 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() + } + + /// Page that follows this one. Returns `Err(InvalidPage)` past the last page. + pub fn next(self) -> Result { + Self::from_page_index(self.page_index() + 1) + } + + /// Page that precedes this one. Returns `Err(InvalidPage)` if already at index 0. + pub fn prev(self) -> Result { + let idx = self.page_index().checked_sub(1).ok_or(Error::InvalidPage)?; + Self::from_page_index(idx) + } + + /// Move forward by `n` pages. Returns `Err(InvalidPage)` past the last page. + 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) + } + + /// Move backward by `n` pages. Returns `Err(InvalidPage)` on underflow. + 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 ddb72c9..6dcdf54 100644 --- a/machine/api/src/lib.rs +++ b/machine/api/src/lib.rs @@ -2,6 +2,8 @@ use core::fmt; use core::{fmt::Display, ops::Range}; + +pub mod flash_addr; pub mod mem; pub mod stack; diff --git a/machine/cortex-m/src/native/flash.rs b/machine/cortex-m/src/native/flash.rs index f03e8e4..a47b570 100644 --- a/machine/cortex-m/src/native/flash.rs +++ b/machine/cortex-m/src/native/flash.rs @@ -1,22 +1,62 @@ use super::bindings; use super::device_tree; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Error { - InvalidArgument, - OutOfBounds, - Misaligned, - NotErased, - Busy, - Locked, - DoubleUnlock, - InvalidPage, - TimedOut, - Io, - NotFound, +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 type Result = core::result::Result; +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 } +} + +// --------------------------------------------------------------------------- +// 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() + } +} + +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 { @@ -52,44 +92,6 @@ fn from_c_rc(rc: u32) -> Result<()> { Err(Error::Io) } -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 } -} - -/// Absolute start address of the flash page identified by `page_index`. -pub fn page_address(page_index: usize) -> Option { - if page_index >= page_count() { - return None; - } - Some(flash_base() + page_index * page_size()) -} - -/// Page index containing the byte at absolute `address`. -pub fn page_index_for_address(address: usize) -> Option { - let base = flash_base(); - let size = total_size(); - if address < base || address >= base + size { - return None; - } - Some((address - base) / page_size()) -} - fn with_unlock Result<()>>(f: F, lock_wait_ms: u32) -> Result<()> { let unlock_rc = unsafe { bindings::flash_unlock() }; from_c_rc(unlock_rc)?; @@ -103,33 +105,43 @@ fn with_unlock Result<()>>(f: F, lock_wait_ms: u32) -> Result<()> result } -/// Erase the flash page identified by `page_index` (i.e., a page number, not -/// an address). The bank is derived automatically inside the C primitive. -pub fn erase_page(page_index: usize, timeout_ms: u32, lock_wait_ms: u32) -> Result<()> { - if page_index >= page_count() { - return Err(Error::InvalidPage); - } +/// 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_index as u32, timeout_ms) }; + let rc = unsafe { bindings::flash_erase(page_start.page_index() as u32, timeout_ms) }; from_c_rc(rc) }, lock_wait_ms, ) } -pub fn program(address: usize, data: &[u64], timeout_ms: u32, lock_wait_ms: u32) -> Result<()> { +/// 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` +/// directly, a `FlashPageStart` (always page- and word-aligned), or a +/// `FlashOffset`. +pub fn program>( + start: A, + data: &[u64], + timeout_ms: u32, + lock_wait_ms: u32, +) -> Result<()> { if data.is_empty() { return Ok(()); } - if address & 0x7 != 0 { + let start: FlashAddress = start.into(); + if start.as_usize() & 0x7 != 0 { return Err(Error::Misaligned); } - let base = flash_base(); - let size = total_size(); let bytes = data.len() * core::mem::size_of::(); - let end = address.checked_add(bytes).ok_or(Error::OutOfBounds)?; - if address < base || end > base + size { + let end = start + .as_usize() + .checked_add(bytes) + .ok_or(Error::OutOfBounds)?; + if end > flash_base() + total_size() { return Err(Error::OutOfBounds); } @@ -137,7 +149,7 @@ pub fn program(address: usize, data: &[u64], timeout_ms: u32, lock_wait_ms: u32) || { let rc = unsafe { bindings::flash_program( - address as u32, + start.as_usize() as u32, data.as_ptr(), data.len() as u32, timeout_ms, @@ -149,19 +161,22 @@ pub fn program(address: usize, data: &[u64], timeout_ms: u32, lock_wait_ms: u32) ) } -pub fn read(address: usize, buf: &mut [u8]) -> Result<()> { +/// 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 base = flash_base(); - let size = total_size(); - let end = address.checked_add(buf.len()).ok_or(Error::OutOfBounds)?; - if address < base || end > base + size { + let start: FlashAddress = start.into(); + let end = start + .as_usize() + .checked_add(buf.len()) + .ok_or(Error::OutOfBounds)?; + if end > flash_base() + total_size() { return Err(Error::OutOfBounds); } - // Flash is memory-mapped; copy via volatile reads to keep the compiler honest. unsafe { - let src = address as *const u8; + let src = start.as_usize() as *const u8; for i in 0..buf.len() { buf[i] = core::ptr::read_volatile(src.add(i)); } @@ -169,6 +184,10 @@ pub fn read(address: usize, buf: &mut [u8]) -> Result<()> { Ok(()) } +// --------------------------------------------------------------------------- +// DT-driven partition handle +// --------------------------------------------------------------------------- + #[derive(Clone, Copy)] pub struct Region(&'static device_tree::FlashPartitionRegistryEntry); @@ -185,6 +204,23 @@ impl Region { .ok_or(Error::NotFound) } + /// Find the partition that contains `addr` and return it together with + /// the byte offset of `addr` from that partition's start. + /// + /// The returned `usize` is **partition-relative**, not a `FlashOffset` + /// (which is from `FLASH_BASE`) — it can be passed straight to the + /// driver-layer `Region::read`/`erase`/`program`/`write` methods. + /// + /// Accepts any `Into`, so a `FlashOffset` or + /// `FlashPageStart` works too. Returns `Err(NotFound)` if `addr` doesn't + /// fall in any declared partition. + pub fn get_by_address(addr: impl Into) -> Result<(Self, usize)> { + let addr: FlashAddress = addr.into(); + device_tree::flash_partition_by_address(addr.as_usize()) + .map(|(entry, offset)| (Self(entry), offset)) + .ok_or(Error::NotFound) + } + pub fn label(&self) -> &'static str { self.0.label } @@ -197,12 +233,17 @@ impl Region { self.0.read_only } - pub fn start(&self) -> usize { - device_tree::FLASH_BASE + self.0.offset + /// 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") } - pub fn offset_in_flash(&self) -> usize { - self.0.offset + /// 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 { diff --git a/machine/cortex-m/src/stub/flash.rs b/machine/cortex-m/src/stub/flash.rs index 6cd0cc2..6f9acfb 100644 --- a/machine/cortex-m/src/stub/flash.rs +++ b/machine/cortex-m/src/stub/flash.rs @@ -1,19 +1,4 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Error { - InvalidArgument, - OutOfBounds, - Misaligned, - NotErased, - Busy, - Locked, - DoubleUnlock, - InvalidPage, - TimedOut, - Io, - NotFound, -} - -pub type Result = core::result::Result; +pub use hal_api::flash_addr::{Error, Result}; pub fn flash_base() -> usize { 0x0800_0000 @@ -30,19 +15,46 @@ pub fn page_size() -> usize { pub fn page_count() -> usize { 0 } -pub fn page_address(_page_index: usize) -> Option { - None -} -pub fn page_index_for_address(_address: usize) -> Option { - None + +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() + } } -pub fn erase_page(_page_index: usize, _timeout_ms: u32, _lock_wait_ms: u32) -> Result<()> { + +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(_address: usize, _data: &[u64], _timeout_ms: u32, _lock_wait_ms: u32) -> Result<()> { + +pub fn program>( + _start: A, + _data: &[u64], + _timeout_ms: u32, + _lock_wait_ms: u32, +) -> Result<()> { Err(Error::Io) } -pub fn read(_address: usize, _buf: &mut [u8]) -> Result<()> { + +pub fn read>(_start: A, _buf: &mut [u8]) -> Result<()> { Err(Error::Io) } @@ -56,6 +68,9 @@ impl Region { pub fn get_by_label(_label: &str) -> Result { Err(Error::NotFound) } + pub fn get_by_address(_addr: impl Into) -> Result<(Self, usize)> { + Err(Error::NotFound) + } pub fn label(&self) -> &'static str { "" } @@ -65,11 +80,13 @@ impl Region { pub fn read_only(&self) -> bool { false } - pub fn start(&self) -> usize { - 0 + 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 offset_in_flash(&self) -> usize { - 0 + pub fn flash_offset(&self) -> FlashOffset { + unreachable!("flash region accessed in host testing stub") } pub fn len(&self) -> usize { 0 diff --git a/machine/cortex-m/st/stm32l4/interface/flash.c b/machine/cortex-m/st/stm32l4/interface/flash.c index 6c6446c..ee183f9 100644 --- a/machine/cortex-m/st/stm32l4/interface/flash.c +++ b/machine/cortex-m/st/stm32l4/interface/flash.c @@ -1,7 +1,7 @@ #include "export.h" #include "lib.h" -#include "stm32l4xx_hal.c" #include "stm32l4xx.h" +#include "stm32l4xx_hal.h" #include "stm32l4xx_hal_cortex.h" #include "stm32l4xx_hal_def.h" #include "stm32l4xx_hal_flash.h" @@ -121,15 +121,18 @@ uint32_t flash_erase(uint32_t page_index, uint32_t timeout_ms) { return HAL_OK; } -uint32_t flash_program(uint32_t start_address, const uint64_t *data, +// 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 = start_address + total_bytes; - if (start_address < FLASH_BASE || end_address < start_address || + 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 ((start_address & 0x7U) != 0U) { + if ((flash_address & 0x7U) != 0U) { return ERR_FLASH_ILLEGAL; } @@ -151,7 +154,7 @@ uint32_t flash_program(uint32_t start_address, const uint64_t *data, __disable_irq(); HAL_StatusTypeDef status = HAL_FLASH_Program( FLASH_TYPEPROGRAM_DOUBLEWORD, - start_address + (dw_written * sizeof(uint64_t)), data[dw_written]); + flash_address + (dw_written * sizeof(uint64_t)), data[dw_written]); __set_PRIMASK(primask); if (status != HAL_OK) { diff --git a/src/drivers/flash.rs b/src/drivers/flash.rs index 8c52181..1bc65cd 100644 --- a/src/drivers/flash.rs +++ b/src/drivers/flash.rs @@ -1,10 +1,12 @@ -pub use hal::flash::Error; -pub use hal::flash::Result; +use crate::hal; + +pub use hal::flash::{Error, FlashAddress, FlashOffset, FlashPageStart, Result}; pub mod raw { + use crate::hal; pub use hal::flash::{ - erase_page, flash_base, is_dual_bank, page_address, page_count, page_index_for_address, - page_size, program, read, total_size, + FlashAddress, FlashOffset, FlashPageStart, erase_page, flash_base, is_dual_bank, + page_count, page_size, program, read, total_size, }; } @@ -50,6 +52,29 @@ impl Region { }) } + /// Find the partition that contains `addr`, open it with `config`, and + /// return both the `Region` and the **partition-relative** byte offset + /// of `addr` within it. The returned `usize` is what + /// `Region::read`/`erase`/`program`/`write` expect — i.e. distance from + /// `region.start_address()`, not a `FlashOffset` (which is from + /// `FLASH_BASE`). + /// + /// ```ignore + /// let (slot, off) = flash::open_by_address(addr, Config::default())?; + /// slot.read(off, &mut buf)?; + /// ``` + /// + /// Accepts any `Into` (so a `FlashOffset` or + /// `FlashPageStart` works too). Returns `Err(NotFound)` if `addr` + /// doesn't fall in any declared partition. + pub fn open_by_address( + addr: impl Into, + config: Config, + ) -> Result<(Self, usize)> { + let (desc, partition_offset) = hal::flash::Region::get_by_address(addr)?; + Ok((Self { desc, config }, partition_offset)) + } + pub fn config(&self) -> Config { self.config } @@ -66,8 +91,14 @@ impl Region { self.desc.read_only() } - pub fn start(&self) -> usize { - self.desc.start() + /// 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 { @@ -87,34 +118,40 @@ impl Region { if ps == 0 { 0 } else { self.len() / ps } } - pub fn read(&self, offset: usize, buf: &mut [u8]) -> Result<()> { + /// Read `buf.len()` bytes starting at `partition_offset` (a byte offset + /// from the partition's start, not an absolute flash address). + pub fn read(&self, partition_offset: usize, buf: &mut [u8]) -> Result<()> { if buf.is_empty() { return Ok(()); } - self.check_range(offset, buf.len())?; - hal::flash::read(self.start() + offset, buf) + self.check_range(partition_offset, buf.len())?; + hal::flash::read(self.start_address().checked_add(partition_offset)?, buf) } - /// Erase `len` bytes starting at `offset` within the partition. - /// Both `offset` and `len` must be multiples of `page_size()`. - pub fn erase(&self, offset: usize, len: usize) -> Result<()> { + /// Erase `len` bytes starting at `partition_offset`. + /// Both `partition_offset` and `len` must be multiples of `page_size()`. + /// Returns `Err(ReadOnly)` if the DT marks this partition `read-only`. + pub fn erase(&self, partition_offset: usize, len: usize) -> Result<()> { if len == 0 { return Ok(()); } - self.check_range(offset, len)?; + if self.read_only() { + return Err(Error::ReadOnly); + } + self.check_range(partition_offset, len)?; let ps = self.page_size(); if ps == 0 { return Err(Error::Io); } - if offset % ps != 0 || len % ps != 0 { + if partition_offset % ps != 0 || len % ps != 0 { return Err(Error::Misaligned); } - let first_page_index = hal::flash::page_index_for_address(self.start() + offset) - .ok_or(Error::OutOfBounds)?; - let page_count = len / ps; - for i in 0..page_count { + let first_page = FlashPageStart::new( + self.start_address().checked_add(partition_offset)?.as_usize(), + )?; + for page in first_page.iter_pages(len / ps)? { hal::flash::erase_page( - first_page_index + i, + page, self.config.erase_timeout_ms, self.config.lock_timeout_ms, )?; @@ -126,37 +163,46 @@ impl Region { self.erase(0, self.len()) } - /// Program 64-bit doublewords at `offset` (which must be 8-byte-aligned). - /// The target range must already be erased; flash reads as `0xFF` after erase. - pub fn program(&self, offset: usize, data: &[u64]) -> Result<()> { + /// Program 64-bit doublewords at `partition_offset` (a byte offset from + /// the partition's start; must be 8-byte-aligned). 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, partition_offset: usize, data: &[u64]) -> Result<()> { if data.is_empty() { return Ok(()); } - if offset % core::mem::size_of::() != 0 { + if self.read_only() { + return Err(Error::ReadOnly); + } + if partition_offset % core::mem::size_of::() != 0 { return Err(Error::Misaligned); } let bytes = data.len() * core::mem::size_of::(); - self.check_range(offset, bytes)?; + self.check_range(partition_offset, bytes)?; hal::flash::program( - self.start() + offset, + self.start_address().checked_add(partition_offset)?, data, self.config.program_timeout_ms, self.config.lock_timeout_ms, ) } - /// Erase any pages overlapping `[offset, offset + data.len())`, then program - /// `data` at `offset` (padding the trailing 8-byte chunk with `0xFF` if - /// `data.len()` isn't a multiple of 8). 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. - pub fn write(&self, offset: usize, data: &[u8]) -> Result<()> { + /// Erase any pages overlapping `[partition_offset, partition_offset + data.len())`, + /// then program `data` at `partition_offset` (padding the trailing 8-byte + /// chunk with `0xFF` if `data.len()` isn't a multiple of 8). 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. + pub fn write(&self, partition_offset: usize, data: &[u8]) -> Result<()> { if data.is_empty() { return Ok(()); } - self.check_range(offset, data.len())?; - if offset % core::mem::size_of::() != 0 { + if self.read_only() { + return Err(Error::ReadOnly); + } + self.check_range(partition_offset, data.len())?; + if partition_offset % core::mem::size_of::() != 0 { return Err(Error::Misaligned); } let ps = self.page_size(); @@ -164,17 +210,17 @@ impl Region { return Err(Error::Io); } - let first = (offset / ps) * ps; - let last = offset + let first_page_offset = (partition_offset / ps) * ps; + let last_page_end = partition_offset .checked_add(data.len()) .and_then(|end| end.checked_add(ps - 1)) .map(|x| (x / ps) * ps) .ok_or(Error::OutOfBounds)?; - let erase_len = last - first; - if first + erase_len > self.len() { + let erase_len = last_page_end - first_page_offset; + if first_page_offset + erase_len > self.len() { return Err(Error::OutOfBounds); } - self.erase(first, erase_len)?; + self.erase(first_page_offset, erase_len)?; // Program one doubleword at a time. `&[u8]` has no alignment guarantee, // so build each u64 from explicit bytes; the trailing partial chunk is @@ -185,15 +231,17 @@ impl Region { let take = core::cmp::min(8, data.len() - written); buf[..take].copy_from_slice(&data[written..written + take]); let word = u64::from_le_bytes(buf); - self.program(offset + written, &[word])?; + self.program(partition_offset + written, &[word])?; written += 8; } Ok(()) } - fn check_range(&self, offset: usize, len: usize) -> Result<()> { - let end = offset.checked_add(len).ok_or(Error::OutOfBounds)?; + fn check_range(&self, partition_offset: usize, len: usize) -> Result<()> { + let end = partition_offset + .checked_add(len) + .ok_or(Error::OutOfBounds)?; if end > self.len() { return Err(Error::OutOfBounds); } diff --git a/src/uapi/flash.rs b/src/uapi/flash.rs index 5651bfd..f71a248 100644 --- a/src/uapi/flash.rs +++ b/src/uapi/flash.rs @@ -1,5 +1,7 @@ pub use crate::drivers::flash::raw; -pub use crate::drivers::flash::{Config, Error, Region, Result}; +pub use crate::drivers::flash::{ + Config, Error, FlashAddress, FlashOffset, FlashPageStart, Region, Result, +}; pub fn open(compatible: &str, ordinal: usize, config: Config) -> Result { Region::open(compatible, ordinal, config) @@ -8,3 +10,13 @@ pub fn open(compatible: &str, ordinal: usize, config: Config) -> Result pub fn open_by_label(label: &str, config: Config) -> Result { Region::open_by_label(label, config) } + +/// Find the partition containing `addr` and open it with `config`. Returns +/// the `Region` plus the **partition-relative** byte offset of `addr` within +/// it (suitable for `Region::read`/`erase`/`program`/`write`). +pub fn open_by_address( + addr: impl Into, + config: Config, +) -> Result<(Region, usize)> { + Region::open_by_address(addr, config) +} diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index d361ef8..08c2c72 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -1572,6 +1572,22 @@ mod flash { ) -> 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 + } } } } From d7297e03c65126ec9f3d8e21bf2ee2d249712cee Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sun, 10 May 2026 22:43:16 +0000 Subject: [PATCH 4/7] Flash: fix erase returning errors on success + Other improvements --- machine/api/src/flash_addr.rs | 52 +++-- machine/cortex-m/src/native/flash.rs | 32 ++- machine/cortex-m/src/stub/flash.rs | 6 + machine/cortex-m/st/stm32l4/interface/flash.c | 38 +++- src/drivers/flash.rs | 215 +++++++++++------- src/uapi/flash.rs | 8 +- 6 files changed, 224 insertions(+), 127 deletions(-) diff --git a/machine/api/src/flash_addr.rs b/machine/api/src/flash_addr.rs index ffb5dfc..34df388 100644 --- a/machine/api/src/flash_addr.rs +++ b/machine/api/src/flash_addr.rs @@ -1,18 +1,7 @@ -//! Generic flash address arithmetic. -//! -//! Backends (`hal-arm`, `hal-testing`, …) implement [`Flash`] to plug their -//! chip-specific queries (`flash_base`, `total_size`, `page_size`, -//! `page_count`) into the validating constructors of the shared newtypes. -//! -//! Each backend then re-exports type aliases so user code calls -//! `hal::flash::FlashAddress::new(addr)` without seeing the generic. - use core::marker::PhantomData; -/// Forward `fmt` and `Hash` to the wrapped `usize`. Lets a newtype be used -/// with `{:x}` / `{:#X}` / `{:b}` / `{}` / `HashMap` keys without being a -/// `usize` itself. `Debug` is left to each type so the output reads as -/// `FlashAddress(0x…)` rather than the bare integer. +/// 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 { @@ -60,11 +49,22 @@ pub enum Error { 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; @@ -74,8 +74,12 @@ pub type Result = core::result::Result; 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; } // --------------------------------------------------------------------------- @@ -89,7 +93,9 @@ 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)?; + let end = base + .checked_add(F::total_size()) + .ok_or(Error::OutOfBounds)?; if address < base || address >= end { return Err(Error::OutOfBounds); } @@ -312,7 +318,9 @@ 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)?; + let end = base + .checked_add(F::total_size()) + .ok_or(Error::OutOfBounds)?; if address < base || address >= end { return Err(Error::OutOfBounds); } @@ -326,7 +334,10 @@ impl FlashPageStart { if page_index >= F::page_count() { return Err(Error::InvalidPage); } - Ok(Self(F::flash_base() + page_index * F::page_size(), PhantomData)) + Ok(Self( + F::flash_base() + page_index * F::page_size(), + PhantomData, + )) } pub fn as_usize(self) -> usize { @@ -337,27 +348,20 @@ impl FlashPageStart { (self.0 - F::flash_base()) / F::page_size() } - /// Page that follows this one. Returns `Err(InvalidPage)` past the last page. pub fn next(self) -> Result { Self::from_page_index(self.page_index() + 1) } - /// Page that precedes this one. Returns `Err(InvalidPage)` if already at index 0. pub fn prev(self) -> Result { let idx = self.page_index().checked_sub(1).ok_or(Error::InvalidPage)?; Self::from_page_index(idx) } - /// Move forward by `n` pages. Returns `Err(InvalidPage)` past the last page. pub fn add_pages(self, n: usize) -> Result { - let idx = self - .page_index() - .checked_add(n) - .ok_or(Error::InvalidPage)?; + let idx = self.page_index().checked_add(n).ok_or(Error::InvalidPage)?; Self::from_page_index(idx) } - /// Move backward by `n` pages. Returns `Err(InvalidPage)` on underflow. 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) diff --git a/machine/cortex-m/src/native/flash.rs b/machine/cortex-m/src/native/flash.rs index a47b570..cc40b97 100644 --- a/machine/cortex-m/src/native/flash.rs +++ b/machine/cortex-m/src/native/flash.rs @@ -27,6 +27,13 @@ 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 // --------------------------------------------------------------------------- @@ -48,6 +55,9 @@ impl hal_api::flash_addr::Flash for ArmFlash { fn page_count() -> usize { page_count() } + fn write_unit_bytes() -> usize { + write_unit_bytes() + } } pub type FlashAddress = hal_api::flash_addr::FlashAddress; @@ -88,6 +98,23 @@ fn from_c_rc(rc: u32) -> Result<()> { 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) } @@ -120,9 +147,8 @@ pub fn erase_page(page_start: FlashPageStart, timeout_ms: u32, lock_wait_ms: u32 /// 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` -/// directly, a `FlashPageStart` (always page- and word-aligned), or a -/// `FlashOffset`. +/// Accepts any type that converts into `FlashAddress` — pass a `FlashAddress`, +/// `FlashPageStart`, or `FlashOffset`. pub fn program>( start: A, data: &[u64], diff --git a/machine/cortex-m/src/stub/flash.rs b/machine/cortex-m/src/stub/flash.rs index 6f9acfb..6ac82cc 100644 --- a/machine/cortex-m/src/stub/flash.rs +++ b/machine/cortex-m/src/stub/flash.rs @@ -15,6 +15,9 @@ pub fn page_size() -> usize { pub fn page_count() -> usize { 0 } +pub fn write_unit_bytes() -> usize { + core::mem::size_of::() +} pub struct TestingFlash; @@ -31,6 +34,9 @@ impl hal_api::flash_addr::Flash for TestingFlash { fn page_count() -> usize { page_count() } + fn write_unit_bytes() -> usize { + write_unit_bytes() + } } pub type FlashAddress = hal_api::flash_addr::FlashAddress; diff --git a/machine/cortex-m/st/stm32l4/interface/flash.c b/machine/cortex-m/st/stm32l4/interface/flash.c index ee183f9..15a9f28 100644 --- a/machine/cortex-m/st/stm32l4/interface/flash.c +++ b/machine/cortex-m/st/stm32l4/interface/flash.c @@ -16,6 +16,20 @@ #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; } @@ -111,14 +125,16 @@ uint32_t flash_erase(uint32_t page_index, uint32_t timeout_ms) { } #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); - if (status != HAL_OK) { - return HAL_FLASH_GetError(); - } + CLEAR_BIT(FLASH->CR, FLASH_CR_PER); - return HAL_OK; + return status_to_err(status); } // Program `length` doublewords (uint64_t) at `flash_address` (an absolute @@ -140,6 +156,8 @@ uint32_t flash_program(uint32_t flash_address, const uint64_t *data, 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++) { @@ -158,7 +176,10 @@ uint32_t flash_program(uint32_t flash_address, const uint64_t *data, __set_PRIMASK(primask); if (status != HAL_OK) { - return HAL_FLASH_GetError(); + uint32_t err = status_to_err(status); + if (err != FLASH_OK) { + return err; + } } elapsed = HAL_GetTick() - tickstart; @@ -167,7 +188,10 @@ uint32_t flash_program(uint32_t flash_address, const uint64_t *data, } status = FLASH_WaitForLastOperation(timeout_ms - elapsed); if (status != HAL_OK) { - return HAL_FLASH_GetError(); + uint32_t err = status_to_err(status); + if (err != FLASH_OK) { + return err; + } } } diff --git a/src/drivers/flash.rs b/src/drivers/flash.rs index 1bc65cd..80a5876 100644 --- a/src/drivers/flash.rs +++ b/src/drivers/flash.rs @@ -6,7 +6,7 @@ pub mod raw { use crate::hal; pub use hal::flash::{ FlashAddress, FlashOffset, FlashPageStart, erase_page, flash_base, is_dual_bank, - page_count, page_size, program, read, total_size, + page_count, page_size, program, read, total_size, write_unit_bytes, }; } @@ -52,27 +52,14 @@ impl Region { }) } - /// Find the partition that contains `addr`, open it with `config`, and - /// return both the `Region` and the **partition-relative** byte offset - /// of `addr` within it. The returned `usize` is what - /// `Region::read`/`erase`/`program`/`write` expect — i.e. distance from - /// `region.start_address()`, not a `FlashOffset` (which is from - /// `FLASH_BASE`). + /// 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`. /// - /// ```ignore - /// let (slot, off) = flash::open_by_address(addr, Config::default())?; - /// slot.read(off, &mut buf)?; - /// ``` - /// - /// Accepts any `Into` (so a `FlashOffset` or - /// `FlashPageStart` works too). Returns `Err(NotFound)` if `addr` - /// doesn't fall in any declared partition. - pub fn open_by_address( - addr: impl Into, - config: Config, - ) -> Result<(Self, usize)> { - let (desc, partition_offset) = hal::flash::Region::get_by_address(addr)?; - Ok((Self { desc, config }, partition_offset)) + /// Returns `Err(NotFound)` if `addr` doesn't fall in any declared partition. + pub fn open_by_address(addr: impl Into, config: Config) -> Result { + let (desc, _) = hal::flash::Region::get_by_address(addr)?; + Ok(Self { desc, config }) } pub fn config(&self) -> Config { @@ -118,38 +105,47 @@ impl Region { if ps == 0 { 0 } else { self.len() / ps } } - /// Read `buf.len()` bytes starting at `partition_offset` (a byte offset - /// from the partition's start, not an absolute flash address). - pub fn read(&self, partition_offset: usize, buf: &mut [u8]) -> Result<()> { + 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(()); } - self.check_range(partition_offset, buf.len())?; - hal::flash::read(self.start_address().checked_add(partition_offset)?, buf) + let addr: FlashAddress = addr.into(); + self.check_range(addr, buf.len())?; + hal::flash::read(addr, buf) } - /// Erase `len` bytes starting at `partition_offset`. - /// Both `partition_offset` and `len` must be multiples of `page_size()`. - /// Returns `Err(ReadOnly)` if the DT marks this partition `read-only`. - pub fn erase(&self, partition_offset: usize, len: usize) -> Result<()> { - if len == 0 { + /// 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(Error::ReadOnly); } - self.check_range(partition_offset, len)?; let ps = self.page_size(); if ps == 0 { return Err(Error::Io); } - if partition_offset % ps != 0 || len % ps != 0 { + if start.as_usize() % ps != 0 || len_bytes % ps != 0 { return Err(Error::Misaligned); } - let first_page = FlashPageStart::new( - self.start_address().checked_add(partition_offset)?.as_usize(), - )?; - for page in first_page.iter_pages(len / ps)? { + 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, @@ -160,89 +156,136 @@ impl Region { } pub fn erase_all(&self) -> Result<()> { - self.erase(0, self.len()) + let start = FlashPageStart::try_from(self.start_address())?; + self.erase(start, self.len()) } - /// Program 64-bit doublewords at `partition_offset` (a byte offset from - /// the partition's start; must be 8-byte-aligned). The target range must + /// 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, partition_offset: usize, data: &[u64]) -> Result<()> { + /// `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(Error::ReadOnly); } - if partition_offset % core::mem::size_of::() != 0 { + let addr: FlashAddress = addr.into(); + let unit = self.write_unit_bytes(); + if unit == 0 { + return Err(Error::Io); + } + if addr.as_usize() % unit != 0 || data.len() % unit != 0 { return Err(Error::Misaligned); } - let bytes = data.len() * core::mem::size_of::(); - self.check_range(partition_offset, bytes)?; - hal::flash::program( - self.start_address().checked_add(partition_offset)?, - data, - self.config.program_timeout_ms, - self.config.lock_timeout_ms, - ) - } - - /// Erase any pages overlapping `[partition_offset, partition_offset + data.len())`, - /// then program `data` at `partition_offset` (padding the trailing 8-byte - /// chunk with `0xFF` if `data.len()` isn't a multiple of 8). 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. - pub fn write(&self, partition_offset: usize, data: &[u8]) -> Result<()> { + self.check_range(addr, data.len())?; + + // Repack bytes into the HAL's native programming type. L4 wants + // &[u64]; other STM32 HALs may want different shapes, at which point + // this needs generalization (e.g. a Flash::WriteUnit assoc type). + debug_assert_eq!( + unit, + core::mem::size_of::(), + "kernel program(&[u8]) currently only supports u64-doubleword HALs" + ); + 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(Error::ReadOnly); } - self.check_range(partition_offset, data.len())?; - if partition_offset % core::mem::size_of::() != 0 { + let addr: FlashAddress = addr.into(); + let unit = self.write_unit_bytes(); + if unit == 0 { + return Err(Error::Io); + } + if addr.as_usize() % unit != 0 { return Err(Error::Misaligned); } + self.check_range(addr, data.len())?; let ps = self.page_size(); if ps == 0 { return Err(Error::Io); } - let first_page_offset = (partition_offset / ps) * ps; - let last_page_end = partition_offset + 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(Error::OutOfBounds)?; - let erase_len = last_page_end - first_page_offset; - if first_page_offset + erase_len > self.len() { - return Err(Error::OutOfBounds); - } - self.erase(first_page_offset, erase_len)?; + let erase_len = last_page_end - first_page; + self.erase(FlashPageStart::new(first_page)?, erase_len)?; - // Program one doubleword at a time. `&[u8]` has no alignment guarantee, - // so build each u64 from explicit bytes; the trailing partial chunk is - // padded with 0xFF so the cell programs as "blank" past the data end. - let mut written = 0usize; - while written < data.len() { - let mut buf = [0xFFu8; 8]; - let take = core::cmp::min(8, data.len() - written); - buf[..take].copy_from_slice(&data[written..written + take]); - let word = u64::from_le_bytes(buf); - self.program(partition_offset + written, &[word])?; - written += 8; + // 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, partition_offset: usize, len: usize) -> Result<()> { - let end = partition_offset - .checked_add(len) + 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(Error::OutOfBounds)?; - if end > self.len() { + let start = addr.as_usize(); + if start < region_start { + return Err(Error::OutOfBounds); + } + let end = start.checked_add(len).ok_or(Error::OutOfBounds)?; + if end > region_end { return Err(Error::OutOfBounds); } Ok(()) diff --git a/src/uapi/flash.rs b/src/uapi/flash.rs index f71a248..55d13d6 100644 --- a/src/uapi/flash.rs +++ b/src/uapi/flash.rs @@ -11,12 +11,6 @@ pub fn open_by_label(label: &str, config: Config) -> Result { Region::open_by_label(label, config) } -/// Find the partition containing `addr` and open it with `config`. Returns -/// the `Region` plus the **partition-relative** byte offset of `addr` within -/// it (suitable for `Region::read`/`erase`/`program`/`write`). -pub fn open_by_address( - addr: impl Into, - config: Config, -) -> Result<(Region, usize)> { +pub fn open_by_address(addr: impl Into, config: Config) -> Result { Region::open_by_address(addr, config) } From 5d329798f91dbc185a4cdf71bc37da9f40684415 Mon Sep 17 00:00:00 2001 From: xarantolus Date: Mon, 11 May 2026 08:17:40 +0000 Subject: [PATCH 5/7] Rework with feedback --- Cargo.lock | 1 - machine/cortex-m/Cargo.toml | 1 - machine/cortex-m/build.rs | 8 ++- machine/cortex-m/src/native/flash.rs | 62 +++++++++++-------- machine/cortex-m/src/stub/flash.rs | 2 +- src/drivers/flash.rs | 89 ++++++++++++++++++---------- src/error.rs | 18 ++++++ src/uapi/flash.rs | 6 +- xtasks/crates/dtgen/src/codegen.rs | 57 +++++++++++++++--- 9 files changed, 172 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f986c8..73bb3ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -778,7 +778,6 @@ dependencies = [ "cbindgen", "cmake", "critical-section", - "dtgen", "hal-api", "hal-builder", "quote", diff --git a/machine/cortex-m/Cargo.toml b/machine/cortex-m/Cargo.toml index d5e3979..bcde991 100644 --- a/machine/cortex-m/Cargo.toml +++ b/machine/cortex-m/Cargo.toml @@ -22,7 +22,6 @@ serde_json = "1.0.145" quote = "1.0.26" syn = { version = "2.0.36", features = ["full"] } regex = "1.11" -dtgen = { workspace = true } [features] panic-exit = [] diff --git a/machine/cortex-m/build.rs b/machine/cortex-m/build.rs index 0d75ba3..5f6e88e 100644 --- a/machine/cortex-m/build.rs +++ b/machine/cortex-m/build.rs @@ -362,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, &hal_builder::dt::soc(&dt))); + 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) { @@ -383,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/flash.rs b/machine/cortex-m/src/native/flash.rs index cc40b97..87d907b 100644 --- a/machine/cortex-m/src/native/flash.rs +++ b/machine/cortex-m/src/native/flash.rs @@ -167,7 +167,10 @@ pub fn program>( .as_usize() .checked_add(bytes) .ok_or(Error::OutOfBounds)?; - if end > flash_base() + total_size() { + let flash_end = flash_base() + .checked_add(total_size()) + .ok_or(Error::OutOfBounds)?; + if end > flash_end { return Err(Error::OutOfBounds); } @@ -198,7 +201,10 @@ pub fn read>(start: A, buf: &mut [u8]) -> Result<()> { .as_usize() .checked_add(buf.len()) .ok_or(Error::OutOfBounds)?; - if end > flash_base() + total_size() { + let flash_end = flash_base() + .checked_add(total_size()) + .ok_or(Error::OutOfBounds)?; + if end > flash_end { return Err(Error::OutOfBounds); } unsafe { @@ -218,33 +224,41 @@ pub fn read>(start: A, buf: &mut [u8]) -> Result<()> { 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 { - device_tree::flash_partition_by_compatible(compatible, ordinal) - .map(Self) - .ok_or(Error::NotFound) + 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 { - device_tree::flash_partition_by_label(label) - .map(Self) - .ok_or(Error::NotFound) - } - - /// Find the partition that contains `addr` and return it together with - /// the byte offset of `addr` from that partition's start. - /// - /// The returned `usize` is **partition-relative**, not a `FlashOffset` - /// (which is from `FLASH_BASE`) — it can be passed straight to the - /// driver-layer `Region::read`/`erase`/`program`/`write` methods. - /// - /// Accepts any `Into`, so a `FlashOffset` or - /// `FlashPageStart` works too. Returns `Err(NotFound)` if `addr` doesn't - /// fall in any declared partition. - pub fn get_by_address(addr: impl Into) -> Result<(Self, usize)> { + 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(); - device_tree::flash_partition_by_address(addr.as_usize()) - .map(|(entry, offset)| (Self(entry), offset)) - .ok_or(Error::NotFound) + 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 { diff --git a/machine/cortex-m/src/stub/flash.rs b/machine/cortex-m/src/stub/flash.rs index 6ac82cc..6ab55dd 100644 --- a/machine/cortex-m/src/stub/flash.rs +++ b/machine/cortex-m/src/stub/flash.rs @@ -74,7 +74,7 @@ impl Region { pub fn get_by_label(_label: &str) -> Result { Err(Error::NotFound) } - pub fn get_by_address(_addr: impl Into) -> Result<(Self, usize)> { + pub fn get_by_address(_addr: impl Into) -> Result { Err(Error::NotFound) } pub fn label(&self) -> &'static str { diff --git a/src/drivers/flash.rs b/src/drivers/flash.rs index 80a5876..72e488d 100644 --- a/src/drivers/flash.rs +++ b/src/drivers/flash.rs @@ -1,12 +1,17 @@ +use crate::error::Result; use crate::hal; -pub use hal::flash::{Error, FlashAddress, FlashOffset, FlashPageStart, Result}; +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::{ - FlashAddress, FlashOffset, FlashPageStart, erase_page, flash_base, is_dual_bank, - page_count, page_size, program, read, total_size, write_unit_bytes, + Error, FlashAddress, FlashOffset, FlashPageStart, Result, erase_page, flash_base, + is_dual_bank, page_count, page_size, program, read, total_size, write_unit_bytes, }; } @@ -58,8 +63,10 @@ impl Region { /// /// Returns `Err(NotFound)` if `addr` doesn't fall in any declared partition. pub fn open_by_address(addr: impl Into, config: Config) -> Result { - let (desc, _) = hal::flash::Region::get_by_address(addr)?; - Ok(Self { desc, config }) + Ok(Self { + desc: hal::flash::Region::get_by_address(addr)?, + config, + }) } pub fn config(&self) -> Config { @@ -119,7 +126,8 @@ impl Region { } let addr: FlashAddress = addr.into(); self.check_range(addr, buf.len())?; - hal::flash::read(addr, buf) + hal::flash::read(addr, buf)?; + Ok(()) } /// Erase `len_bytes` bytes starting at `start`. `len_bytes` must be a @@ -135,14 +143,17 @@ impl Region { return Ok(()); } if self.read_only() { - return Err(Error::ReadOnly); + return Err(kerr!(EROFS, "flash partition is read-only; erase refused")); } let ps = self.page_size(); if ps == 0 { - return Err(Error::Io); + return Err(kerr!(EIO, "flash HAL reported page_size == 0")); } if start.as_usize() % ps != 0 || len_bytes % ps != 0 { - return Err(Error::Misaligned); + 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)? { @@ -169,26 +180,30 @@ impl Region { return Ok(()); } if self.read_only() { - return Err(Error::ReadOnly); + return Err(kerr!( + EROFS, + "flash partition is read-only; program refused" + )); } let addr: FlashAddress = addr.into(); let unit = self.write_unit_bytes(); - if unit == 0 { - return Err(Error::Io); + 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(Error::Misaligned); + return Err(kerr!( + EINVAL, + "flash program: address or length not write-unit-aligned" + )); } self.check_range(addr, data.len())?; - // Repack bytes into the HAL's native programming type. L4 wants - // &[u64]; other STM32 HALs may want different shapes, at which point - // this needs generalization (e.g. a Flash::WriteUnit assoc type). - debug_assert_eq!( - unit, - core::mem::size_of::(), - "kernel program(&[u8]) currently only supports u64-doubleword HALs" - ); const BATCH: usize = 32; let mut buf = [0u64; BATCH]; let mut written = 0; @@ -233,20 +248,25 @@ impl Region { return Ok(()); } if self.read_only() { - return Err(Error::ReadOnly); + return Err(kerr!(EROFS, "flash partition is read-only; write refused")); } let addr: FlashAddress = addr.into(); let unit = self.write_unit_bytes(); - if unit == 0 { - return Err(Error::Io); + 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(Error::Misaligned); + 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(Error::Io); + return Err(kerr!(EIO, "flash HAL reported page_size == 0")); } let addr_u = addr.as_usize(); @@ -255,7 +275,12 @@ impl Region { .checked_add(data.len()) .and_then(|end| end.checked_add(ps - 1)) .map(|x| (x / ps) * ps) - .ok_or(Error::OutOfBounds)?; + .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)?; @@ -279,14 +304,16 @@ impl Region { let region_start = self.start_address().as_usize(); let region_end = region_start .checked_add(self.len()) - .ok_or(Error::OutOfBounds)?; + .ok_or_else(|| kerr!(ERANGE, "flash region end overflows usize"))?; let start = addr.as_usize(); if start < region_start { - return Err(Error::OutOfBounds); + return Err(kerr!(ERANGE, "flash access starts before region")); } - let end = start.checked_add(len).ok_or(Error::OutOfBounds)?; + let end = start + .checked_add(len) + .ok_or_else(|| kerr!(ERANGE, "flash access end overflows usize"))?; if end > region_end { - return Err(Error::OutOfBounds); + return Err(kerr!(ERANGE, "flash access extends past region end")); } Ok(()) } diff --git a/src/error.rs b/src/error.rs index ca035a0..98f97d7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -195,6 +195,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/flash.rs b/src/uapi/flash.rs index 55d13d6..c0902bc 100644 --- a/src/uapi/flash.rs +++ b/src/uapi/flash.rs @@ -1,7 +1,7 @@ +use crate::error::Result; + pub use crate::drivers::flash::raw; -pub use crate::drivers::flash::{ - Config, Error, FlashAddress, FlashOffset, FlashPageStart, Region, Result, -}; +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) diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index 08c2c72..fd2233f 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -1394,9 +1394,9 @@ mod flash { read_only: bool, } - /// Returns (base, size) of the first `flash@*` node found in the tree, if any. - /// All partitions are assumed to live in this flash; multi-flash systems - /// would need an extension here. + /// 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@") { @@ -1413,15 +1413,14 @@ mod flash { None } - fn collect(dt: &DeviceTree) -> Vec { + 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; } - // We don't need the flash base here — it's emitted as a top-level - // FLASH_BASE constant; partitions store offsets relative to it. + 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 { @@ -1440,6 +1439,14 @@ mod flash { 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", @@ -1492,11 +1499,45 @@ mod flash { 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 parts = collect(dt); - let (flash_base, flash_total_size) = find_primary_flash(dt) + 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; From 6b2436f4109eba331b7ccc1bedca19a22a14dac6 Mon Sep 17 00:00:00 2001 From: xarantolus Date: Mon, 11 May 2026 08:57:16 +0000 Subject: [PATCH 6/7] Formatting --- xtasks/crates/dtgen/src/codegen/flash.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xtasks/crates/dtgen/src/codegen/flash.rs b/xtasks/crates/dtgen/src/codegen/flash.rs index d9bdf40..9735af4 100644 --- a/xtasks/crates/dtgen/src/codegen/flash.rs +++ b/xtasks/crates/dtgen/src/codegen/flash.rs @@ -73,8 +73,7 @@ fn collect(dt: &DeviceTree, primary_idx: Option) -> Vec { ); continue; }; - let (Ok(offset), Ok(len)) = - (usize::try_from(offset_u64), usize::try_from(len_u64)) + 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", From 4f2909fb74bbf55035c52e7e9b7082ee8146687f Mon Sep 17 00:00:00 2001 From: xarantolus Date: Tue, 12 May 2026 14:16:19 +0000 Subject: [PATCH 7/7] Formatting --- src/drivers.rs | 2 +- src/uapi.rs | 2 +- xtasks/crates/dtgen/src/codegen.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/drivers.rs b/src/drivers.rs index e458bde..da2147c 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -1,5 +1,5 @@ -pub mod flash; pub mod can; +pub mod flash; pub mod i2c; pub mod spi; diff --git a/src/uapi.rs b/src/uapi.rs index 0e8899e..8179fda 100644 --- a/src/uapi.rs +++ b/src/uapi.rs @@ -1,5 +1,5 @@ -pub mod flash; pub mod can; +pub mod flash; pub mod i2c; pub mod print; pub mod sched; diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index b0b2dae..07ee75b 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -2,8 +2,8 @@ use crate::ir::{DeviceTree, PropValue}; use proc_macro2::TokenStream; use quote::quote; -mod flash; mod can; +mod flash; mod i2c; mod spi;