m5stack_core/board/multicore.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Second-core (APP CPU) bring-up harness (#35 C4).
3//!
4//! Every binary hand-rolls the same dance to put work on the APP core: build a
5//! [`CpuControl`], defensively park the APP core (a JTAG-reset workaround),
6//! create an esp-rtos [`InterruptExecutor`], start the core on a `waiti` idle
7//! loop, and start the executor at a priority. [`run_app_core`] encapsulates all
8//! of it; the binary supplies only the stack, the priority, and a closure that
9//! spawns its APP-core tasks from the [`SendSpawner`].
10
11extern crate alloc;
12
13use embassy_executor::SendSpawner;
14use esp_hal::interrupt::Priority;
15use esp_hal::interrupt::software::SoftwareInterrupt;
16use esp_hal::peripherals::CPU_CTRL;
17use esp_hal::system::{AppCoreGuard, Cpu, CpuControl, Stack};
18use esp_rtos::embassy::InterruptExecutor;
19
20/// Park + start the APP core running an [`InterruptExecutor`] at `prio`; `init`
21/// runs on the APP core with the executor's [`SendSpawner`] to spawn its tasks.
22///
23/// Encapsulates the defensive `park_core(AppCpu)` reset — probe-rs's JTAG reset
24/// doesn't always return the APP-core control registers to power-on state, so a
25/// JTAG-flashed boot can find the core reported "running" and `start_app_core`
26/// panics with `CoreAlreadyRunning`; parking clears that, and `start_app_core`
27/// unparks before the core runs. (Documented perf tradeoff on CoreS3: the
28/// disturbance measurably slows LCD render — accepted for boot reliability.)
29///
30/// Call from the PRO core (main). The returned [`AppCoreGuard`] must be kept
31/// alive for the program's lifetime — dropping it stops the APP core.
32///
33/// `sw_int` is one of `SystemResources::sw_int.software_interruptN`; `stack` is
34/// a `&'static mut Stack<N>` the caller allocates (e.g. `mk_static!`/`StaticCell`).
35pub fn run_app_core<const N: usize, const SWI: u8, F>(
36 cpu_ctrl: CPU_CTRL<'static>,
37 sw_int: SoftwareInterrupt<'static, SWI>,
38 stack: &'static mut Stack<N>,
39 prio: Priority,
40 init: F,
41) -> AppCoreGuard<'static>
42where
43 F: FnOnce(SendSpawner) + Send + 'static,
44{
45 let mut cpu = CpuControl::new(cpu_ctrl);
46 // SAFETY: we run on the PRO core here, never the APP core being parked.
47 unsafe { cpu.park_core(Cpu::AppCpu) };
48
49 // The executor must be `'static` (its `SendSpawner` borrows it forever).
50 // `InterruptExecutor<SWI>` is generic over the interrupt number, so a
51 // `static`/`make_static!` (whose type can't depend on a fn generic) won't do
52 // — leak a single heap allocation instead (one-time, lives for the program).
53 let exec: &'static mut InterruptExecutor<SWI> =
54 alloc::boxed::Box::leak(alloc::boxed::Box::new(InterruptExecutor::new(sw_int)));
55
56 cpu.start_app_core(stack, move || {
57 let spawner = exec.start(prio);
58 init(spawner);
59 // The closure must never return — the executor lives on this frame.
60 loop {
61 // Xtensa wait-for-interrupt; the InterruptExecutor wakes on its IRQ.
62 unsafe { core::arch::asm!("waiti 0") };
63 }
64 })
65 .expect("start_app_core")
66}