diff --git a/esp-storage/CHANGELOG.md b/esp-storage/CHANGELOG.md index d1363c0af..037a23ec0 100644 --- a/esp-storage/CHANGELOG.md +++ b/esp-storage/CHANGELOG.md @@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [v0.8.0] - 2025-09-10 + +### Added + +- Added strategies for dealing with multi-core systems (#4082) ## [v0.7.0] - 2025-07-16 diff --git a/esp-storage/Cargo.toml b/esp-storage/Cargo.toml index fcaff193c..500eac74d 100644 --- a/esp-storage/Cargo.toml +++ b/esp-storage/Cargo.toml @@ -20,6 +20,7 @@ bench = false [dependencies] embedded-storage = "0.3.1" procmacros = { version = "0.19.0", package = "esp-hal-procmacros", path = "../esp-hal-procmacros" } +cfg-if = "1.0.0" # Optional dependencies esp-sync = { version = "0.0.0", path = "../esp-sync", optional = true } @@ -29,6 +30,15 @@ defmt = { version = "1.0.1", optional = true } # Unstable dependencies that are not (strictly) part of the public API document-features = "0.2.11" +[target.'cfg(target_arch = "riscv32")'.dependencies] +riscv = { version = "0.14.0", optional = true} + +[target.'cfg(target_arch = "xtensa")'.dependencies] +xtensa-lx = { version = "0.12.0", path = "../xtensa-lx", optional = true } + +[build-dependencies] +esp-metadata-generated = { version = "0.1.0", path = "../esp-metadata-generated", features = ["build-script"] } + [features] default = ["critical-section"] @@ -53,10 +63,10 @@ esp32c6 = ["esp-rom-sys/esp32c6", "esp-sync/esp32c6"] ## esp32h2 = ["esp-rom-sys/esp32h2", "esp-sync/esp32h2"] ## -esp32 = ["esp-rom-sys/esp32", "esp-sync/esp32"] +esp32 = ["esp-rom-sys/esp32", "esp-sync/esp32", "xtensa-lx"] ## esp32s2 = ["esp-rom-sys/esp32s2", "esp-sync/esp32s2"] ## -esp32s3 = ["esp-rom-sys/esp32s3", "esp-sync/esp32s3"] +esp32s3 = ["esp-rom-sys/esp32s3", "esp-sync/esp32s3", "xtensa-lx"] ## Used for testing on a host. emulation = [] diff --git a/esp-storage/build.rs b/esp-storage/build.rs index 04dc6f562..aaab88eaf 100644 --- a/esp-storage/build.rs +++ b/esp-storage/build.rs @@ -1,4 +1,13 @@ +use esp_metadata_generated::Chip; + fn main() -> Result<(), String> { + if !cfg!(feature = "emulation") { + // Load the configuration file for the configured device: + let chip = Chip::from_cargo_feature()?; + + // Define all necessary configuration symbols for the configured device: + chip.define_cfgs(); + } if cfg!(feature = "esp32") { match std::env::var("OPT_LEVEL") { Ok(level) if std::env::var("CI").is_err() => { diff --git a/esp-storage/src/common.rs b/esp-storage/src/common.rs index fdc0bdd95..f15790a66 100644 --- a/esp-storage/src/common.rs +++ b/esp-storage/src/common.rs @@ -1,6 +1,8 @@ use core::mem::MaybeUninit; use crate::chip_specific; +#[cfg(multi_core)] +use crate::multi_core::MultiCoreStrategy; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] @@ -17,6 +19,12 @@ pub enum FlashStorageError { NotAligned, /// Address or length out of bounds. OutOfBounds, + /// Cannot write to flash as more than one core is running. + /// Either manually suspend the other core, or use one of the available strategies: + /// * [`FlashStorage::multicore_auto_park`] + /// * [`FlashStorage::multicore_ignore`] + #[cfg(multi_core)] + OtherCoreRunning, /// Other error with the given error code. Other(i32), } @@ -38,6 +46,8 @@ pub fn check_rc(rc: i32) -> Result<(), FlashStorageError> { pub struct FlashStorage { pub(crate) capacity: usize, unlocked: bool, + #[cfg(multi_core)] + pub(crate) multi_core_strategy: MultiCoreStrategy, } impl Default for FlashStorage { @@ -57,6 +67,8 @@ impl FlashStorage { let mut storage = FlashStorage { capacity: 0, unlocked: false, + #[cfg(multi_core)] + multi_core_strategy: MultiCoreStrategy::Error, }; #[cfg(not(any(feature = "esp32", feature = "esp32s2")))] @@ -126,9 +138,16 @@ impl FlashStorage { } pub(crate) fn internal_erase(&mut self, sector: u32) -> Result<(), FlashStorageError> { - self.unlock_once()?; + #[cfg(multi_core)] + let unpark = self.multi_core_strategy.pre_write()?; - check_rc(chip_specific::spiflash_erase_sector(sector)) + self.unlock_once()?; + check_rc(chip_specific::spiflash_erase_sector(sector))?; + + #[cfg(multi_core)] + self.multi_core_strategy.post_write(unpark); + + Ok(()) } pub(crate) fn internal_write( @@ -136,12 +155,19 @@ impl FlashStorage { offset: u32, bytes: &[u8], ) -> Result<(), FlashStorageError> { - self.unlock_once()?; + #[cfg(multi_core)] + let unpark = self.multi_core_strategy.pre_write()?; + self.unlock_once()?; check_rc(chip_specific::spiflash_write( offset, bytes.as_ptr() as *const u32, bytes.len() as u32, - )) + ))?; + + #[cfg(multi_core)] + self.multi_core_strategy.post_write(unpark); + + Ok(()) } } diff --git a/esp-storage/src/lib.rs b/esp-storage/src/lib.rs index c7c057cf3..e4208d616 100644 --- a/esp-storage/src/lib.rs +++ b/esp-storage/src/lib.rs @@ -9,6 +9,9 @@ mod chip_specific; mod buffer; mod common; +#[cfg(multi_core)] +mod multi_core; + pub use common::{FlashStorage, FlashStorageError}; pub mod ll; diff --git a/esp-storage/src/multi_core.rs b/esp-storage/src/multi_core.rs new file mode 100644 index 000000000..78c5973e7 --- /dev/null +++ b/esp-storage/src/multi_core.rs @@ -0,0 +1,220 @@ +//! Only available/needed on multi-core systems like the ESP32-S3 and ESP32 + +use crate::{FlashStorage, common::FlashStorageError}; + +#[cfg(esp32)] +mod registers { + pub(crate) const OPTIONS0: u32 = 0x3ff4_8000; + pub(crate) const SW_CPU_STALL: u32 = 0x3ff4_80ac; + pub(crate) const APPCPU_CTRL_B: u32 = 0x3FF0_0030; + pub(crate) const APPCPU_CTRL_C: u32 = 0x3FF0_0034; +} + +#[cfg(esp32s3)] +mod registers { + pub(crate) const OPTIONS0: u32 = 0x6000_8000; + pub(crate) const SW_CPU_STALL: u32 = 0x6000_80bc; + pub(crate) const CORE_1_CONTROL_0: u32 = 0x600c_0000; +} + +const C0_VALUE_PRO: u32 = 0x02 << 2; +const C1_VALUE_PRO: u32 = 0x21 << 26; +const C0_VALUE_APP: u32 = 0x02; +const C1_VALUE_APP: u32 = 0x21 << 20; + +/// Strategy to use on a multi core system where writing to the flash needs exclusive access from +/// one core +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) enum MultiCoreStrategy { + /// Flash writes simply fail if the second core is active while attempting a write (default + /// behavior) + Error, + /// Auto park the other core before writing. Un-park it when writing is complete + AutoPark, + /// Ignore that the other core is active. + /// This is useful if the second core is known to not fetch instructions from the flash for the + /// duration of the write. This is unsafe to use. + Ignore, +} + +impl FlashStorage { + /// Enable auto parking of the second core before writing to flash + /// The other core will be automatically un-parked when the write is complete + pub fn multicore_auto_park(mut self) -> FlashStorage { + self.multi_core_strategy = MultiCoreStrategy::AutoPark; + self + } + + /// Do not check if the second core is active before writing to flash. + /// + /// # Safety + /// Only enable this if you are sure that the second core is not fetching instructions from the + /// flash during the write + pub unsafe fn multicore_ignore(mut self) -> FlashStorage { + self.multi_core_strategy = MultiCoreStrategy::Ignore; + self + } +} + +#[inline(always)] +fn raw_core() -> usize { + // This method must never return UNUSED_THREAD_ID_VALUE + cfg_if::cfg_if! { + if #[cfg(all(multi_core, riscv))] { + riscv::register::mhartid::read() + } else if #[cfg(all(multi_core, xtensa))] { + (xtensa_lx::get_processor_id() & 0x2000) as usize + } else { + 0 + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Cpu { + /// The first core + ProCpu = 0, + /// The second core + AppCpu = 1, +} + +impl Cpu { + /// Returns the core other than the one which this function is called on + #[inline(always)] + fn other() -> Cpu { + // This works for both RISCV and Xtensa because both + // get_raw_core functions return zero, _or_ something + // greater than zero; 1 in the case of RISCV and 0x2000 + // in the case of Xtensa. + match raw_core() { + 0 => Cpu::AppCpu, + #[cfg(all(multi_core, riscv))] + 1 => Cpu::ProCpu, + #[cfg(all(multi_core, xtensa))] + 0x2000 => Cpu::ProCpu, + _ => unreachable!(), + } + } + + #[inline(always)] + fn get_c0_c1_bits(self) -> (u32, u32) { + match self { + Cpu::ProCpu => (C0_VALUE_PRO, C1_VALUE_PRO), + Cpu::AppCpu => (C0_VALUE_APP, C1_VALUE_APP), + } + } + + /// Park or un-park the core + #[inline(always)] + fn park_core(self, park: bool) { + let sw_cpu_stall = registers::SW_CPU_STALL as *mut u32; + let options0 = registers::OPTIONS0 as *mut u32; + + // Write 0x2 to options0 to stall a particular core + // offset for app cpu: 0 + // offset for pro cpu: 2 + + // Write 0x21 to sw_cpu_stall to allow stalling of a particular core + // offset for app cpu: 20 + // offset for pro cpu: 26 + + let (c0_bits, c1_bits) = self.get_c0_c1_bits(); + unsafe { + let mut c0 = options0.read_volatile() & !(c0_bits); + let mut c1 = sw_cpu_stall.read_volatile() & !(c1_bits); + if park { + c0 |= c0_bits; + c1 |= c1_bits; + } + sw_cpu_stall.write_volatile(c1); + options0.write_volatile(c0); + } + } + + /// Returns true if the core is running + #[inline(always)] + fn is_running(&self) -> bool { + // If the core is the app cpu we need to check first if it was even enabled + if let Cpu::AppCpu = *self { + cfg_if::cfg_if! { + if #[cfg(esp32s3)] { + // CORE_1_RUNSTALL in bit 0 -> needs to be 0 to not stall + // CORE_1_CLKGATE_EN in bit 1 -> needs to be 1 to even be enabled + let core_1_control_0 = registers::CORE_1_CONTROL_0 as *mut u32; + if unsafe { core_1_control_0.read_volatile() } & 0x03 != 0x02 { + // If the core is not enabled we can take this shortcut + return false; + } + } else if #[cfg(esp32)] { + // DPORT_APPCPU_CLKGATE_EN in APPCPU_CTRL_B bit 0 -> needs to be 1 to even be enabled + // DPORT_APPCPU_RUNSTALL in APPCPU_CTRL_C bit 0 -> needs to be 0 to not stall + let appcpu_ctrl_b = registers::APPCPU_CTRL_B as *mut u32; + if unsafe { appcpu_ctrl_b.read_volatile() } & 0x01 != 0x01 { + // If the core is not enabled we can take this shortcut + return false; + } + let appcpu_ctrl_c = registers::APPCPU_CTRL_C as *mut u32; + if unsafe { appcpu_ctrl_c.read_volatile() } & 0x01 == 0x01 { + // If the core is stalled we can take this shortcut + return false; + } + } + } + } + + let sw_cpu_stall = registers::SW_CPU_STALL as *mut u32; + let options0 = registers::OPTIONS0 as *mut u32; + + let (c0_bits, c1_bits) = self.get_c0_c1_bits(); + let (c0, c1) = unsafe { + ( + options0.read_volatile() & c0_bits, + sw_cpu_stall.read_volatile() & c1_bits, + ) + }; + !(c0 == c0_bits && c1 == c1_bits) + } +} + +impl MultiCoreStrategy { + /// Perform checks/Prepare for a flash write according to the current strategy + /// + /// # Returns + /// * `True` if the other core needs to be un-parked by post_write + /// * `False` otherwise + pub(crate) fn pre_write(&self) -> Result { + match self { + MultiCoreStrategy::Error => { + if Cpu::other().is_running() { + Err(FlashStorageError::OtherCoreRunning) + } else { + Ok(false) + } + } + MultiCoreStrategy::AutoPark => { + let other_cpu = Cpu::other(); + if other_cpu.is_running() { + other_cpu.park_core(true); + Ok(true) + } else { + Ok(false) + } + } + MultiCoreStrategy::Ignore => Ok(false), + } + } + + /// Perform post-write actions + /// + /// # Returns + /// * `True` if the other core needs to be un-parked by post_write + /// * `False` otherwise + pub(crate) fn post_write(&self, unpark: bool) { + if let MultiCoreStrategy::AutoPark = self { + if unpark { + Cpu::other().park_core(false); + } + } + } +}