#[cfg(feature = "esp-radio")]
use core::ffi::c_void;
use esp_hal::{
interrupt::{self, software::SoftwareInterrupt},
system::Cpu,
};
#[cfg(feature = "rtos-trace")]
use crate::TraceEvents;
use crate::{
SCHEDULER,
task::{IdleFn, Task},
};
#[derive(Debug, Default, Clone)]
#[repr(C)]
pub struct CpuContext {
pub ra: usize,
pub t0: usize,
pub t1: usize,
pub t2: usize,
pub t3: usize,
pub t4: usize,
pub t5: usize,
pub t6: usize,
pub a0: usize,
pub a1: usize,
pub a2: usize,
pub a3: usize,
pub a4: usize,
pub a5: usize,
pub a6: usize,
pub a7: usize,
pub s0: usize,
pub s1: usize,
pub s2: usize,
pub s3: usize,
pub s4: usize,
pub s5: usize,
pub s6: usize,
pub s7: usize,
pub s8: usize,
pub s9: usize,
pub s10: usize,
pub s11: usize,
pub gp: usize,
pub tp: usize,
pub sp: usize,
pub pc: usize,
}
impl CpuContext {
pub const fn new() -> Self {
unsafe { core::mem::zeroed() }
}
}
pub(crate) fn set_idle_hook_entry(idle_context: &mut CpuContext, hook_fn: IdleFn) {
idle_context.pc = hook_fn as usize;
idle_context.tp = 0;
}
#[inline(always)]
pub(crate) fn read_thread_pointer() -> *mut Task {
let tp: *mut Task;
unsafe { core::arch::asm!("c.mv {0}, tp", out(reg) tp, options(nostack)) };
tp
}
#[inline(always)]
pub(crate) fn write_thread_pointer(task: *mut Task) {
unsafe { core::arch::asm!("c.mv tp, {0}", in(reg) task, options(nostack)) };
}
#[cfg(feature = "esp-radio")]
pub(crate) fn new_task_context(
task: extern "C" fn(*mut c_void),
param: *mut c_void,
stack_top: *mut (),
) -> CpuContext {
let stack_top = stack_top as usize;
let stack_top = stack_top - (stack_top % 16);
CpuContext {
pc: super::task_wrapper as *const () as usize,
a0: task as usize,
a1: param as usize,
sp: stack_top,
..Default::default()
}
}
pub fn task_switch(_old_ctx: *mut CpuContext, new_ctx: *mut CpuContext) {
unsafe {
core::arch::asm!("mv tp, {}", in(reg) new_ctx as *const CpuContext as usize);
}
}
pub(crate) fn setup_multitasking<const IRQ: u8>(_irq: SoftwareInterrupt<'static, IRQ>) {
let interrupt = match IRQ {
0 => esp_hal::peripherals::Interrupt::FROM_CPU_INTR0,
_ => panic!("Invalid IRQ number"),
};
interrupt::enable_direct(
interrupt,
interrupt::Priority::min(),
interrupt::DirectBindableCpuInterrupt::Interrupt0,
swint_handler_trampoline,
);
}
#[cfg(multi_core)]
pub(crate) fn setup_smp<const IRQ: u8>(irq: SoftwareInterrupt<'static, IRQ>) {
setup_multitasking(irq);
}
#[unsafe(link_section = ".trap.rust")]
#[unsafe(no_mangle)]
#[unsafe(naked)]
#[rustfmt::skip]
unsafe extern "C" fn swint_handler_trampoline() {
core::arch::naked_asm! {"
.cfi_startproc
# https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/139d8d8e1d8ee8c0c3ee150de709ceaab5c08417/riscv-dwarf.adoc
# .cfi_register ra, 0x1341 # Unwind with MEPC as return address, crashes probe-rs
# Save registers
addi sp, sp, -16 # allocate 16 bytes for saving regs (RISC-V requires 16-byte alignment)
# Store the thread pointer on the stack. We'll use it to check what needs to be restored
sw tp, 0*4(sp)
# Skip storing context for the idle context or deleted tasks (no thread pointer)
beqz tp, 1f # Skip to calling the interrupt handler
sw ra, 0*4(tp)
sw t0, 1*4(tp)
sw t1, 2*4(tp)
sw t2, 3*4(tp)
sw t3, 4*4(tp)
sw t4, 5*4(tp)
sw t5, 6*4(tp)
sw t6, 7*4(tp)
sw a0, 8*4(tp)
sw a1, 9*4(tp)
sw a2, 10*4(tp)
sw a3, 11*4(tp)
sw a4, 12*4(tp)
sw a5, 13*4(tp)
sw a6, 14*4(tp)
sw a7, 15*4(tp)
1:
# Let's run the interrupt handler, which runs the scheduler. If the scheduler
# decides we need to switch context, it will change the thread pointer to the new context.
la t0, {scheduler_interrupt_handler}
jalr ra, t0, 0
# Load old thread pointer and free up stack. This way we store/reload the unmodified stack pointer.
lw t0, 0*4(sp)
addi sp, sp, 16
# If the thread pointer has not changed, just restore caller-saved registers
beq t0, tp, 3f # Skip to restoring caller-saved registers in the new context
# Skip storing context for the idle context or deleted tasks (no thread pointer)
beqz t0, 2f # Skip to loading registers for the new context
# If the thread pointer has changed, switch context
# First, save registers to the old context
sw s0, 16*4(t0)
sw s1, 17*4(t0)
sw s2, 18*4(t0)
sw s3, 19*4(t0)
sw s4, 20*4(t0)
sw s5, 21*4(t0)
sw s6, 22*4(t0)
sw s7, 23*4(t0)
sw s8, 24*4(t0)
sw s9, 25*4(t0)
sw s10, 26*4(t0)
sw s11, 27*4(t0)
sw gp, 28*4(t0)
# sw tp, 29*4(t0) # No need to save TP, it's set up when the task is created.
sw sp, 30*4(t0)
# mepc -> pc
csrr t1, mepc
sw t1, 31*4(t0)
2:
# Next, load registers from the new context
lw s0, 16*4(tp)
lw s1, 17*4(tp)
lw s2, 18*4(tp)
lw s3, 19*4(tp)
lw s4, 20*4(tp)
lw s5, 21*4(tp)
lw s6, 22*4(tp)
lw s7, 23*4(tp)
lw s8, 24*4(tp)
lw s9, 25*4(tp)
lw s10, 26*4(tp)
lw s11, 27*4(tp)
lw gp, 28*4(tp)
# TP will be restored last.
lw sp, 30*4(tp)
lw t1, 31*4(tp)
csrw mepc, t1
3:
lw ra, 0*4(tp)
lw t0, 1*4(tp)
lw t1, 2*4(tp)
lw t2, 3*4(tp)
lw t3, 4*4(tp)
lw t4, 5*4(tp)
lw t5, 6*4(tp)
lw t6, 7*4(tp)
lw a0, 8*4(tp)
lw a1, 9*4(tp)
lw a2, 10*4(tp)
lw a3, 11*4(tp)
lw a4, 12*4(tp)
lw a5, 13*4(tp)
lw a6, 14*4(tp)
lw a7, 15*4(tp)
# Restore TP last. For the idle hook, this should write 0, which prevents saving its state.
lw tp, 29*4(tp)
mret
.cfi_endproc
",
scheduler_interrupt_handler = sym swint_handler,
}
}
#[esp_hal::ram]
extern "C" fn swint_handler() {
match Cpu::current() {
Cpu::ProCpu => unsafe { SoftwareInterrupt::<'static, 0>::steal() }.reset(),
#[cfg(multi_core)]
Cpu::AppCpu => unsafe { SoftwareInterrupt::<'static, 1>::steal() }.reset(),
}
SCHEDULER.with(|scheduler| scheduler.switch_task());
}
#[inline]
pub(crate) fn yield_task() {
#[cfg(feature = "rtos-trace")]
{
rtos_trace::trace::marker_begin(TraceEvents::YieldTask as u32);
rtos_trace::trace::marker_end(TraceEvents::YieldTask as u32);
}
match Cpu::current() {
Cpu::ProCpu => unsafe { SoftwareInterrupt::<'static, 0>::steal() }.raise(),
#[cfg(multi_core)]
Cpu::AppCpu => unsafe { SoftwareInterrupt::<'static, 1>::steal() }.raise(),
}
}