Esp storage implement multi core strategies (#4082)

* First draft of multicore strategies implementation

* Implemented second core active detection mechanism
Removed dependency to esp-hal

* Guarded flash unlock as well
Made esp_metadata_generated only check for chips if not building with "emulation"
Added more documentation

* Applied suggestions

* Restored second core active detection for esp32

* Flipped check of stall condition on esp32

* Implement defmt::Format and common traits

---------

Co-authored-by: Dániel Buga <bugadani@gmail.com>
This commit is contained in:
florianL21 2025-09-17 15:00:05 +02:00 committed by GitHub
parent 555922b887
commit acf3327fa3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 279 additions and 6 deletions

View File

@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
## [v0.8.0] - 2025-09-10
### Added
- Added strategies for dealing with multi-core systems (#4082)
## [v0.7.0] - 2025-07-16 ## [v0.7.0] - 2025-07-16

View File

@ -20,6 +20,7 @@ bench = false
[dependencies] [dependencies]
embedded-storage = "0.3.1" embedded-storage = "0.3.1"
procmacros = { version = "0.19.0", package = "esp-hal-procmacros", path = "../esp-hal-procmacros" } procmacros = { version = "0.19.0", package = "esp-hal-procmacros", path = "../esp-hal-procmacros" }
cfg-if = "1.0.0"
# Optional dependencies # Optional dependencies
esp-sync = { version = "0.0.0", path = "../esp-sync", optional = true } 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 # Unstable dependencies that are not (strictly) part of the public API
document-features = "0.2.11" 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] [features]
default = ["critical-section"] default = ["critical-section"]
@ -53,10 +63,10 @@ esp32c6 = ["esp-rom-sys/esp32c6", "esp-sync/esp32c6"]
## ##
esp32h2 = ["esp-rom-sys/esp32h2", "esp-sync/esp32h2"] 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"] 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. ## Used for testing on a host.
emulation = [] emulation = []

View File

@ -1,4 +1,13 @@
use esp_metadata_generated::Chip;
fn main() -> Result<(), String> { 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") { if cfg!(feature = "esp32") {
match std::env::var("OPT_LEVEL") { match std::env::var("OPT_LEVEL") {
Ok(level) if std::env::var("CI").is_err() => { Ok(level) if std::env::var("CI").is_err() => {

View File

@ -1,6 +1,8 @@
use core::mem::MaybeUninit; use core::mem::MaybeUninit;
use crate::chip_specific; use crate::chip_specific;
#[cfg(multi_core)]
use crate::multi_core::MultiCoreStrategy;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
@ -17,6 +19,12 @@ pub enum FlashStorageError {
NotAligned, NotAligned,
/// Address or length out of bounds. /// Address or length out of bounds.
OutOfBounds, 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 error with the given error code.
Other(i32), Other(i32),
} }
@ -38,6 +46,8 @@ pub fn check_rc(rc: i32) -> Result<(), FlashStorageError> {
pub struct FlashStorage { pub struct FlashStorage {
pub(crate) capacity: usize, pub(crate) capacity: usize,
unlocked: bool, unlocked: bool,
#[cfg(multi_core)]
pub(crate) multi_core_strategy: MultiCoreStrategy,
} }
impl Default for FlashStorage { impl Default for FlashStorage {
@ -57,6 +67,8 @@ impl FlashStorage {
let mut storage = FlashStorage { let mut storage = FlashStorage {
capacity: 0, capacity: 0,
unlocked: false, unlocked: false,
#[cfg(multi_core)]
multi_core_strategy: MultiCoreStrategy::Error,
}; };
#[cfg(not(any(feature = "esp32", feature = "esp32s2")))] #[cfg(not(any(feature = "esp32", feature = "esp32s2")))]
@ -126,9 +138,16 @@ impl FlashStorage {
} }
pub(crate) fn internal_erase(&mut self, sector: u32) -> Result<(), FlashStorageError> { 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( pub(crate) fn internal_write(
@ -136,12 +155,19 @@ impl FlashStorage {
offset: u32, offset: u32,
bytes: &[u8], bytes: &[u8],
) -> Result<(), FlashStorageError> { ) -> 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( check_rc(chip_specific::spiflash_write(
offset, offset,
bytes.as_ptr() as *const u32, bytes.as_ptr() as *const u32,
bytes.len() as u32, bytes.len() as u32,
)) ))?;
#[cfg(multi_core)]
self.multi_core_strategy.post_write(unpark);
Ok(())
} }
} }

View File

@ -9,6 +9,9 @@ mod chip_specific;
mod buffer; mod buffer;
mod common; mod common;
#[cfg(multi_core)]
mod multi_core;
pub use common::{FlashStorage, FlashStorageError}; pub use common::{FlashStorage, FlashStorageError};
pub mod ll; pub mod ll;

View File

@ -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<bool, FlashStorageError> {
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);
}
}
}
}