From 57cc450f44daf4a99871159b540dc30e39ac90c8 Mon Sep 17 00:00:00 2001 From: Tobias Aguiar Date: Tue, 20 Jan 2026 05:25:32 +0100 Subject: [PATCH 1/7] riscv: add HAL API and test backend --- include/cmrx/arch/riscv/hal.h | 49 ++++++++++++++++++++++ src/os/arch/riscv/CMakeLists.txt | 6 +++ src/os/arch/riscv/hal/arch/corelocal.h | 23 +++++++++++ src/os/arch/riscv/hal/arch/mpu.h | 17 ++++++++ src/os/arch/riscv/hal/arch/runtime.h | 12 ++++++ src/os/arch/riscv/hal/arch/static.h | 8 ++++ src/os/arch/riscv/hal/arch/sysenter.h | 6 +++ src/os/arch/riscv/hal/riscv_hal_backend.c | 2 + src/os/arch/testing/CMakeLists.txt | 1 + src/os/arch/testing/riscv_hal_testing.c | 50 +++++++++++++++++++++++ src/os/kernel/tests/CMakeLists.txt | 1 + src/os/kernel/tests/riscv_hal.c | 45 ++++++++++++++++++++ 12 files changed, 220 insertions(+) create mode 100644 include/cmrx/arch/riscv/hal.h create mode 100644 src/os/arch/riscv/CMakeLists.txt create mode 100644 src/os/arch/riscv/hal/arch/corelocal.h create mode 100644 src/os/arch/riscv/hal/arch/mpu.h create mode 100644 src/os/arch/riscv/hal/arch/runtime.h create mode 100644 src/os/arch/riscv/hal/arch/static.h create mode 100644 src/os/arch/riscv/hal/arch/sysenter.h create mode 100644 src/os/arch/riscv/hal/riscv_hal_backend.c create mode 100644 src/os/arch/testing/riscv_hal_testing.c create mode 100644 src/os/kernel/tests/riscv_hal.c diff --git a/include/cmrx/arch/riscv/hal.h b/include/cmrx/arch/riscv/hal.h new file mode 100644 index 00000000..a85efa16 --- /dev/null +++ b/include/cmrx/arch/riscv/hal.h @@ -0,0 +1,49 @@ +/* + * Thin internal RISC-V HAL (CMSIS-like) for CMRX. + * + * Purpose: + * - Provide a minimal, swappable API for CSR and interrupt primitives needed by + * the RISC-V thread switcher and its safe-point logic. + * + * Notes: + * - This header intentionally does not define CSR bit layouts or trap semantics. + * - Unit-test builds use a testing backend implemented with static fakes. + */ +#pragma once + +#include +#include + +/* ---- Minimal CSR accessors (subset justified by switcher needs) ---- */ + +uint32_t cmrx_riscv_csr_read_mstatus(void); +void cmrx_riscv_csr_write_mstatus(uint32_t value); + +uint32_t cmrx_riscv_csr_read_mepc(void); +void cmrx_riscv_csr_write_mepc(uint32_t value); + +uint32_t cmrx_riscv_csr_read_mcause(void); +void cmrx_riscv_csr_write_mcause(uint32_t value); + +uint32_t cmrx_riscv_csr_read_mtvec(void); +void cmrx_riscv_csr_write_mtvec(uint32_t value); + +uint32_t cmrx_riscv_csr_read_mie(void); +void cmrx_riscv_csr_write_mie(uint32_t value); + +uint32_t cmrx_riscv_csr_read_mip(void); +void cmrx_riscv_csr_write_mip(uint32_t value); + +/* ---- Interrupt primitives ---- */ + +void cmrx_riscv_irq_disable(void); +void cmrx_riscv_irq_enable(void); +bool cmrx_riscv_irq_is_enabled(void); + +/* ---- Minimal barriers ---- */ + +static inline void cmrx_riscv_compiler_barrier(void) +{ + __asm__ volatile("" ::: "memory"); +} + diff --git a/src/os/arch/riscv/CMakeLists.txt b/src/os/arch/riscv/CMakeLists.txt new file mode 100644 index 00000000..cfcdf191 --- /dev/null +++ b/src/os/arch/riscv/CMakeLists.txt @@ -0,0 +1,6 @@ +set(cmrx_riscv_SRCS + hal/riscv_hal_backend.c +) + +target_sources(os PRIVATE ${cmrx_riscv_SRCS}) + diff --git a/src/os/arch/riscv/hal/arch/corelocal.h b/src/os/arch/riscv/hal/arch/corelocal.h new file mode 100644 index 00000000..80c84a2f --- /dev/null +++ b/src/os/arch/riscv/hal/arch/corelocal.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +/* Portability layer header for includes. */ + +#ifndef CMRX_ARCH_SMP_SUPPORTED +#define OS_NUM_CORES 1 +#endif + +typedef void (*callback_t)(); + +extern unsigned coreid(); +extern void os_core_lock(); +extern void os_core_unlock(); + +extern void os_smp_lock(); +extern void os_smp_unlock(); + +#ifndef OS_NUM_CORES +#error "Macro OS_NUM_CORES is not defined. Use -DOS_NUM_CORES=x to tell the CMRX kernel how many cores it manages!" +#endif + diff --git a/src/os/arch/riscv/hal/arch/mpu.h b/src/os/arch/riscv/hal/arch/mpu.h new file mode 100644 index 00000000..a7cd582c --- /dev/null +++ b/src/os/arch/riscv/hal/arch/mpu.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +/* + * Stub MPU definitions required by the kernel headers. + * Memory protection is not implemented by this HAL. + */ + +struct MPU_Registers { + uint32_t REG1; + uint32_t REG2; +}; + +typedef struct MPU_Registers MPU_State[MPU_STATE_SIZE]; + diff --git a/src/os/arch/riscv/hal/arch/runtime.h b/src/os/arch/riscv/hal/arch/runtime.h new file mode 100644 index 00000000..8259c199 --- /dev/null +++ b/src/os/arch/riscv/hal/arch/runtime.h @@ -0,0 +1,12 @@ +#pragma once + +/* No special handling yet; platform integration will define this as needed. */ +#define os_thread_initialize_arch(x) + +struct Arch_State_t { + /* intentionally left empty */ +}; + +#define os_init_arch(x) +#define os_init_core(x) + diff --git a/src/os/arch/riscv/hal/arch/static.h b/src/os/arch/riscv/hal/arch/static.h new file mode 100644 index 00000000..a0a0908d --- /dev/null +++ b/src/os/arch/riscv/hal/arch/static.h @@ -0,0 +1,8 @@ +#pragma once + +unsigned static_init_thread_count(); +const struct OS_thread_create_t * static_init_thread_table(); + +unsigned static_init_process_count(); +const struct OS_process_definition_t * static_init_process_table(); + diff --git a/src/os/arch/riscv/hal/arch/sysenter.h b/src/os/arch/riscv/hal/arch/sysenter.h new file mode 100644 index 00000000..628cdf87 --- /dev/null +++ b/src/os/arch/riscv/hal/arch/sysenter.h @@ -0,0 +1,6 @@ +#pragma once + +/* Syscall/trap entry is platform-specific. */ +#define __SYSCALL +#define __SVC(x) return 0; + diff --git a/src/os/arch/riscv/hal/riscv_hal_backend.c b/src/os/arch/riscv/hal/riscv_hal_backend.c new file mode 100644 index 00000000..44510f3b --- /dev/null +++ b/src/os/arch/riscv/hal/riscv_hal_backend.c @@ -0,0 +1,2 @@ +#error "CMRX RISC-V HAL backend is not implemented yet." + diff --git a/src/os/arch/testing/CMakeLists.txt b/src/os/arch/testing/CMakeLists.txt index 57cf279c..cf89ec59 100644 --- a/src/os/arch/testing/CMakeLists.txt +++ b/src/os/arch/testing/CMakeLists.txt @@ -6,6 +6,7 @@ set(cmrx_testing_SRCS timing_provider.c pendsv.c rpc.c + riscv_hal_testing.c ) target_sources(os PRIVATE ${cmrx_testing_SRCS}) target_link_libraries(os PRIVATE ctest) diff --git a/src/os/arch/testing/riscv_hal_testing.c b/src/os/arch/testing/riscv_hal_testing.c new file mode 100644 index 00000000..2e35e3a5 --- /dev/null +++ b/src/os/arch/testing/riscv_hal_testing.c @@ -0,0 +1,50 @@ +/* + * RISC-V HAL backend used by unit tests. + * + * This backend provides in-memory fake CSR values and interrupt state so the HAL + * API can be tested on the host without any vendor headers or hardware. + */ + +/* + * NOTE: Some unit-test build configurations may redefine `static` for testability. + * This backend must keep its fakes translation-unit local, so we explicitly undo + * that here if needed. + */ +#ifdef static +#undef static +#endif + +#include + +static uint32_t fake_mstatus; +static uint32_t fake_mepc; +static uint32_t fake_mcause; +static uint32_t fake_mtvec; +static uint32_t fake_mie; +static uint32_t fake_mip; + +/* IRQ enable state is represented as a uint32_t fake (0/1) per requirements. */ +static uint32_t fake_irq_enabled; + +uint32_t cmrx_riscv_csr_read_mstatus(void) { return fake_mstatus; } +void cmrx_riscv_csr_write_mstatus(uint32_t value) { fake_mstatus = value; } + +uint32_t cmrx_riscv_csr_read_mepc(void) { return fake_mepc; } +void cmrx_riscv_csr_write_mepc(uint32_t value) { fake_mepc = value; } + +uint32_t cmrx_riscv_csr_read_mcause(void) { return fake_mcause; } +void cmrx_riscv_csr_write_mcause(uint32_t value) { fake_mcause = value; } + +uint32_t cmrx_riscv_csr_read_mtvec(void) { return fake_mtvec; } +void cmrx_riscv_csr_write_mtvec(uint32_t value) { fake_mtvec = value; } + +uint32_t cmrx_riscv_csr_read_mie(void) { return fake_mie; } +void cmrx_riscv_csr_write_mie(uint32_t value) { fake_mie = value; } + +uint32_t cmrx_riscv_csr_read_mip(void) { return fake_mip; } +void cmrx_riscv_csr_write_mip(uint32_t value) { fake_mip = value; } + +void cmrx_riscv_irq_disable(void) { fake_irq_enabled = 0u; } +void cmrx_riscv_irq_enable(void) { fake_irq_enabled = 1u; } +bool cmrx_riscv_irq_is_enabled(void) { return fake_irq_enabled != 0u; } + diff --git a/src/os/kernel/tests/CMakeLists.txt b/src/os/kernel/tests/CMakeLists.txt index 90bb709e..4bbfdccc 100644 --- a/src/os/kernel/tests/CMakeLists.txt +++ b/src/os/kernel/tests/CMakeLists.txt @@ -18,6 +18,7 @@ set(test_kernel_SRCS os_notify_wait_object.c os_notify_futex.c os_shutdown.c + riscv_hal.c stubs.c test_traits.c ) diff --git a/src/os/kernel/tests/riscv_hal.c b/src/os/kernel/tests/riscv_hal.c new file mode 100644 index 00000000..ae7fe0b8 --- /dev/null +++ b/src/os/kernel/tests/riscv_hal.c @@ -0,0 +1,45 @@ +#include + +#include + +CTEST(riscv_hal, csr_read_write_roundtrip) +{ + /* mstatus */ + cmrx_riscv_csr_write_mstatus(0x11223344u); + ASSERT_EQUAL(cmrx_riscv_csr_read_mstatus(), 0x11223344u); + cmrx_riscv_csr_write_mstatus(0xa5a5a5a5u); + ASSERT_EQUAL(cmrx_riscv_csr_read_mstatus(), 0xa5a5a5a5u); + + /* mepc */ + cmrx_riscv_csr_write_mepc(0x01020304u); + ASSERT_EQUAL(cmrx_riscv_csr_read_mepc(), 0x01020304u); + + /* mcause */ + cmrx_riscv_csr_write_mcause(0x8000000bu); + ASSERT_EQUAL(cmrx_riscv_csr_read_mcause(), 0x8000000bu); + + /* mtvec */ + cmrx_riscv_csr_write_mtvec(0x00001000u); + ASSERT_EQUAL(cmrx_riscv_csr_read_mtvec(), 0x00001000u); + + /* mie */ + cmrx_riscv_csr_write_mie(0x00000088u); + ASSERT_EQUAL(cmrx_riscv_csr_read_mie(), 0x00000088u); + + /* mip */ + cmrx_riscv_csr_write_mip(0x00000008u); + ASSERT_EQUAL(cmrx_riscv_csr_read_mip(), 0x00000008u); +} + +CTEST(riscv_hal, irq_enable_disable) +{ + cmrx_riscv_irq_disable(); + ASSERT_EQUAL(cmrx_riscv_irq_is_enabled(), false); + + cmrx_riscv_irq_enable(); + ASSERT_EQUAL(cmrx_riscv_irq_is_enabled(), true); + + cmrx_riscv_irq_disable(); + ASSERT_EQUAL(cmrx_riscv_irq_is_enabled(), false); +} + From 59ae2e69d2d1c1488abad3b0cbdf6917155d3b13 Mon Sep 17 00:00:00 2001 From: Tobias Aguiar Date: Fri, 23 Jan 2026 06:43:55 +0100 Subject: [PATCH 2/7] riscv: add thread switcher (request + safe point + minimal context switch) --- include/cmrx/arch/riscv/context_switch.h | 14 +++ src/os/arch/riscv/CMakeLists.txt | 2 + src/os/arch/riscv/context_switch_core.c | 114 ++++++++++++++++++ src/os/arch/riscv/context_switch_request.c | 8 ++ src/os/arch/riscv/hal/arch/sysenter.h | 4 +- src/os/arch/riscv/hal/testing/CMakeLists.txt | 9 ++ .../riscv/hal/testing/riscv_context_switch.c | 114 ++++++++++++++++++ .../hal/testing/riscv_context_switch_fake.c | 74 ++++++++++++ .../hal/testing/riscv_context_switch_fake.h | 8 ++ .../riscv/hal/testing}/riscv_hal.c | 0 .../hal/testing/riscv_hal_fake.c} | 0 src/os/arch/testing/CMakeLists.txt | 3 +- src/os/kernel/tests/CMakeLists.txt | 1 - 13 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 include/cmrx/arch/riscv/context_switch.h create mode 100644 src/os/arch/riscv/context_switch_core.c create mode 100644 src/os/arch/riscv/context_switch_request.c create mode 100644 src/os/arch/riscv/hal/testing/CMakeLists.txt create mode 100644 src/os/arch/riscv/hal/testing/riscv_context_switch.c create mode 100644 src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c create mode 100644 src/os/arch/riscv/hal/testing/riscv_context_switch_fake.h rename src/os/{kernel/tests => arch/riscv/hal/testing}/riscv_hal.c (100%) rename src/os/arch/{testing/riscv_hal_testing.c => riscv/hal/testing/riscv_hal_fake.c} (100%) diff --git a/include/cmrx/arch/riscv/context_switch.h b/include/cmrx/arch/riscv/context_switch.h new file mode 100644 index 00000000..cef6b53e --- /dev/null +++ b/include/cmrx/arch/riscv/context_switch.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +/* Context frame size keeps stack 16-byte aligned (riscv-abi documentation, 2.2 Hardware + * Floating-point Calling Convention section). + */ +#define CMRX_RISCV_CONTEXT_FRAME_WORDS 16u +#define CMRX_RISCV_CONTEXT_FRAME_BYTES (CMRX_RISCV_CONTEXT_FRAME_WORDS * sizeof(uint32_t)) + +void os_riscv_context_switch_request(bool activate); +bool os_riscv_context_switch_is_pending(void); +void os_riscv_context_switch_safe_point(void); diff --git a/src/os/arch/riscv/CMakeLists.txt b/src/os/arch/riscv/CMakeLists.txt index cfcdf191..84b72db1 100644 --- a/src/os/arch/riscv/CMakeLists.txt +++ b/src/os/arch/riscv/CMakeLists.txt @@ -1,5 +1,7 @@ set(cmrx_riscv_SRCS hal/riscv_hal_backend.c + context_switch_core.c + context_switch_request.c ) target_sources(os PRIVATE ${cmrx_riscv_SRCS}) diff --git a/src/os/arch/riscv/context_switch_core.c b/src/os/arch/riscv/context_switch_core.c new file mode 100644 index 00000000..97fff2fb --- /dev/null +++ b/src/os/arch/riscv/context_switch_core.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +static volatile bool pending_context_switch[OS_NUM_CORES]; + +#define os_riscv_context_switch_barrier() __asm__ volatile("" ::: "memory") + +void os_riscv_context_switch_request(bool activate) +{ + pending_context_switch[coreid()] = activate; + os_riscv_context_switch_barrier(); +} + +bool os_riscv_context_switch_is_pending(void) +{ + return pending_context_switch[coreid()]; +} + +static void os_riscv_context_switch_update_state(void) +{ + struct OS_core_state_t * const cpu_state = &core[coreid()]; + + if (os_threads[cpu_state->thread_current].state == THREAD_STATE_RUNNING) + { + os_threads[cpu_state->thread_current].state = THREAD_STATE_READY; + } + + cpu_state->thread_current = cpu_state->thread_next; + os_threads[cpu_state->thread_current].state = THREAD_STATE_RUNNING; +} + +__attribute__((noinline, used)) static uint32_t os_riscv_context_switch_prepare(void) +{ + if (!os_riscv_context_switch_is_pending()) + { + return 0u; + } + + os_riscv_context_switch_request(false); + os_riscv_context_switch_update_state(); + return 1u; +} + +/* Save/restore s0-s11 (callee-saved) per psABI (riscv-abi documentation, + * 1.1 Integer Register Convention section). Frame size keeps SP 16-byte aligned + * (riscv-abi documentation, 2.2 Hardware Floating-point Calling Convention section, + * "stack pointer shall be aligned to a 128-bit boundary upon procedure entry" and + * "must remain aligned throughout procedure execution" sections). + */ +__attribute__((naked)) void os_riscv_context_switch_perform(void) +{ + __asm__ volatile( + "addi sp, sp, -%[frame]\n\t" + "sw s0, 0(sp)\n\t" + "sw s1, 4(sp)\n\t" + "sw s2, 8(sp)\n\t" + "sw s3, 12(sp)\n\t" + "sw s4, 16(sp)\n\t" + "sw s5, 20(sp)\n\t" + "sw s6, 24(sp)\n\t" + "sw s7, 28(sp)\n\t" + "sw s8, 32(sp)\n\t" + "sw s9, 36(sp)\n\t" + "sw s10, 40(sp)\n\t" + "sw s11, 44(sp)\n\t" + "la t0, cpu_context\n\t" + "lw t1, %[old_task_off](t0)\n\t" + "sw sp, %[thread_sp_off](t1)\n\t" + "lw t2, %[new_task_off](t0)\n\t" + "lw sp, %[thread_sp_off](t2)\n\t" + "lw s0, 0(sp)\n\t" + "lw s1, 4(sp)\n\t" + "lw s2, 8(sp)\n\t" + "lw s3, 12(sp)\n\t" + "lw s4, 16(sp)\n\t" + "lw s5, 20(sp)\n\t" + "lw s6, 24(sp)\n\t" + "lw s7, 28(sp)\n\t" + "lw s8, 32(sp)\n\t" + "lw s9, 36(sp)\n\t" + "lw s10, 40(sp)\n\t" + "lw s11, 44(sp)\n\t" + "addi sp, sp, %[frame]\n\t" + "ret\n\t" + : + : [frame] "i"(CMRX_RISCV_CONTEXT_FRAME_BYTES), + [old_task_off] "i"(offsetof(struct OS_scheduling_context_t, old_task)), + [new_task_off] "i"(offsetof(struct OS_scheduling_context_t, new_task)), + [thread_sp_off] "i"(offsetof(struct OS_thread_t, sp)) + : "t0", "t1", "t2", "memory"); +} + +__attribute__((naked)) void os_riscv_context_switch_safe_point(void) +{ + __asm__ volatile( + "mv t0, ra\n\t" + "call os_riscv_context_switch_prepare\n\t" + "mv ra, t0\n\t" + "beqz a0, 1f\n\t" + "j os_riscv_context_switch_perform\n\t" + "1:\n\t" + "ret\n\t" + : + : + : "t0", "a0", "memory"); +} + +#endif diff --git a/src/os/arch/riscv/context_switch_request.c b/src/os/arch/riscv/context_switch_request.c new file mode 100644 index 00000000..1ff08189 --- /dev/null +++ b/src/os/arch/riscv/context_switch_request.c @@ -0,0 +1,8 @@ +#include +#include +#include + +void os_request_context_switch(bool activate) +{ + os_riscv_context_switch_request(activate); +} diff --git a/src/os/arch/riscv/hal/arch/sysenter.h b/src/os/arch/riscv/hal/arch/sysenter.h index 628cdf87..a178a838 100644 --- a/src/os/arch/riscv/hal/arch/sysenter.h +++ b/src/os/arch/riscv/hal/arch/sysenter.h @@ -1,6 +1,8 @@ #pragma once +#include + /* Syscall/trap entry is platform-specific. */ #define __SYSCALL -#define __SVC(x) return 0; +#define __SVC(x) return E_NOTAVAIL; diff --git a/src/os/arch/riscv/hal/testing/CMakeLists.txt b/src/os/arch/riscv/hal/testing/CMakeLists.txt new file mode 100644 index 00000000..a2848e74 --- /dev/null +++ b/src/os/arch/riscv/hal/testing/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources(os PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/riscv_hal_fake.c + ${CMAKE_CURRENT_SOURCE_DIR}/riscv_context_switch_fake.c +) + +target_sources(test_kernel PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/riscv_hal.c + ${CMAKE_CURRENT_SOURCE_DIR}/riscv_context_switch.c +) diff --git a/src/os/arch/riscv/hal/testing/riscv_context_switch.c b/src/os/arch/riscv/hal/testing/riscv_context_switch.c new file mode 100644 index 00000000..adaa3842 --- /dev/null +++ b/src/os/arch/riscv/hal/testing/riscv_context_switch.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "riscv_context_switch_fake.h" + +static uint32_t riscv_stack_a[64] __attribute__((aligned(16))); +static uint32_t riscv_stack_b[64] __attribute__((aligned(16))); + +static intmax_t riscv_ptr_value(const void *ptr) +{ + return (intmax_t)(uintptr_t) ptr; +} + +static void riscv_reset_state(void) +{ + memset(&cpu_context, 0, sizeof(cpu_context)); + memset(os_threads, 0, sizeof(os_threads)); + memset(core, 0, sizeof(core)); + os_riscv_context_switch_request(false); + riscv_context_switch_called = false; + riscv_context_switch_perform_switch = true; + riscv_context_switch_fake_sp = NULL; +} + +static void riscv_setup_switch(uint32_t **old_sp, uint32_t **new_saved_sp, uint32_t **new_sp_after) +{ + riscv_reset_state(); + + core[0].thread_current = 0; + core[0].thread_next = 1; + + os_threads[0].state = THREAD_STATE_RUNNING; + os_threads[1].state = THREAD_STATE_READY; + + cpu_context.old_task = &os_threads[0]; + cpu_context.new_task = &os_threads[1]; + + *old_sp = &riscv_stack_a[64]; + *new_sp_after = &riscv_stack_b[64]; + *new_saved_sp = *new_sp_after - CMRX_RISCV_CONTEXT_FRAME_WORDS; + + cpu_context.new_task->sp = *new_saved_sp; + riscv_context_switch_fake_sp = *old_sp; +} + +CTEST_DATA(riscv_context_switch_reset) { +}; + +CTEST_SETUP(riscv_context_switch_reset) { + riscv_reset_state(); +} + +CTEST2(riscv_context_switch_reset, request_set_clear) +{ + os_riscv_context_switch_request(false); + ASSERT_EQUAL(os_riscv_context_switch_is_pending(), false); + + os_riscv_context_switch_request(true); + ASSERT_EQUAL(os_riscv_context_switch_is_pending(), true); + + os_riscv_context_switch_request(false); + ASSERT_EQUAL(os_riscv_context_switch_is_pending(), false); +} + +CTEST_DATA(riscv_context_switch_safe_point) { + uint32_t *old_sp; + uint32_t *new_saved_sp; + uint32_t *new_sp_after; +}; + +CTEST_SETUP(riscv_context_switch_safe_point) { + uint32_t *old_sp = NULL; + uint32_t *new_saved_sp = NULL; + uint32_t *new_sp_after = NULL; + + memset(data, 0, sizeof(*data)); + riscv_setup_switch(&old_sp, &new_saved_sp, &new_sp_after); + data->old_sp = old_sp; + data->new_saved_sp = new_saved_sp; + data->new_sp_after = new_sp_after; +} + +CTEST2(riscv_context_switch_safe_point, safe_point_consumes_once) +{ + os_riscv_context_switch_request(true); + + os_riscv_context_switch_safe_point(); + ASSERT_EQUAL(os_riscv_context_switch_is_pending(), false); + + uint32_t *saved_old_sp = cpu_context.old_task->sp; + os_riscv_context_switch_safe_point(); + ASSERT_EQUAL(riscv_ptr_value(cpu_context.old_task->sp), riscv_ptr_value(saved_old_sp)); +} + +CTEST2(riscv_context_switch_safe_point, sp_bookkeeping_and_state_transition) +{ + os_riscv_context_switch_request(true); + os_riscv_context_switch_safe_point(); + + ASSERT_EQUAL(riscv_ptr_value(cpu_context.old_task->sp), + riscv_ptr_value(data->old_sp - CMRX_RISCV_CONTEXT_FRAME_WORDS)); + ASSERT_EQUAL(riscv_ptr_value(riscv_context_switch_fake_sp), + riscv_ptr_value(data->new_sp_after)); + + ASSERT_EQUAL(core[0].thread_current, 1); + ASSERT_EQUAL(os_threads[0].state, THREAD_STATE_READY); + ASSERT_EQUAL(os_threads[1].state, THREAD_STATE_RUNNING); +} diff --git a/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c b/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c new file mode 100644 index 00000000..7a615e2d --- /dev/null +++ b/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "riscv_context_switch_fake.h" + +bool riscv_context_switch_called = false; +bool riscv_context_switch_perform_switch = true; +uint32_t *riscv_context_switch_fake_sp = NULL; + +static volatile bool pending_context_switch[OS_NUM_CORES]; + +void os_riscv_context_switch_request(bool activate) +{ + pending_context_switch[coreid()] = activate; +} + +bool os_riscv_context_switch_is_pending(void) +{ + return pending_context_switch[coreid()]; +} + +static void os_riscv_context_switch_update_state(void) +{ + struct OS_core_state_t * const cpu_state = &core[coreid()]; + + if (os_threads[cpu_state->thread_current].state == THREAD_STATE_RUNNING) + { + os_threads[cpu_state->thread_current].state = THREAD_STATE_READY; + } + + cpu_state->thread_current = cpu_state->thread_next; + os_threads[cpu_state->thread_current].state = THREAD_STATE_RUNNING; +} + +static void os_riscv_context_switch_perform(void) +{ + uint32_t *sp = riscv_context_switch_fake_sp; + if (sp != NULL) + { + sp -= CMRX_RISCV_CONTEXT_FRAME_WORDS; + } + cpu_context.old_task->sp = sp; + + sp = cpu_context.new_task->sp; + if (sp != NULL) + { + sp += CMRX_RISCV_CONTEXT_FRAME_WORDS; + } + riscv_context_switch_fake_sp = sp; + riscv_context_switch_called = true; +} + +void os_riscv_context_switch_safe_point(void) +{ + if (!os_riscv_context_switch_is_pending()) + { + return; + } + + os_riscv_context_switch_request(false); + + if (!riscv_context_switch_perform_switch) + { + return; + } + + os_riscv_context_switch_update_state(); + os_riscv_context_switch_perform(); +} diff --git a/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.h b/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.h new file mode 100644 index 00000000..e55ff0de --- /dev/null +++ b/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +extern bool riscv_context_switch_called; +extern bool riscv_context_switch_perform_switch; +extern uint32_t *riscv_context_switch_fake_sp; diff --git a/src/os/kernel/tests/riscv_hal.c b/src/os/arch/riscv/hal/testing/riscv_hal.c similarity index 100% rename from src/os/kernel/tests/riscv_hal.c rename to src/os/arch/riscv/hal/testing/riscv_hal.c diff --git a/src/os/arch/testing/riscv_hal_testing.c b/src/os/arch/riscv/hal/testing/riscv_hal_fake.c similarity index 100% rename from src/os/arch/testing/riscv_hal_testing.c rename to src/os/arch/riscv/hal/testing/riscv_hal_fake.c diff --git a/src/os/arch/testing/CMakeLists.txt b/src/os/arch/testing/CMakeLists.txt index cf89ec59..4e9a3996 100644 --- a/src/os/arch/testing/CMakeLists.txt +++ b/src/os/arch/testing/CMakeLists.txt @@ -6,7 +6,8 @@ set(cmrx_testing_SRCS timing_provider.c pendsv.c rpc.c - riscv_hal_testing.c ) target_sources(os PRIVATE ${cmrx_testing_SRCS}) target_link_libraries(os PRIVATE ctest) + +add_subdirectory(../riscv/hal/testing ${CMAKE_CURRENT_BINARY_DIR}/riscv_hal_testing) diff --git a/src/os/kernel/tests/CMakeLists.txt b/src/os/kernel/tests/CMakeLists.txt index 4bbfdccc..90bb709e 100644 --- a/src/os/kernel/tests/CMakeLists.txt +++ b/src/os/kernel/tests/CMakeLists.txt @@ -18,7 +18,6 @@ set(test_kernel_SRCS os_notify_wait_object.c os_notify_futex.c os_shutdown.c - riscv_hal.c stubs.c test_traits.c ) From 376ac00ca230babfc7205a9f2ab474f6032e951d Mon Sep 17 00:00:00 2001 From: Tobias Aguiar Date: Fri, 23 Jan 2026 07:05:52 +0100 Subject: [PATCH 3/7] riscv: move switcher tests into testing arch --- src/os/arch/riscv/context_switch_core.c | 2 -- src/os/arch/riscv/hal/testing/riscv_context_switch.c | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/os/arch/riscv/context_switch_core.c b/src/os/arch/riscv/context_switch_core.c index 97fff2fb..d7c8901d 100644 --- a/src/os/arch/riscv/context_switch_core.c +++ b/src/os/arch/riscv/context_switch_core.c @@ -110,5 +110,3 @@ __attribute__((naked)) void os_riscv_context_switch_safe_point(void) : : "t0", "a0", "memory"); } - -#endif diff --git a/src/os/arch/riscv/hal/testing/riscv_context_switch.c b/src/os/arch/riscv/hal/testing/riscv_context_switch.c index adaa3842..cd764ded 100644 --- a/src/os/arch/riscv/hal/testing/riscv_context_switch.c +++ b/src/os/arch/riscv/hal/testing/riscv_context_switch.c @@ -12,6 +12,10 @@ static uint32_t riscv_stack_a[64] __attribute__((aligned(16))); static uint32_t riscv_stack_b[64] __attribute__((aligned(16))); +extern bool riscv_context_switch_called; +extern bool riscv_context_switch_perform_switch; +extern uint32_t *riscv_context_switch_fake_sp; + static intmax_t riscv_ptr_value(const void *ptr) { return (intmax_t)(uintptr_t) ptr; From 2576eb6c91a40487eb76eecf7cfba337e7f7d293 Mon Sep 17 00:00:00 2001 From: Tobias Aguiar Date: Sun, 1 Feb 2026 18:04:42 +0100 Subject: [PATCH 4/7] riscv: implement generic HAL backend --- cmake/arch/riscv/hal/CMRX.cmake | 65 +++++++++++++++++ cmake/arch/riscv/hal/cmrx_sections.ld | 36 +++++++++ src/extra/CMakeLists.txt | 26 ++++--- src/os/arch/riscv/CMakeLists.txt | 2 + src/os/arch/riscv/hal/arch/application.h | 41 +++++++++++ src/os/arch/riscv/hal/arch/runtime.h | 2 +- src/os/arch/riscv/hal/arch/syscall.h | 17 +++++ src/os/arch/riscv/hal/riscv_hal_backend.c | 21 +++++- src/os/arch/riscv/hal/riscv_hal_backend_csr.c | 73 +++++++++++++++++++ src/os/arch/riscv/rpc.c | 20 +++++ src/os/kernel/tests/CMakeLists.txt | 13 ++++ src/os/kernel/tests/riscv_hal_backend.c | 32 ++++++++ .../kernel/tests/riscv_hal_backend_fake_csr.c | 26 +++++++ 13 files changed, 360 insertions(+), 14 deletions(-) create mode 100644 cmake/arch/riscv/hal/CMRX.cmake create mode 100644 cmake/arch/riscv/hal/cmrx_sections.ld create mode 100644 src/os/arch/riscv/hal/arch/application.h create mode 100644 src/os/arch/riscv/hal/arch/syscall.h create mode 100644 src/os/arch/riscv/hal/riscv_hal_backend_csr.c create mode 100644 src/os/arch/riscv/rpc.c create mode 100644 src/os/kernel/tests/riscv_hal_backend.c create mode 100644 src/os/kernel/tests/riscv_hal_backend_fake_csr.c diff --git a/cmake/arch/riscv/hal/CMRX.cmake b/cmake/arch/riscv/hal/CMRX.cmake new file mode 100644 index 00000000..2e9405fb --- /dev/null +++ b/cmake/arch/riscv/hal/CMRX.cmake @@ -0,0 +1,65 @@ +# CMRX CMake module for RISC-V HAL +# +# Provides minimal cmake infrastructure for RISC-V targets. +# Note: MPU linker script management is not yet implemented for RISC-V. + +message(STATUS "RISC-V HAL CMRX CMake component loaded") + +find_program(PYTHON_EXE NAMES python3 python REQUIRED DOC "Python 3 executable") + +# Path to the CMRX sections linker script fragment +set(CMRX_RISCV_SECTIONS_LD "${CMAKE_CURRENT_LIST_DIR}/cmrx_sections.ld") + +# Stub: add_firmware wraps add_executable for RISC-V +# Full linker script management is not yet implemented +function(add_firmware FW_NAME) + get_property(CMRX_ROOT_DIR GLOBAL PROPERTY CMRX_ROOT_DIR) + if ("${CMRX_ROOT_DIR}" STREQUAL "") + message(FATAL_ERROR "CMRX source tree directory property not set! Did you forget to add_subdirectory(cmrx)?") + endif() + + add_executable(${FW_NAME} ${ARGN}) + set_property(TARGET ${FW_NAME} PROPERTY CMRX_IS_FIRMWARE 1) + + # Generate map file for analysis + target_link_options(${FW_NAME} PUBLIC -Wl,-Map=${FW_NAME}.map) + + # Include the CMRX sections linker script fragment. + # This defines .cmrx_applications and .cmrx_thread_create sections + # with __applications_start/__applications_end and + # __thread_create_start/__thread_create_end boundary symbols. + target_link_options(${FW_NAME} PUBLIC + "-T${CMRX_RISCV_SECTIONS_LD}" + ) +endfunction() + +# Stub: target_add_applications links applications to firmware +# Full linker script application support is not yet implemented +function(target_add_applications TGT_NAME) + target_link_libraries(${TGT_NAME} ${ARGN}) + + get_target_property(IS_FIRMWARE ${TGT_NAME} CMRX_IS_FIRMWARE) + if ("${IS_FIRMWARE}" EQUAL "1") + foreach(LIBRARY ${ARGN}) + get_target_property(IS_APPLICATION ${LIBRARY} CMRX_IS_APPLICATION) + if ("${IS_APPLICATION}" EQUAL "1") + message(STATUS "Adding application ${LIBRARY} to firmware ${TGT_NAME}") + endif() + endforeach() + endif() +endfunction() + +## Add firmware application definition +# RISC-V implementation mirrors the generic/static-library model used by +# other architectures and marks the target as a CMRX application. +function(add_application NAME) + get_property(CMRX_ROOT_DIR GLOBAL PROPERTY CMRX_ROOT_DIR) + if ("${CMRX_ROOT_DIR}" STREQUAL "") + message(FATAL_ERROR "CMRX source tree directory property not set! Did you forget to add_subdirectory(cmrx)?") + endif() + + add_library(${NAME} STATIC EXCLUDE_FROM_ALL ${ARGN}) + set_property(TARGET ${NAME} PROPERTY CMRX_IS_APPLICATION 1) + target_compile_definitions(${NAME} PRIVATE -D APPLICATION_NAME=${NAME}) + target_link_libraries(${NAME} stdlib) +endfunction() diff --git a/cmake/arch/riscv/hal/cmrx_sections.ld b/cmake/arch/riscv/hal/cmrx_sections.ld new file mode 100644 index 00000000..297387cf --- /dev/null +++ b/cmake/arch/riscv/hal/cmrx_sections.ld @@ -0,0 +1,36 @@ +/* + * CMRX linker script fragment for RISC-V. + * + * This linker script defines the CMRX static initialization sections + * and their boundary symbols. + * + * The CMRX macros OS_APPLICATION and OS_THREAD_CREATE place data in + * .applications and .thread_create sections respectively. + */ + +SECTIONS +{ + /* Application definitions table */ + .cmrx_applications : { + PROVIDE(__applications_start = .); + KEEP(*(.applications)) + KEEP(*(.applications.*)) + PROVIDE(__applications_end = .); + } + + /* Thread creation table */ + .cmrx_thread_create : { + PROVIDE(__thread_create_start = .); + KEEP(*(.thread_create)) + KEEP(*(.thread_create.*)) + PROVIDE(__thread_create_end = .); + } + + /* Syscall table entries */ + .cmrx_syscall : { + PROVIDE(__syscall_start = .); + KEEP(*(.syscall)) + KEEP(*(.syscall.*)) + PROVIDE(__syscall_end = .); + } +} diff --git a/src/extra/CMakeLists.txt b/src/extra/CMakeLists.txt index 7e69376c..9d62cb67 100644 --- a/src/extra/CMakeLists.txt +++ b/src/extra/CMakeLists.txt @@ -1,17 +1,19 @@ if (NOT UNIT_TESTING_BUILD) - set(aux_systick_SRCS systick.c) - add_library(aux_systick STATIC EXCLUDE_FROM_ALL ${aux_systick_SRCS}) - target_link_libraries(aux_systick cmsis_headers) - if (CMRX_STDLIB_USE_CMSIS_CORE) - target_link_libraries(aux_systick cmsis_core) - endif() - target_compile_options(aux_systick PRIVATE -mgeneral-regs-only) + if ("${CMRX_ARCH}" STREQUAL "arm") + set(aux_systick_SRCS systick.c) + add_library(aux_systick STATIC EXCLUDE_FROM_ALL ${aux_systick_SRCS}) + target_link_libraries(aux_systick cmsis_headers) + if (CMRX_STDLIB_USE_CMSIS_CORE) + target_link_libraries(aux_systick cmsis_core) + endif() + target_compile_options(aux_systick PRIVATE -mgeneral-regs-only) - set(aux_ipi_SRCS cm_ipi.c) - add_library(aux_ipi STATIC EXCLUDE_FROM_ALL ${aux_ipi_SRCS}) - target_link_libraries(aux_ipi cmsis_headers) - if (CMRX_STDLIB_USE_CMSIS_CORE) - target_link_libraries(aux_ipi cmsis_core) + set(aux_ipi_SRCS cm_ipi.c) + add_library(aux_ipi STATIC EXCLUDE_FROM_ALL ${aux_ipi_SRCS}) + target_link_libraries(aux_ipi cmsis_headers) + if (CMRX_STDLIB_USE_CMSIS_CORE) + target_link_libraries(aux_ipi cmsis_core) + endif() endif() set(phy_emu_SRCS emulator.c) diff --git a/src/os/arch/riscv/CMakeLists.txt b/src/os/arch/riscv/CMakeLists.txt index 84b72db1..0e8a2c05 100644 --- a/src/os/arch/riscv/CMakeLists.txt +++ b/src/os/arch/riscv/CMakeLists.txt @@ -1,7 +1,9 @@ set(cmrx_riscv_SRCS hal/riscv_hal_backend.c + hal/riscv_hal_backend_csr.c context_switch_core.c context_switch_request.c + rpc.c ) target_sources(os PRIVATE ${cmrx_riscv_SRCS}) diff --git a/src/os/arch/riscv/hal/arch/application.h b/src/os/arch/riscv/hal/arch/application.h new file mode 100644 index 00000000..5ae4a904 --- /dev/null +++ b/src/os/arch/riscv/hal/arch/application.h @@ -0,0 +1,41 @@ +#pragma once + +/* + * RISC-V HAL application metadata placement. + * + * The RP2350 RISC-V linker fragment defines .applications and .thread_create + * sections and boundary symbols consumed by CMRX startup. + */ +#define CMRX_VTABLE_SPECIFIER __attribute__((section(".vtable."))) const + +#define CMRX_APPLICATION_INSTANCE_ATTRIBUTES __attribute__((used, section(".applications"))) +#define CMRX_THREAD_AUTOCREATE_ATTRIBUTES __attribute__((used, section(".thread_create"))) + +#define CMRX_APPLICATION_INSTANCE_CONSTRUCTOR(application) \ +void * __APPL_SYMBOL(application, data_start) = (void *)1; \ +void * __APPL_SYMBOL(application, data_end) = (void *)1; \ +void * __APPL_SYMBOL(application, bss_start) = (void *)1; \ +void * __APPL_SYMBOL(application, bss_end) = (void *)1; \ +void * __APPL_SYMBOL(application, shared_start) = (void *)1; \ +void * __APPL_SYMBOL(application, shared_end) = (void *)1; \ +void * __APPL_SYMBOL(application, vtable_start) = (void *)1; \ +void * __APPL_SYMBOL(application, vtable_end) = (void *)1; \ +CMRX_APPLICATION_INSTANCE_ATTRIBUTES const struct OS_process_definition_t __APPL_SYMBOL(application, instance) = { \ + { \ + { &__APPL_SYMBOL(application, data_start), &__APPL_SYMBOL(application, data_end) }, \ + { &__APPL_SYMBOL(application, bss_start), &__APPL_SYMBOL(application, bss_end) }, \ + { __APPL_SYMBOL(application, mmio_start), __APPL_SYMBOL(application, mmio_end) }, \ + { __APPL_SYMBOL(application, mmio_2_start), __APPL_SYMBOL(application, mmio_2_end) }, \ + { &__APPL_SYMBOL(application, shared_start), &__APPL_SYMBOL(application, shared_end) } \ + }, \ + { &__APPL_SYMBOL(application, vtable_start), &__APPL_SYMBOL(application, vtable_end) } \ +} + +#define CMRX_THREAD_AUTOCREATE_CONSTRUCTOR(application, entrypoint, data, priority, core) \ +CMRX_THREAD_AUTOCREATE_ATTRIBUTES const struct OS_thread_create_t __APPL_SYMBOL(application, thread_create_ ## entrypoint) = { \ + &__APPL_SYMBOL(application, instance), \ + entrypoint, \ + data, \ + priority, \ + core \ +} diff --git a/src/os/arch/riscv/hal/arch/runtime.h b/src/os/arch/riscv/hal/arch/runtime.h index 8259c199..ab635304 100644 --- a/src/os/arch/riscv/hal/arch/runtime.h +++ b/src/os/arch/riscv/hal/arch/runtime.h @@ -1,7 +1,7 @@ #pragma once /* No special handling yet; platform integration will define this as needed. */ -#define os_thread_initialize_arch(x) +#define os_thread_initialize_arch(...) struct Arch_State_t { /* intentionally left empty */ diff --git a/src/os/arch/riscv/hal/arch/syscall.h b/src/os/arch/riscv/hal/arch/syscall.h new file mode 100644 index 00000000..52f22651 --- /dev/null +++ b/src/os/arch/riscv/hal/arch/syscall.h @@ -0,0 +1,17 @@ +#pragma once + +extern struct Syscall_Entry_t __syscall_start; +extern struct Syscall_Entry_t __syscall_end; + +static inline struct Syscall_Entry_t *os_syscalls_start(void) +{ + return &__syscall_start; +} + +static inline struct Syscall_Entry_t *os_syscalls_end(void) +{ + return &__syscall_end; +} + +#define REGISTER_SYSCALLS(...) \ +static SYSCALL_DEFINITION struct Syscall_Entry_t syscalls_ ## __COUNTER__[] = { __VA_ARGS__ } diff --git a/src/os/arch/riscv/hal/riscv_hal_backend.c b/src/os/arch/riscv/hal/riscv_hal_backend.c index 44510f3b..80be0ea4 100644 --- a/src/os/arch/riscv/hal/riscv_hal_backend.c +++ b/src/os/arch/riscv/hal/riscv_hal_backend.c @@ -1,2 +1,21 @@ -#error "CMRX RISC-V HAL backend is not implemented yet." +#include +/* MIE is bit 3 in mstatus (riscv-privileged architecture document, Figure 3.6, Section 3.1.6.1, p.21). */ +#define CMRX_RISCV_MSTATUS_MIE_MASK (1u << 3u) + +void cmrx_riscv_irq_disable(void) +{ + const uint32_t mstatus = cmrx_riscv_csr_read_mstatus(); + cmrx_riscv_csr_write_mstatus(mstatus & ~CMRX_RISCV_MSTATUS_MIE_MASK); +} + +void cmrx_riscv_irq_enable(void) +{ + const uint32_t mstatus = cmrx_riscv_csr_read_mstatus(); + cmrx_riscv_csr_write_mstatus(mstatus | CMRX_RISCV_MSTATUS_MIE_MASK); +} + +bool cmrx_riscv_irq_is_enabled(void) +{ + return (cmrx_riscv_csr_read_mstatus() & CMRX_RISCV_MSTATUS_MIE_MASK) != 0u; +} diff --git a/src/os/arch/riscv/hal/riscv_hal_backend_csr.c b/src/os/arch/riscv/hal/riscv_hal_backend_csr.c new file mode 100644 index 00000000..6f71f0c7 --- /dev/null +++ b/src/os/arch/riscv/hal/riscv_hal_backend_csr.c @@ -0,0 +1,73 @@ +#include + +uint32_t cmrx_riscv_csr_read_mstatus(void) +{ + uint32_t value = 0u; + __asm__ volatile("csrr %0, mstatus" : "=r"(value)); + return value; +} + +void cmrx_riscv_csr_write_mstatus(uint32_t value) +{ + __asm__ volatile("csrw mstatus, %0" : : "r"(value)); +} + +uint32_t cmrx_riscv_csr_read_mepc(void) +{ + uint32_t value = 0u; + __asm__ volatile("csrr %0, mepc" : "=r"(value)); + return value; +} + +void cmrx_riscv_csr_write_mepc(uint32_t value) +{ + __asm__ volatile("csrw mepc, %0" : : "r"(value)); +} + +uint32_t cmrx_riscv_csr_read_mcause(void) +{ + uint32_t value = 0u; + __asm__ volatile("csrr %0, mcause" : "=r"(value)); + return value; +} + +void cmrx_riscv_csr_write_mcause(uint32_t value) +{ + __asm__ volatile("csrw mcause, %0" : : "r"(value)); +} + +uint32_t cmrx_riscv_csr_read_mtvec(void) +{ + uint32_t value = 0u; + __asm__ volatile("csrr %0, mtvec" : "=r"(value)); + return value; +} + +void cmrx_riscv_csr_write_mtvec(uint32_t value) +{ + __asm__ volatile("csrw mtvec, %0" : : "r"(value)); +} + +uint32_t cmrx_riscv_csr_read_mie(void) +{ + uint32_t value = 0u; + __asm__ volatile("csrr %0, mie" : "=r"(value)); + return value; +} + +void cmrx_riscv_csr_write_mie(uint32_t value) +{ + __asm__ volatile("csrw mie, %0" : : "r"(value)); +} + +uint32_t cmrx_riscv_csr_read_mip(void) +{ + uint32_t value = 0u; + __asm__ volatile("csrr %0, mip" : "=r"(value)); + return value; +} + +void cmrx_riscv_csr_write_mip(uint32_t value) +{ + __asm__ volatile("csrw mip, %0" : : "r"(value)); +} diff --git a/src/os/arch/riscv/rpc.c b/src/os/arch/riscv/rpc.c new file mode 100644 index 00000000..cc899812 --- /dev/null +++ b/src/os/arch/riscv/rpc.c @@ -0,0 +1,20 @@ +#include +#include + +int os_rpc_call(unsigned long arg0, unsigned long arg1, unsigned long arg2, unsigned long arg3) +{ + (void)arg0; + (void)arg1; + (void)arg2; + (void)arg3; + return E_NOTAVAIL; +} + +int os_rpc_return(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint32_t arg3) +{ + (void)arg0; + (void)arg1; + (void)arg2; + (void)arg3; + return E_NOTAVAIL; +} diff --git a/src/os/kernel/tests/CMakeLists.txt b/src/os/kernel/tests/CMakeLists.txt index 90bb709e..da0fd46c 100644 --- a/src/os/kernel/tests/CMakeLists.txt +++ b/src/os/kernel/tests/CMakeLists.txt @@ -49,3 +49,16 @@ add_executable(test_algo ${test_algo_SRCS}) target_link_libraries(test_algo ctest) add_test(NAME test_algo COMMAND test_algo) + +get_property(CMRX_ROOT_DIR GLOBAL PROPERTY CMRX_ROOT_DIR) +set(riscv_hal_backend_test_SRCS + riscv_hal_backend.c + riscv_hal_backend_fake_csr.c + ${CMRX_ROOT_DIR}/src/os/arch/riscv/hal/riscv_hal_backend.c + ) + +add_executable(test_riscv_hal_backend ${riscv_hal_backend_test_SRCS}) +target_include_directories(test_riscv_hal_backend PRIVATE ${CMRX_ROOT_DIR}/include) +target_link_libraries(test_riscv_hal_backend ctest) +add_test(NAME test_riscv_hal_backend COMMAND test_riscv_hal_backend) + diff --git a/src/os/kernel/tests/riscv_hal_backend.c b/src/os/kernel/tests/riscv_hal_backend.c new file mode 100644 index 00000000..0317273f --- /dev/null +++ b/src/os/kernel/tests/riscv_hal_backend.c @@ -0,0 +1,32 @@ +#include + +#include + +/* MIE is bit 3 in mstatus (RISC-V Privileged Specification, Section 3.1.6.1). */ +#define CMRX_RISCV_MSTATUS_MIE_MASK (1u << 3u) + +CTEST(riscv_hal_backend, irq_enable_sets_mie_only) +{ + const uint32_t base = 0x5a5a5a5au & ~CMRX_RISCV_MSTATUS_MIE_MASK; + + cmrx_riscv_csr_write_mstatus(base); + cmrx_riscv_irq_enable(); + + const uint32_t after = cmrx_riscv_csr_read_mstatus(); + ASSERT_EQUAL(after & CMRX_RISCV_MSTATUS_MIE_MASK, CMRX_RISCV_MSTATUS_MIE_MASK); + ASSERT_EQUAL(after & ~CMRX_RISCV_MSTATUS_MIE_MASK, base & ~CMRX_RISCV_MSTATUS_MIE_MASK); + ASSERT_EQUAL(cmrx_riscv_irq_is_enabled(), true); +} + +CTEST(riscv_hal_backend, irq_disable_clears_mie_only) +{ + const uint32_t base = 0xa5a5a5a5u | CMRX_RISCV_MSTATUS_MIE_MASK; + + cmrx_riscv_csr_write_mstatus(base); + cmrx_riscv_irq_disable(); + + const uint32_t after = cmrx_riscv_csr_read_mstatus(); + ASSERT_EQUAL(after & CMRX_RISCV_MSTATUS_MIE_MASK, 0u); + ASSERT_EQUAL(after & ~CMRX_RISCV_MSTATUS_MIE_MASK, base & ~CMRX_RISCV_MSTATUS_MIE_MASK); + ASSERT_EQUAL(cmrx_riscv_irq_is_enabled(), false); +} diff --git a/src/os/kernel/tests/riscv_hal_backend_fake_csr.c b/src/os/kernel/tests/riscv_hal_backend_fake_csr.c new file mode 100644 index 00000000..86df6fd9 --- /dev/null +++ b/src/os/kernel/tests/riscv_hal_backend_fake_csr.c @@ -0,0 +1,26 @@ +#include + +static uint32_t fake_mstatus; +static uint32_t fake_mepc; +static uint32_t fake_mcause; +static uint32_t fake_mtvec; +static uint32_t fake_mie; +static uint32_t fake_mip; + +uint32_t cmrx_riscv_csr_read_mstatus(void) { return fake_mstatus; } +void cmrx_riscv_csr_write_mstatus(uint32_t value) { fake_mstatus = value; } + +uint32_t cmrx_riscv_csr_read_mepc(void) { return fake_mepc; } +void cmrx_riscv_csr_write_mepc(uint32_t value) { fake_mepc = value; } + +uint32_t cmrx_riscv_csr_read_mcause(void) { return fake_mcause; } +void cmrx_riscv_csr_write_mcause(uint32_t value) { fake_mcause = value; } + +uint32_t cmrx_riscv_csr_read_mtvec(void) { return fake_mtvec; } +void cmrx_riscv_csr_write_mtvec(uint32_t value) { fake_mtvec = value; } + +uint32_t cmrx_riscv_csr_read_mie(void) { return fake_mie; } +void cmrx_riscv_csr_write_mie(uint32_t value) { fake_mie = value; } + +uint32_t cmrx_riscv_csr_read_mip(void) { return fake_mip; } +void cmrx_riscv_csr_write_mip(uint32_t value) { fake_mip = value; } From fbf3cf667ab2092b93965a95df3f069f710cad84 Mon Sep 17 00:00:00 2001 From: Tobias Aguiar Date: Sun, 1 Feb 2026 18:05:16 +0100 Subject: [PATCH 5/7] riscv: add Pico SDK integration and scheduler support Add RISC-V architecture support for running on RP2350 with Pico SDK: - quirks/pico-sdk-riscv: Pico SDK RISC-V integration specifics - Custom CRT0 wrapper for IRQ exit context switch hook - ecall exception handler for CMRX syscalls - CMake integration for firmware builds - src/os/arch/riscv: Architecture-specific kernel components - sched.c: Thread stack setup, boot, and sleep primitives - mpu.c: Memory protection stubs (PMP not implemented) - static.c: Static thread/application table accessors - corelocal.h: Single-core lock/unlock via IRQ disable - sysenter.h: ecall-based syscall implementation - cmake/arch/riscv/hal: Build system support - CMRX.cmake: add_firmware() function - cmrx_sections.ld: Linker symbols for static init tables - src/lib/arch/riscv: Library support - mutex.c: Mutex implementation using HAL primitives --- quirks/CMakeLists.txt | 10 +- quirks/pico-sdk-riscv/CMakeLists.txt | 34 +++ quirks/pico-sdk-riscv/README.md | 36 +++ quirks/pico-sdk-riscv/cmrx_ecall_handler.c | 99 +++++++ quirks/pico-sdk-riscv/crt0_riscv_cmrx.S | 111 ++++++++ .../riscv_rp2350_exception_frame.c | 43 +++ .../riscv_rp2350_exception_frame.h | 52 ++++ src/lib/arch/riscv/arch.cmake | 2 + src/lib/arch/riscv/mutex.c | 144 ++++++++++ src/os/arch/riscv/CMakeLists.txt | 3 + src/os/arch/riscv/hal/arch/assert.h | 7 + src/os/arch/riscv/hal/arch/corelocal.h | 49 +++- src/os/arch/riscv/hal/arch/runtime.h | 1 - src/os/arch/riscv/hal/arch/sysenter.h | 34 ++- src/os/arch/riscv/mpu.c | 109 ++++++++ src/os/arch/riscv/sched.c | 258 ++++++++++++++++++ src/os/arch/riscv/static.c | 56 ++++ 17 files changed, 1030 insertions(+), 18 deletions(-) create mode 100644 quirks/pico-sdk-riscv/CMakeLists.txt create mode 100644 quirks/pico-sdk-riscv/README.md create mode 100644 quirks/pico-sdk-riscv/cmrx_ecall_handler.c create mode 100644 quirks/pico-sdk-riscv/crt0_riscv_cmrx.S create mode 100644 quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c create mode 100644 quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h create mode 100644 src/lib/arch/riscv/arch.cmake create mode 100644 src/lib/arch/riscv/mutex.c create mode 100644 src/os/arch/riscv/hal/arch/assert.h create mode 100644 src/os/arch/riscv/mpu.c create mode 100644 src/os/arch/riscv/sched.c create mode 100644 src/os/arch/riscv/static.c diff --git a/quirks/CMakeLists.txt b/quirks/CMakeLists.txt index 6cbecc3d..07706580 100644 --- a/quirks/CMakeLists.txt +++ b/quirks/CMakeLists.txt @@ -4,7 +4,11 @@ if (TARGET pico_stdlib) if (NOT TARGET cmrx) message(FATAL_ERROR "CMRX target does not exist yet! Include this quirk after CMRX directory has been added!") endif() - - add_subdirectory(pico-sdk) - + + if (DEFINED PICO_PLATFORM AND PICO_PLATFORM STREQUAL "rp2350-riscv") + add_subdirectory(pico-sdk-riscv) + else() + add_subdirectory(pico-sdk) + endif() + endif() diff --git a/quirks/pico-sdk-riscv/CMakeLists.txt b/quirks/pico-sdk-riscv/CMakeLists.txt new file mode 100644 index 00000000..2c7eefdc --- /dev/null +++ b/quirks/pico-sdk-riscv/CMakeLists.txt @@ -0,0 +1,34 @@ +# CMRX quirk for Pico SDK RISC-V (RP2350) +# +# Provides: +# - Replacement external IRQ handler that calls CMRX context switch safe-point +# - ecall exception handler for CMRX syscalls + +if(NOT DEFINED PICO_PLATFORM) + return() +endif() + +if(NOT PICO_PLATFORM STREQUAL "rp2350-riscv") + return() +endif() + +message(STATUS "CMRX: Pico SDK RISC-V quirk enabled") + +# Add the CMRX RISC-V handlers to the OS library +target_sources(os PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/cmrx_ecall_handler.c + ${CMAKE_CURRENT_LIST_DIR}/riscv_rp2350_exception_frame.c +) + +# Ensure headers are accessible: +# - CMRX context switch header +# - Pico SDK headers for the assembly file (pico.h, hardware/regs/rvcsr.h) +target_include_directories(os PRIVATE + ${CMRX_ROOT_DIR}/include +) + +# Link against pico_base_headers to get Pico SDK includes +target_link_libraries(os PRIVATE + pico_base_headers + hardware_regs +) diff --git a/quirks/pico-sdk-riscv/README.md b/quirks/pico-sdk-riscv/README.md new file mode 100644 index 00000000..276af534 --- /dev/null +++ b/quirks/pico-sdk-riscv/README.md @@ -0,0 +1,36 @@ +# Pico SDK RISC-V Quirk + +This directory contains integration code for running CMRX on RP2350 RISC-V using Pico SDK. + +## Problem + +The CMRX RISC-V context switcher uses a "safe-point" model where pending context switches +are checked and executed at defined boundaries (typically trap/IRQ exit). The portable +RISC-V code provides the safe-point function but does not hook it into any specific +trap entry/exit mechanism. + +On Pico SDK for RISC-V, the external IRQ handler (`isr_riscv_machine_external_irq`) is +declared weak in `crt0_riscv.S`, allowing it to be overridden. + +## Solution + +This quirk provides a replacement `isr_riscv_machine_external_irq` that: + +1. Performs the same IRQ dispatch as the original Pico SDK handler +2. Calls `os_riscv_context_switch_safe_point()` after all IRQs are dispatched +3. Restores context and returns via `mret` + +The safe-point call happens at a well-defined boundary where: +- All IRQs have been serviced +- IRQs are globally disabled (`mstatus.MIE` = 0) +- Caller-saved registers are saved on the stack + +## Files + +- `crt0_riscv_cmrx.S` - Replacement external IRQ handler with safe-point hook +- `CMakeLists.txt` - Build integration (activated when `PICO_PLATFORM=rp2350-riscv`) + +## Usage + +This quirk is automatically enabled when building CMRX for Pico SDK RISC-V. Ensure the +quirks subdirectory is included after the main CMRX target is defined. diff --git a/quirks/pico-sdk-riscv/cmrx_ecall_handler.c b/quirks/pico-sdk-riscv/cmrx_ecall_handler.c new file mode 100644 index 00000000..8456030e --- /dev/null +++ b/quirks/pico-sdk-riscv/cmrx_ecall_handler.c @@ -0,0 +1,99 @@ +/* + * CMRX RISC-V ecall (syscall) handler for Pico SDK (RP2350). + * + * Implements isr_riscv_machine_ecall_mmode_exception to handle syscalls + * triggered by the ecall instruction from M-mode code. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include "riscv_rp2350_exception_frame.h" + +/* Forward declaration - defined in kernel */ +extern int os_system_call(uint32_t arg0, uint32_t arg1, uint32_t arg2, + uint32_t arg3, uint8_t syscall_id); + +/* + * RISC-V Privileged Specification: + * - Table 3.6: mcause=11 identifies environment call from M-mode + * - Section 3.1.14: mepc points at ecall; software advances by 4 to continue + */ + +/* + * C helper to dispatch the syscall. + * Called from the assembly handler with a pointer to the exception frame. + * + * @param frame Pointer to the exception frame on stack + * @return syscall return value (to be written to a0 in frame) + */ +int cmrx_ecall_dispatch(RiscvRp2350ExceptionFrame *frame) +{ + uint32_t arg0 = riscv_rp2350_exception_get_arg(frame, 0); + uint32_t arg1 = riscv_rp2350_exception_get_arg(frame, 1); + uint32_t arg2 = riscv_rp2350_exception_get_arg(frame, 2); + uint32_t arg3 = riscv_rp2350_exception_get_arg(frame, 3); + uint8_t syscall_id = riscv_rp2350_exception_get_syscall_id(frame); + + return os_system_call(arg0, arg1, arg2, arg3, syscall_id); +} + +void cmrx_ecall_dispatch_writeback(RiscvRp2350ExceptionFrame *frame) +{ + int retval = cmrx_ecall_dispatch(frame); + riscv_rp2350_exception_set_retval(frame, retval); +} + +/* + * RISC-V ecall exception handler. + * + * This overrides the weak isr_riscv_machine_ecall_mmode_exception + * from Pico SDK's exception_table_riscv.S. + * + * When called by the exception dispatch code: + * - sp points to the exception frame (caller-saved regs already saved) + * - mepc points to the ecall instruction + * - We must increment mepc by 4 before returning + * + * After dispatching the syscall, we call the context switch safe-point + * to allow a pending context switch to occur (e.g., after usleep()). + * + * This handler is marked naked to have full control over the stack. + * + * See RISC-V Privileged Specification, Section 3.1.14 for mepc behavior. + */ +__attribute__((naked)) void isr_riscv_machine_ecall_mmode_exception(void) +{ + __asm__ volatile( + /* Save ra - we'll make calls */ + "addi sp, sp, -16\n\t" + "sw ra, 0(sp)\n\t" + "sw s0, 4(sp)\n\t" + + /* Save exception frame pointer in s0 */ + /* Exception frame is at sp + 16 (our frame size) */ + "addi s0, sp, 16\n\t" + + /* Increment mepc by 4 to skip past ecall instruction */ + "call riscv_rp2350_exception_advance_mepc_by_4\n\t" + + /* Call C helper to dispatch syscall and write a0 into frame */ + /* a0 = exception frame pointer */ + "mv a0, s0\n\t" + "call cmrx_ecall_dispatch_writeback\n\t" + + /* Call context switch safe-point */ + /* This allows pending context switches after syscalls like usleep() */ + "call os_riscv_context_switch_safe_point\n\t" + + /* Restore ra and s0, return to exception dispatcher */ + "lw ra, 0(sp)\n\t" + "lw s0, 4(sp)\n\t" + "addi sp, sp, 16\n\t" + "ret\n\t" + : + : + : "memory" + ); +} diff --git a/quirks/pico-sdk-riscv/crt0_riscv_cmrx.S b/quirks/pico-sdk-riscv/crt0_riscv_cmrx.S new file mode 100644 index 00000000..7e712765 --- /dev/null +++ b/quirks/pico-sdk-riscv/crt0_riscv_cmrx.S @@ -0,0 +1,111 @@ +/* + * CMRX RISC-V IRQ handler wrapper for Pico SDK (RP2350). + * + * Provides a replacement for isr_riscv_machine_external_irq that calls the + * CMRX context switch safe-point at the IRQ exit boundary. + * + * This file is derived from pico-sdk crt0_riscv.S (BSD-3-Clause). + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "pico.h" +#include "hardware/regs/rvcsr.h" + +.section .time_critical.cmrx_irq_handler, "ax" + +/* + * External IRQ handler with CMRX safe-point hook. + * + * This replaces the weak isr_riscv_machine_external_irq from crt0_riscv.S. + * The structure mirrors the original but adds a call to the context switch + * safe-point before returning from the IRQ. + */ +.global isr_riscv_machine_external_irq +isr_riscv_machine_external_irq: + addi sp, sp, -80 + sw ra, 0(sp) + sw t0, 4(sp) + sw t1, 8(sp) + sw t2, 12(sp) + sw a0, 16(sp) + sw a1, 20(sp) + sw a2, 24(sp) + sw a3, 28(sp) + sw a4, 32(sp) + sw a5, 36(sp) + sw a6, 40(sp) + sw a7, 44(sp) + sw t3, 48(sp) + sw t4, 52(sp) + sw t5, 56(sp) + sw t6, 60(sp) + csrr a0, mepc + csrr a1, mstatus + sw a0, 64(sp) + sw a1, 68(sp) +cmrx_save_meicontext: + csrrsi a2, RVCSR_MEICONTEXT_OFFSET, RVCSR_MEICONTEXT_CLEARTS_BITS + sw a2, 72(sp) + +cmrx_get_first_irq: + csrrsi a0, RVCSR_MEINEXT_OFFSET, RVCSR_MEINEXT_UPDATE_BITS + bltz a0, cmrx_no_more_irqs +cmrx_dispatch_irq: + csrsi mstatus, 0x8 + lui a1, %hi(__soft_vector_table) + add a1, a1, a0 + lw a1, %lo(__soft_vector_table)(a1) + jalr ra, a1 + csrci mstatus, 0x8 +cmrx_get_next_irq: + csrrsi a0, RVCSR_MEINEXT_OFFSET, RVCSR_MEINEXT_UPDATE_BITS + bgez a0, cmrx_dispatch_irq + +cmrx_no_more_irqs: + /* + * All IRQs dispatched. Before restoring context and returning, + * call the CMRX context switch safe-point. This allows a pending + * context switch (requested by the scheduler during IRQ handling) + * to be performed now, at a safe boundary. + * + * The safe-point function preserves s0-s11 per RISC-V calling convention. + * Caller-saved registers (a0, t0, etc.) may be clobbered, but we restore + * them from the stack frame below, so this is safe. + */ + call os_riscv_context_switch_safe_point + + /* Restore saved context and return from IRQ */ + lw a0, 64(sp) + lw a1, 68(sp) + lw a2, 72(sp) + csrw mepc, a0 + csrw mstatus, a1 + csrw RVCSR_MEICONTEXT_OFFSET, a2 + lw ra, 0(sp) + lw t0, 4(sp) + lw t1, 8(sp) + lw t2, 12(sp) + /* skip a0 for now */ + lw a1, 20(sp) + lw a2, 24(sp) + lw a3, 28(sp) + lw a4, 32(sp) + lw a5, 36(sp) + lw a6, 40(sp) + lw a7, 44(sp) + lw t3, 48(sp) + lw t4, 52(sp) + lw t5, 56(sp) + lw t6, 60(sp) + +cmrx_check_irq_before_exit: + /* + * Optimization: check for new IRQ before completing the mret sequence. + * If an IRQ arrived while we were in the safe-point, handle it now + * rather than taking the mret->enter->save latency. + */ + csrr a0, RVCSR_MEINEXT_OFFSET + bgez a0, cmrx_save_meicontext + lw a0, 16(sp) + addi sp, sp, 80 + mret diff --git a/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c b/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c new file mode 100644 index 00000000..cb2aa8d0 --- /dev/null +++ b/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c @@ -0,0 +1,43 @@ +#include "riscv_rp2350_exception_frame.h" + +uint32_t riscv_rp2350_exception_get_arg(const RiscvRp2350ExceptionFrame *frame, unsigned argno) +{ + switch (argno) { + case 0: + return frame->a0; + case 1: + return frame->a1; + case 2: + return frame->a2; + case 3: + return frame->a3; + default: + return 0; + } +} + +uint8_t riscv_rp2350_exception_get_syscall_id(const RiscvRp2350ExceptionFrame *frame) +{ + return (uint8_t)frame->a7; +} + +void riscv_rp2350_exception_set_retval(RiscvRp2350ExceptionFrame *frame, int32_t retval) +{ + frame->a0 = (uint32_t)retval; +} + +void riscv_rp2350_exception_advance_mepc_by_4(void) +{ + uint32_t mepc; + + __asm__ volatile( + "csrr %0, mepc\n\t" + : "=r"(mepc) + ); + mepc += 4u; + __asm__ volatile( + "csrw mepc, %0\n\t" + : + : "r"(mepc) + ); +} diff --git a/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h b/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h new file mode 100644 index 00000000..bea86ac5 --- /dev/null +++ b/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +/* + * RP2350 (Hazard3) machine exception frame as saved by Pico SDK + * exception entry in external/pico-sdk/src/rp2_common/hardware_exception/exception_table_riscv.S. + * + * The layout below matches stores at offsets 0..60 bytes: + * 0 : unused (ra is held in mscratch during handler execution) + * 4 : t0 + * 8 : t1 + * 12 : t2 + * 16 : a0 + * 20 : a1 + * 24 : a2 + * 28 : a3 + * 32 : a4 + * 36 : a5 + * 40 : a6 + * 44 : a7 + * 48 : t3 + * 52 : t4 + * 56 : t5 + * 60 : t6 + */ +typedef struct { + uint32_t ra_slot_unused; + uint32_t t0; + uint32_t t1; + uint32_t t2; + uint32_t a0; + uint32_t a1; + uint32_t a2; + uint32_t a3; + uint32_t a4; + uint32_t a5; + uint32_t a6; + uint32_t a7; + uint32_t t3; + uint32_t t4; + uint32_t t5; + uint32_t t6; +} RiscvRp2350ExceptionFrame; + +_Static_assert(sizeof(RiscvRp2350ExceptionFrame) == (16u * sizeof(uint32_t)), + "RP2350 exception frame must be 16 words"); + +uint32_t riscv_rp2350_exception_get_arg(const RiscvRp2350ExceptionFrame *frame, unsigned argno); +uint8_t riscv_rp2350_exception_get_syscall_id(const RiscvRp2350ExceptionFrame *frame); +void riscv_rp2350_exception_set_retval(RiscvRp2350ExceptionFrame *frame, int32_t retval); +void riscv_rp2350_exception_advance_mepc_by_4(void); diff --git a/src/lib/arch/riscv/arch.cmake b/src/lib/arch/riscv/arch.cmake new file mode 100644 index 00000000..c30cc70e --- /dev/null +++ b/src/lib/arch/riscv/arch.cmake @@ -0,0 +1,2 @@ +# Customization of stdlib target for RISC-V architecture +# (placeholder for future RISC-V specific dependencies) diff --git a/src/lib/arch/riscv/mutex.c b/src/lib/arch/riscv/mutex.c new file mode 100644 index 00000000..fcf2fa74 --- /dev/null +++ b/src/lib/arch/riscv/mutex.c @@ -0,0 +1,144 @@ +/* + * RISC-V mutex/futex implementation for CMRX. + * + * Uses LR.W/SC.W (load-reserved/store-conditional) from the RISC-V A + * extension for atomic fast-path lock and unlock. + * + * futex_t must be placed at a 4-byte aligned address for LR.W/SC.W to + * operate correctly. + */ + +#if defined(__riscv_atomic) || defined(__riscv_a) + +#include +#include +#include +#include +#include + +/** Lock futex. + * Perform atomic futex lock using LR.W/SC.W. It is only possible to lock an + * unlocked mutex. Locking may fail spuriously due to hardware specifics of + * load-reserved/store-conditional. Recursive mutexes are not supported. + * @param futex futex to be locked + * @param thread_id identification of calling thread + * @param max_depth unused + * @returns 0 if futex lock was successful, 1 if locking failed for whatever reason + */ +int __futex_fast_lock(futex_t *futex, uint8_t thread_id, unsigned max_depth) +{ + uint32_t loaded, stored, new_val; + (void)max_depth; + + uint32_t *word = (uint32_t *)(uintptr_t)futex; + + __asm__ volatile ("lr.w %0, (%1)" + : "=r"(loaded) : "r"(word) : "memory"); + + if ((loaded & 0xFFu) != 0) { + return FUTEX_FAILURE; + } + + new_val = (loaded & ~0xFFFFu) | ((uint32_t)thread_id << 8) | 1u; + + __asm__ volatile ("sc.w %0, %2, (%1)" + : "=r"(stored) : "r"(word), "r"(new_val) : "memory"); + + return (stored == 0) ? FUTEX_SUCCESS : FUTEX_FAILURE; +} + +/** Unlock futex. + * This function performs fast unlock of futex if that is possible. + * If mutex is not locked, this action will fail. It is an undefined + * behavior to unlock mutex locked by another thread. + * @param futex Futex to be unlocked + * @param thread_id Numeric identification of futex owner + * @returns 0 if futex unlock was successful, 1 if unlocking failed for + * whatever reason. + */ +int __futex_fast_unlock(futex_t *futex, uint8_t thread_id) +{ + uint32_t loaded, stored, new_val; + + uint32_t *word = (uint32_t *)(uintptr_t)futex; + + __asm__ volatile ("lr.w %0, (%1)" + : "=r"(loaded) : "r"(word) : "memory"); + + uint8_t state = (uint8_t)(loaded & 0xFFu); + if (state == 0) { + return FUTEX_FAILURE; + } + + ASSERT(((loaded >> 8) & 0xFFu) == thread_id); + if (((loaded >> 8) & 0xFFu) != thread_id) { + return FUTEX_FAILURE; + } + + new_val = (loaded & ~0xFFu) | (state - 1u); + + __asm__ volatile ("sc.w %0, %2, (%1)" + : "=r"(stored) : "r"(word), "r"(new_val) : "memory"); + + return (stored == 0) ? FUTEX_SUCCESS : FUTEX_FAILURE; +} + +int futex_init(futex_t *restrict futex) +{ + ASSERT(futex != NULL); + futex->owner = 0xFF; + futex->state = 0; + return 0; +} + +int futex_lock(futex_t *futex) +{ + ASSERT(futex != NULL); + uint8_t thread_id = get_tid(); + int success; + do { + success = __futex_fast_lock(futex, thread_id, 0); + if (success != FUTEX_SUCCESS) { + wait_for_object_value(&futex->state, 0, 0, + NOTIFY_PRIORITY_INHERIT(futex->owner)); + } + } while (success != FUTEX_SUCCESS); + return 0; +} + +int futex_trylock(futex_t *futex) +{ + ASSERT(futex != NULL); + uint8_t thread_id = get_tid(); + return __futex_fast_lock(futex, thread_id, 0); +} + +int futex_unlock(futex_t *futex) +{ + ASSERT(futex != NULL); + uint8_t thread_id = get_tid(); + int success; + do { + success = __futex_fast_unlock(futex, thread_id); + if (success == FUTEX_SUCCESS) { + notify_object2(&futex->state, NOTIFY_PRIORITY_DROP); + } + } while (success != FUTEX_SUCCESS); + return 0; +} + +int futex_destroy(futex_t *futex) +{ + ASSERT(futex != NULL); + futex->state = 0; + futex->owner = 0xFF; + return 0; +} + +#else + +#error "RISC-V A extension (atomics) is required for the LR.W/SC.W-based mutex. \ + Target -march= does not include 'a'. Either enable the A extension or \ + provide a non-atomic fallback implementation." + +#endif \ No newline at end of file diff --git a/src/os/arch/riscv/CMakeLists.txt b/src/os/arch/riscv/CMakeLists.txt index 0e8a2c05..1dc46972 100644 --- a/src/os/arch/riscv/CMakeLists.txt +++ b/src/os/arch/riscv/CMakeLists.txt @@ -3,7 +3,10 @@ set(cmrx_riscv_SRCS hal/riscv_hal_backend_csr.c context_switch_core.c context_switch_request.c + sched.c + mpu.c rpc.c + static.c ) target_sources(os PRIVATE ${cmrx_riscv_SRCS}) diff --git a/src/os/arch/riscv/hal/arch/assert.h b/src/os/arch/riscv/hal/arch/assert.h new file mode 100644 index 00000000..da19ea0b --- /dev/null +++ b/src/os/arch/riscv/hal/arch/assert.h @@ -0,0 +1,7 @@ +#pragma once + +#define ASSERT(cond) \ +if (!(cond)) \ +{\ + asm volatile("ebreak\n\t");\ +} diff --git a/src/os/arch/riscv/hal/arch/corelocal.h b/src/os/arch/riscv/hal/arch/corelocal.h index 80c84a2f..12e1d134 100644 --- a/src/os/arch/riscv/hal/arch/corelocal.h +++ b/src/os/arch/riscv/hal/arch/corelocal.h @@ -1,23 +1,52 @@ #pragma once #include +#include -/* Portability layer header for includes. */ - -#ifndef CMRX_ARCH_SMP_SUPPORTED -#define OS_NUM_CORES 1 -#endif +/* + * RISC-V corelocal portability layer. + * + * For single-core builds, these are simple macros. + * For SMP builds, actual implementations would be needed. + */ typedef void (*callback_t)(); -extern unsigned coreid(); -extern void os_core_lock(); -extern void os_core_unlock(); +extern void os_core_sleep(void); -extern void os_smp_lock(); -extern void os_smp_unlock(); +#ifndef CMRX_ARCH_SMP_SUPPORTED + +# define coreid() 0 +# define OS_NUM_CORES 1 +# define os_smp_lock() +# define os_smp_unlock() + +/* + * Core lock/unlock via interrupt disable/enable. + * Uses RISC-V HAL primitives. + */ +static inline void os_core_lock(void) +{ + cmrx_riscv_irq_disable(); +} + +static inline void os_core_unlock(void) +{ + cmrx_riscv_irq_enable(); +} + +#else + +/* SMP mode - not implemented yet */ +extern unsigned coreid(void); +extern void os_core_lock(void); +extern void os_core_unlock(void); +extern void os_smp_lock(void); +extern void os_smp_unlock(void); #ifndef OS_NUM_CORES #error "Macro OS_NUM_CORES is not defined. Use -DOS_NUM_CORES=x to tell the CMRX kernel how many cores it manages!" #endif +#endif /* CMRX_ARCH_SMP_SUPPORTED */ + diff --git a/src/os/arch/riscv/hal/arch/runtime.h b/src/os/arch/riscv/hal/arch/runtime.h index ab635304..22ac069e 100644 --- a/src/os/arch/riscv/hal/arch/runtime.h +++ b/src/os/arch/riscv/hal/arch/runtime.h @@ -1,7 +1,6 @@ #pragma once /* No special handling yet; platform integration will define this as needed. */ -#define os_thread_initialize_arch(...) struct Arch_State_t { /* intentionally left empty */ diff --git a/src/os/arch/riscv/hal/arch/sysenter.h b/src/os/arch/riscv/hal/arch/sysenter.h index a178a838..fb5e2ac3 100644 --- a/src/os/arch/riscv/hal/arch/sysenter.h +++ b/src/os/arch/riscv/hal/arch/sysenter.h @@ -1,8 +1,34 @@ #pragma once -#include +/* + * RISC-V syscall implementation. + * + * Uses standard RISC-V syscall convention: + * - Syscall number in a7 + * - Arguments in a0-a3 (already there from C calling convention) + * - Return value in a0 + * + * The ecall instruction causes mcause=11 (environment call from M-mode) + * which is handled by isr_riscv_machine_ecall_mmode_exception. + */ -/* Syscall/trap entry is platform-specific. */ -#define __SYSCALL -#define __SVC(x) return E_NOTAVAIL; +#define __SYSCALL __attribute__((naked)) __attribute__((noinline)) + +/* + * Perform syscall via ecall instruction. + * @param no syscall number (placed in a7 before ecall) + * + * The function arguments are already in a0-a3 per RISC-V calling convention. + * We load the syscall number into a7, execute ecall, then return. + * The ecall handler will place the return value in a0. + */ +#define __SVC(no, ...) \ + asm volatile( \ + "li a7, %[syscall_id]\n\t" \ + "ecall\n\t" \ + "ret\n\t" \ + : \ + : [syscall_id] "i" (no) \ + : "a7" \ + ) diff --git a/src/os/arch/riscv/mpu.c b/src/os/arch/riscv/mpu.c new file mode 100644 index 00000000..17b78b47 --- /dev/null +++ b/src/os/arch/riscv/mpu.c @@ -0,0 +1,109 @@ +/* + * CMRX RISC-V Memory Protection stubs. + * + * RISC-V uses PMP (Physical Memory Protection) instead of MPU. + * This file provides stub implementations to allow the kernel to build + * without actual memory protection. + * + * Memory protection for RISC-V is NOT implemented in this port. + * These stubs allow the kernel to link and run without protection. + */ + +#include +#include +#include +#include +#include +#include + +/* + * Restore MPU state. + * Stub - does nothing since PMP is not implemented. + */ +int mpu_restore(const MPU_State *hosted_state, const MPU_State *parent_state) +{ + (void)hosted_state; + (void)parent_state; + return E_OK; +} + +/* + * Initialize stack protection for a thread. + * Stub - does nothing since PMP is not implemented. + */ +int mpu_init_stack(int thread_id) +{ + (void)thread_id; + return E_OK; +} + +/* + * Start memory protection. + * Stub - does nothing since PMP is not implemented. + */ +void os_memory_protection_start(void) +{ + /* PMP not implemented */ +} + +/* + * Stop memory protection. + * Stub - does nothing since PMP is not implemented. + */ +void os_memory_protection_stop(void) +{ + /* PMP not implemented */ +} + +/* + * Set an MPU region. + * Stub - does nothing since PMP is not implemented. + */ +int mpu_set_region(uint8_t region, const void *base, uint32_t size, uint8_t cls) +{ + (void)region; + (void)base; + (void)size; + (void)cls; + return E_OK; +} + +/* + * Configure an MPU region. + * Stub - does nothing since PMP is not implemented. + */ +int mpu_configure_region(uint8_t region, const void *base, uint32_t size, + uint8_t cls, uint32_t *RBAR, uint32_t *RASR) +{ + (void)region; + (void)base; + (void)size; + (void)cls; + + if (RBAR) *RBAR = 0; + if (RASR) *RASR = 0; + + return E_OK; +} + +/* + * Clear an MPU region. + * Stub - does nothing since PMP is not implemented. + */ +int mpu_clear_region(uint8_t region) +{ + (void)region; + return E_OK; +} + +/* + * Check if an address is within MPU bounds. + * Stub - always returns false since PMP is not implemented. + */ +bool mpu_check_bounds(const MPU_State *state, uint8_t region, uint32_t *address) +{ + (void)state; + (void)region; + (void)address; + return false; +} diff --git a/src/os/arch/riscv/sched.c b/src/os/arch/riscv/sched.c new file mode 100644 index 00000000..1f5baebf --- /dev/null +++ b/src/os/arch/riscv/sched.c @@ -0,0 +1,258 @@ +/* + * CMRX RISC-V scheduler architecture support. + * + * Provides RISC-V implementations for scheduler primitives: + * - Thread stack population + * - Thread boot + * - Kernel shutdown + * - Syscall return value manipulation + * + */ + +#include +#include +#include +#include +#include +#include + +/* + * Populate stack of new thread so it can be executed. + * + * Sets up the stack frame so that when the thread is first scheduled, + * it will start executing at the entrypoint with 'data' as argument. + * + * RISC-V calling convention (psABI): + * - a0: first argument (data pointer) + * - ra: return address (thread dispose handler) + * - Stack must be 16-byte aligned + * + * Stack layout for first context switch (os_riscv_context_switch_perform): + * The context switch saves/restores s0-s11 (12 words = 48 bytes). + * For initial boot, we also need the exception-like frame that + * os_boot_thread will use. + * + */ +uint32_t *os_thread_populate_stack(int stack_id, unsigned stack_size, + entrypoint_t *entrypoint, void *data) +{ + uint32_t *stack = os_stack_get(stack_id); + + /* + * Initial stack layout (from top, stack_size words): + * + * stack[stack_size - 1] : (padding for 16-byte alignment if needed) + * stack[stack_size - 2] : Initial PC (entrypoint) + * stack[stack_size - 3] : Initial ra (os_thread_dispose) + * stack[stack_size - 4] : Initial a0 (data) + * stack[stack_size - 5] : Reserved + * stack[stack_size - 6] : Reserved + * stack[stack_size - 7] : Reserved + * stack[stack_size - 8] : Reserved + * stack[stack_size - 9] : s11 + * stack[stack_size - 10] : s10 + * stack[stack_size - 11] : s9 + * stack[stack_size - 12] : s8 + * stack[stack_size - 13] : s7 + * stack[stack_size - 14] : s6 + * stack[stack_size - 15] : s5 + * stack[stack_size - 16] : s4 + * stack[stack_size - 17] : s3 + * stack[stack_size - 18] : s2 + * stack[stack_size - 19] : s1 + * stack[stack_size - 20] : s0 + * + * SP will point to stack[stack_size - 20] + */ + + /* Clear the stack area we'll use */ + for (unsigned i = 0; i < 20; i++) { + stack[stack_size - 1 - i] = 0; + } + + /* Set up initial register values */ + stack[stack_size - 2] = (uint32_t)entrypoint; /* Initial PC */ + stack[stack_size - 3] = (uint32_t)os_thread_dispose; /* ra - return to dispose */ + stack[stack_size - 4] = (uint32_t)data; /* a0 - argument */ + + /* s0-s11 are all zero (callee-saved, will be restored by context switch) */ + + /* Return SP pointing to where context switch expects it */ + return &stack[stack_size - 20]; +} + +/** Platform-specific way of initializing threads. + * Populates the stack of the thread with the initial values. + * @param thread pointer to the thread to initialize + * @param stack_size size of the stack + * @param entrypoint address of the entrypoint + * @param data pointer to the data + */ +void os_thread_initialize_arch(struct OS_thread_t * thread, unsigned stack_size, entrypoint_t * entrypoint, void * data) +{ + thread->sp = os_thread_populate_stack(thread->stack_id, stack_size, entrypoint, data); +} + +/* + * Create a process. + * + * For RISC-V without PMP/MPU implementation, this is a minimal stub + * that just records the process definition. + */ +int os_process_create(Process_t process_id, + const struct OS_process_definition_t *definition) +{ + if (process_id >= OS_PROCESSES) { + return E_OUT_OF_RANGE; + } + + if (os_processes[process_id].definition != NULL) { + return E_INVALID; + } + + os_processes[process_id].definition = definition; + + /* No MPU configuration - PMP not implemented */ + + return E_OK; +} + +/* + * Boot the first thread. + * + * This function never returns. It sets up the CPU state and jumps + * to the thread's entry point. + * + * For RISC-V, we: + * 1. Load the thread's stack pointer + * 2. Pop the initial register values + * 3. Jump to the entry point + */ +__attribute__((noreturn)) +void os_boot_thread(Thread_t boot_thread) +{ + struct OS_thread_t *thread = os_thread_get(boot_thread); + uint32_t *thread_sp = thread->sp; + + /* + * The stack was set up by os_thread_populate_stack. + * Layout at thread_sp: + * +0: s0 + * +4: s1 + * ... + * +44: s11 + * +48: reserved + * +52: reserved + * +56: reserved + * +60: a0 (argument) + * +64: ra (return address = os_thread_dispose) + * +68: PC (entry point) + * + * We need to: + * 1. Skip past s0-s11 (they're just placeholders for first boot) + * 2. Load a0, ra + * 3. Jump to PC + */ + + __asm__ volatile( + /* Set stack pointer to thread stack */ + "mv sp, %[sp]\n\t" + + /* Skip past callee-saved registers (s0-s11 = 12*4 = 48 bytes) */ + "addi sp, sp, 48\n\t" + + /* Skip reserved area (4 words = 16 bytes) */ + "addi sp, sp, 16\n\t" + + /* Load a0 (first argument) */ + "lw a0, 0(sp)\n\t" + + /* Load ra (return address) */ + "lw ra, 4(sp)\n\t" + + /* Load entry point into t0 */ + "lw t0, 8(sp)\n\t" + + /* Adjust sp past our boot frame */ + "addi sp, sp, 16\n\t" + + /* Jump to entry point */ + "jr t0\n\t" + : + : [sp] "r"(thread_sp) + : "memory" + ); + + /* Never reached */ + __builtin_unreachable(); +} + +/* + * Default CMRX shutdown handler. + * This can be overridden by the application. + */ +__attribute__((weak, noreturn)) +void cmrx_shutdown_handler(void) +{ + /* Default: infinite loop */ + while (1) { + __asm__ volatile("wfi"); + } +} + +/* + * Perform the kernel shutdown. + * Disables interrupts and calls the shutdown handler. + */ +__attribute__((noreturn)) +void os_kernel_shutdown(void) +{ + os_core_lock(); /* Disable interrupts */ + cmrx_shutdown_handler(); + + /* Never reached */ + __builtin_unreachable(); +} + +/* + * Set the return value for a syscall on a specific thread. + * + * This is used for async syscalls where the return value is set + * after the thread has been blocked and will resume later. + * + * For RISC-V, we need to modify a0 in the thread's saved context. + */ +int os_set_syscall_return_value(Thread_t thread_id, int32_t retval) +{ + struct OS_thread_t *thread = os_thread_get(thread_id); + if (thread == NULL) { + return E_INVALID; + } + + /* + * The thread's SP points to its saved context. + * For RISC-V, the ecall handler saves the exception frame on stack. + * The return value goes in a0. + * + * However, the exact stack layout depends on whether the thread + * was blocked during ecall (exception frame) or IRQ (different frame). + * + * For now, this is a stub - proper implementation needs to understand + * the exact stack layout for each case. + */ + + /* TODO: Proper implementation based on stack frame layout */ + (void)thread; + (void)retval; + + return E_OK; +} + +/* + * Put the CPU core to sleep. + * Uses RISC-V WFI (Wait For Interrupt) instruction. + */ +void os_core_sleep(void) +{ + __asm__ volatile("wfi"); +} diff --git a/src/os/arch/riscv/static.c b/src/os/arch/riscv/static.c new file mode 100644 index 00000000..281b197f --- /dev/null +++ b/src/os/arch/riscv/static.c @@ -0,0 +1,56 @@ +/* + * CMRX RISC-V static initialization. + * + * Provides functions to retrieve static initialization structures + * for threads and processes defined at compile time. + * + * These functions rely on linker-generated symbols that mark the + * boundaries of the application and thread tables. + */ + +#include +#include + +/* + * Linker-generated symbols marking the boundaries of the + * statically-defined application and thread tables. + * + * These are populated by the OS_APPLICATION and OS_THREAD_CREATE macros. + */ +extern const struct OS_process_definition_t __applications_start; +extern const struct OS_process_definition_t __applications_end; + +extern const struct OS_thread_create_t __thread_create_start; +extern const struct OS_thread_create_t __thread_create_end; + +/* + * Get the count of statically-defined threads. + */ +unsigned static_init_thread_count(void) +{ + return &__thread_create_end - &__thread_create_start; +} + +/* + * Get pointer to the static thread creation table. + */ +const struct OS_thread_create_t *static_init_thread_table(void) +{ + return &__thread_create_start; +} + +/* + * Get the count of statically-defined processes/applications. + */ +unsigned static_init_process_count(void) +{ + return &__applications_end - &__applications_start; +} + +/* + * Get pointer to the static process definition table. + */ +const struct OS_process_definition_t *static_init_process_table(void) +{ + return &__applications_start; +} From 11dfd5b5a461a648d4d6521b9c4a1609154007e2 Mon Sep 17 00:00:00 2001 From: Tobias Aguiar Date: Sun, 1 Mar 2026 13:07:20 +0100 Subject: [PATCH 6/7] riscv: unify ExceptionFrame for trap handlers and context switching Add arch-level ExceptionFrame (32 words / 128 bytes) covering all GP registers, mepc, and mstatus. Simplify context switch to a pure SP swap; full register save/restore is now the trap handler's responsibility. Update os_thread_populate_stack to build an ExceptionFrame, os_boot_thread to use mret for consistent trap-return entry, and os_set_syscall_return_value to write a0 via the unified frame. Adjust unit tests accordingly. --- include/cmrx/arch/riscv/context_switch.h | 7 +- include/cmrx/arch/riscv/exception_frame.h | 168 ++++++++++++++++++ src/os/arch/riscv/context_switch_core.c | 42 +---- .../riscv/hal/testing/riscv_context_switch.c | 14 +- .../hal/testing/riscv_context_switch_fake.c | 15 +- src/os/arch/riscv/sched.c | 153 ++++------------ 6 files changed, 216 insertions(+), 183 deletions(-) create mode 100644 include/cmrx/arch/riscv/exception_frame.h diff --git a/include/cmrx/arch/riscv/context_switch.h b/include/cmrx/arch/riscv/context_switch.h index cef6b53e..b37bf150 100644 --- a/include/cmrx/arch/riscv/context_switch.h +++ b/include/cmrx/arch/riscv/context_switch.h @@ -2,12 +2,7 @@ #include #include - -/* Context frame size keeps stack 16-byte aligned (riscv-abi documentation, 2.2 Hardware - * Floating-point Calling Convention section). - */ -#define CMRX_RISCV_CONTEXT_FRAME_WORDS 16u -#define CMRX_RISCV_CONTEXT_FRAME_BYTES (CMRX_RISCV_CONTEXT_FRAME_WORDS * sizeof(uint32_t)) +#include void os_riscv_context_switch_request(bool activate); bool os_riscv_context_switch_is_pending(void); diff --git a/include/cmrx/arch/riscv/exception_frame.h b/include/cmrx/arch/riscv/exception_frame.h new file mode 100644 index 00000000..eb20fb4e --- /dev/null +++ b/include/cmrx/arch/riscv/exception_frame.h @@ -0,0 +1,168 @@ +#pragma once + +/** @defgroup arch_riscv_exception_frame RISC-V Exception Frame + * @ingroup arch_riscv + * + * Unified exception frame for RISC-V trap handlers and context switching. + * + * This header defines the standard full-context frame saved on trap entry + * and restored on trap exit. It is the RISC-V equivalent of the ARM + * ExceptionFrame in cortex.h. + * + * The layout covers all general-purpose registers that must be preserved + * across a trap (x1/ra, x5-x31), plus the mepc and mstatus CSRs. + * Registers x0 (zero), x2 (sp), x3 (gp), and x4 (tp) are excluded: + * x0 is hardwired to zero; sp is saved separately in the thread control + * block; gp and tp are program-global constants in M-mode. + * + * The frame is 32 words (128 bytes), keeping the stack 16-byte aligned + * per the RISC-V psABI (riscv-abi documentation, section 2.1). + * + * References: + * - RISC-V Privileged Specification (mepc, mstatus, mcause) + * - RISC-V psABI (register conventions, stack alignment) + * @{ + */ + +/* + * Byte offsets — usable from both C and assembly. + * + * Caller-saved registers (offsets 0-60): + */ +#define EF_RA 0 +#define EF_T0 4 +#define EF_T1 8 +#define EF_T2 12 +#define EF_A0 16 +#define EF_A1 20 +#define EF_A2 24 +#define EF_A3 28 +#define EF_A4 32 +#define EF_A5 36 +#define EF_A6 40 +#define EF_A7 44 +#define EF_T3 48 +#define EF_T4 52 +#define EF_T5 56 +#define EF_T6 60 + +/* Callee-saved registers (offsets 64-108): */ +#define EF_S0 64 +#define EF_S1 68 +#define EF_S2 72 +#define EF_S3 76 +#define EF_S4 80 +#define EF_S5 84 +#define EF_S6 88 +#define EF_S7 92 +#define EF_S8 96 +#define EF_S9 100 +#define EF_S10 104 +#define EF_S11 108 + +/* CSRs (offsets 112-116): */ +#define EF_MEPC 112 +#define EF_MSTATUS 116 + +/** Total frame size in bytes. */ +#define EXCEPTION_FRAME_SIZE 128 + +#ifndef __ASSEMBLER__ + +#include + +/** Full RISC-V trap context frame. + * + * Saved on the thread stack by every trap entry (timer ISR, exception + * handler) and restored on trap exit. When a context switch swaps SP + * between save and restore, the restore operates on the new thread's + * frame, launching it transparently. + * + * Initial thread stacks are populated with this same layout by + * os_thread_populate_stack(). + */ +typedef struct { + /* Caller-saved registers */ + uint32_t ra; + uint32_t t0; + uint32_t t1; + uint32_t t2; + uint32_t a0; + uint32_t a1; + uint32_t a2; + uint32_t a3; + uint32_t a4; + uint32_t a5; + uint32_t a6; + uint32_t a7; + uint32_t t3; + uint32_t t4; + uint32_t t5; + uint32_t t6; + /* Callee-saved registers */ + uint32_t s0; + uint32_t s1; + uint32_t s2; + uint32_t s3; + uint32_t s4; + uint32_t s5; + uint32_t s6; + uint32_t s7; + uint32_t s8; + uint32_t s9; + uint32_t s10; + uint32_t s11; + /* CSRs */ + uint32_t mepc; + uint32_t mstatus; + /* Padding for 16-byte alignment */ + uint32_t _pad[2]; +} ExceptionFrame; + +_Static_assert(sizeof(ExceptionFrame) == EXCEPTION_FRAME_SIZE, + "ExceptionFrame size must match EXCEPTION_FRAME_SIZE"); + +/** How many uint32_t slots the exception frame occupies. */ +#define EXCEPTION_FRAME_WORDS (sizeof(ExceptionFrame) / sizeof(uint32_t)) + +/** Initial mstatus value for newly created threads. + * + * MPIE = 1 (bit 7): after mret, MIE is set so interrupts are enabled. + * MPP = 3 (bits 12:11): stay in M-mode after mret. + * + * These are standard RISC-V privileged specification bit positions. + */ +#define CMRX_RISCV_INITIAL_MSTATUS ((1u << 7) | (3u << 11)) + +/** Retrieve a syscall argument from the exception frame. + * @param frame exception frame pointer + * @param argno argument index (0-3 maps to a0-a3) + */ +static inline uint32_t riscv_exception_get_arg(const ExceptionFrame *frame, + unsigned argno) +{ + switch (argno) { + case 0: return frame->a0; + case 1: return frame->a1; + case 2: return frame->a2; + case 3: return frame->a3; + default: return 0; + } +} + +/** Retrieve the syscall ID from the exception frame (a7 per RISC-V ecall ABI). */ +static inline uint8_t riscv_exception_get_syscall_id(const ExceptionFrame *frame) +{ + return (uint8_t)frame->a7; +} + +/** Write the syscall return value into the exception frame (a0). */ +static inline void riscv_exception_set_retval(ExceptionFrame *frame, + int32_t retval) +{ + frame->a0 = (uint32_t)retval; +} + +#endif /* __ASSEMBLER__ */ + +/** @} */ diff --git a/src/os/arch/riscv/context_switch_core.c b/src/os/arch/riscv/context_switch_core.c index d7c8901d..d8e62dc5 100644 --- a/src/os/arch/riscv/context_switch_core.c +++ b/src/os/arch/riscv/context_switch_core.c @@ -47,50 +47,22 @@ __attribute__((noinline, used)) static uint32_t os_riscv_context_switch_prepare( return 1u; } -/* Save/restore s0-s11 (callee-saved) per psABI (riscv-abi documentation, - * 1.1 Integer Register Convention section). Frame size keeps SP 16-byte aligned - * (riscv-abi documentation, 2.2 Hardware Floating-point Calling Convention section, - * "stack pointer shall be aligned to a 128-bit boundary upon procedure entry" and - * "must remain aligned throughout procedure execution" sections). +/* Pure SP swap between old and new task. + * + * The full register context (ExceptionFrame) is saved/restored by the + * trap handler assembly that calls the safe-point, not by this function. */ __attribute__((naked)) void os_riscv_context_switch_perform(void) { __asm__ volatile( - "addi sp, sp, -%[frame]\n\t" - "sw s0, 0(sp)\n\t" - "sw s1, 4(sp)\n\t" - "sw s2, 8(sp)\n\t" - "sw s3, 12(sp)\n\t" - "sw s4, 16(sp)\n\t" - "sw s5, 20(sp)\n\t" - "sw s6, 24(sp)\n\t" - "sw s7, 28(sp)\n\t" - "sw s8, 32(sp)\n\t" - "sw s9, 36(sp)\n\t" - "sw s10, 40(sp)\n\t" - "sw s11, 44(sp)\n\t" "la t0, cpu_context\n\t" "lw t1, %[old_task_off](t0)\n\t" "sw sp, %[thread_sp_off](t1)\n\t" "lw t2, %[new_task_off](t0)\n\t" "lw sp, %[thread_sp_off](t2)\n\t" - "lw s0, 0(sp)\n\t" - "lw s1, 4(sp)\n\t" - "lw s2, 8(sp)\n\t" - "lw s3, 12(sp)\n\t" - "lw s4, 16(sp)\n\t" - "lw s5, 20(sp)\n\t" - "lw s6, 24(sp)\n\t" - "lw s7, 28(sp)\n\t" - "lw s8, 32(sp)\n\t" - "lw s9, 36(sp)\n\t" - "lw s10, 40(sp)\n\t" - "lw s11, 44(sp)\n\t" - "addi sp, sp, %[frame]\n\t" "ret\n\t" : - : [frame] "i"(CMRX_RISCV_CONTEXT_FRAME_BYTES), - [old_task_off] "i"(offsetof(struct OS_scheduling_context_t, old_task)), + : [old_task_off] "i"(offsetof(struct OS_scheduling_context_t, old_task)), [new_task_off] "i"(offsetof(struct OS_scheduling_context_t, new_task)), [thread_sp_off] "i"(offsetof(struct OS_thread_t, sp)) : "t0", "t1", "t2", "memory"); @@ -99,9 +71,9 @@ __attribute__((naked)) void os_riscv_context_switch_perform(void) __attribute__((naked)) void os_riscv_context_switch_safe_point(void) { __asm__ volatile( - "mv t0, ra\n\t" + "mv s0, ra\n\t" "call os_riscv_context_switch_prepare\n\t" - "mv ra, t0\n\t" + "mv ra, s0\n\t" "beqz a0, 1f\n\t" "j os_riscv_context_switch_perform\n\t" "1:\n\t" diff --git a/src/os/arch/riscv/hal/testing/riscv_context_switch.c b/src/os/arch/riscv/hal/testing/riscv_context_switch.c index cd764ded..61baddea 100644 --- a/src/os/arch/riscv/hal/testing/riscv_context_switch.c +++ b/src/os/arch/riscv/hal/testing/riscv_context_switch.c @@ -32,7 +32,7 @@ static void riscv_reset_state(void) riscv_context_switch_fake_sp = NULL; } -static void riscv_setup_switch(uint32_t **old_sp, uint32_t **new_saved_sp, uint32_t **new_sp_after) +static void riscv_setup_switch(uint32_t **old_sp, uint32_t **new_saved_sp) { riscv_reset_state(); @@ -46,8 +46,7 @@ static void riscv_setup_switch(uint32_t **old_sp, uint32_t **new_saved_sp, uint3 cpu_context.new_task = &os_threads[1]; *old_sp = &riscv_stack_a[64]; - *new_sp_after = &riscv_stack_b[64]; - *new_saved_sp = *new_sp_after - CMRX_RISCV_CONTEXT_FRAME_WORDS; + *new_saved_sp = &riscv_stack_b[32]; cpu_context.new_task->sp = *new_saved_sp; riscv_context_switch_fake_sp = *old_sp; @@ -75,19 +74,16 @@ CTEST2(riscv_context_switch_reset, request_set_clear) CTEST_DATA(riscv_context_switch_safe_point) { uint32_t *old_sp; uint32_t *new_saved_sp; - uint32_t *new_sp_after; }; CTEST_SETUP(riscv_context_switch_safe_point) { uint32_t *old_sp = NULL; uint32_t *new_saved_sp = NULL; - uint32_t *new_sp_after = NULL; memset(data, 0, sizeof(*data)); - riscv_setup_switch(&old_sp, &new_saved_sp, &new_sp_after); + riscv_setup_switch(&old_sp, &new_saved_sp); data->old_sp = old_sp; data->new_saved_sp = new_saved_sp; - data->new_sp_after = new_sp_after; } CTEST2(riscv_context_switch_safe_point, safe_point_consumes_once) @@ -108,9 +104,9 @@ CTEST2(riscv_context_switch_safe_point, sp_bookkeeping_and_state_transition) os_riscv_context_switch_safe_point(); ASSERT_EQUAL(riscv_ptr_value(cpu_context.old_task->sp), - riscv_ptr_value(data->old_sp - CMRX_RISCV_CONTEXT_FRAME_WORDS)); + riscv_ptr_value(data->old_sp)); ASSERT_EQUAL(riscv_ptr_value(riscv_context_switch_fake_sp), - riscv_ptr_value(data->new_sp_after)); + riscv_ptr_value(data->new_saved_sp)); ASSERT_EQUAL(core[0].thread_current, 1); ASSERT_EQUAL(os_threads[0].state, THREAD_STATE_READY); diff --git a/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c b/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c index 7a615e2d..02ed19dd 100644 --- a/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c +++ b/src/os/arch/riscv/hal/testing/riscv_context_switch_fake.c @@ -39,19 +39,8 @@ static void os_riscv_context_switch_update_state(void) static void os_riscv_context_switch_perform(void) { - uint32_t *sp = riscv_context_switch_fake_sp; - if (sp != NULL) - { - sp -= CMRX_RISCV_CONTEXT_FRAME_WORDS; - } - cpu_context.old_task->sp = sp; - - sp = cpu_context.new_task->sp; - if (sp != NULL) - { - sp += CMRX_RISCV_CONTEXT_FRAME_WORDS; - } - riscv_context_switch_fake_sp = sp; + cpu_context.old_task->sp = riscv_context_switch_fake_sp; + riscv_context_switch_fake_sp = cpu_context.new_task->sp; riscv_context_switch_called = true; } diff --git a/src/os/arch/riscv/sched.c b/src/os/arch/riscv/sched.c index 1f5baebf..aa32e860 100644 --- a/src/os/arch/riscv/sched.c +++ b/src/os/arch/riscv/sched.c @@ -13,72 +13,35 @@ #include #include #include +#include #include #include +#include /* * Populate stack of new thread so it can be executed. * - * Sets up the stack frame so that when the thread is first scheduled, - * it will start executing at the entrypoint with 'data' as argument. - * - * RISC-V calling convention (psABI): - * - a0: first argument (data pointer) - * - ra: return address (thread dispose handler) - * - Stack must be 16-byte aligned - * - * Stack layout for first context switch (os_riscv_context_switch_perform): - * The context switch saves/restores s0-s11 (12 words = 48 bytes). - * For initial boot, we also need the exception-like frame that - * os_boot_thread will use. + * Places an ExceptionFrame at the top of the thread stack. When the + * trap handler restores from this frame after a context switch, the + * thread starts executing at 'entrypoint' with 'data' in a0 and + * os_thread_dispose in ra. * + * This follows the same pattern as ARM's os_thread_populate_stack + * which builds an ExceptionFrame for PendSV to restore. */ uint32_t *os_thread_populate_stack(int stack_id, unsigned stack_size, entrypoint_t *entrypoint, void *data) { uint32_t *stack = os_stack_get(stack_id); + ExceptionFrame *frame = (ExceptionFrame *)&stack[stack_size - EXCEPTION_FRAME_WORDS]; - /* - * Initial stack layout (from top, stack_size words): - * - * stack[stack_size - 1] : (padding for 16-byte alignment if needed) - * stack[stack_size - 2] : Initial PC (entrypoint) - * stack[stack_size - 3] : Initial ra (os_thread_dispose) - * stack[stack_size - 4] : Initial a0 (data) - * stack[stack_size - 5] : Reserved - * stack[stack_size - 6] : Reserved - * stack[stack_size - 7] : Reserved - * stack[stack_size - 8] : Reserved - * stack[stack_size - 9] : s11 - * stack[stack_size - 10] : s10 - * stack[stack_size - 11] : s9 - * stack[stack_size - 12] : s8 - * stack[stack_size - 13] : s7 - * stack[stack_size - 14] : s6 - * stack[stack_size - 15] : s5 - * stack[stack_size - 16] : s4 - * stack[stack_size - 17] : s3 - * stack[stack_size - 18] : s2 - * stack[stack_size - 19] : s1 - * stack[stack_size - 20] : s0 - * - * SP will point to stack[stack_size - 20] - */ - - /* Clear the stack area we'll use */ - for (unsigned i = 0; i < 20; i++) { - stack[stack_size - 1 - i] = 0; - } - - /* Set up initial register values */ - stack[stack_size - 2] = (uint32_t)entrypoint; /* Initial PC */ - stack[stack_size - 3] = (uint32_t)os_thread_dispose; /* ra - return to dispose */ - stack[stack_size - 4] = (uint32_t)data; /* a0 - argument */ + memset(frame, 0, sizeof(*frame)); + frame->ra = (uint32_t)os_thread_dispose; + frame->a0 = (uint32_t)data; + frame->mepc = (uint32_t)entrypoint; + frame->mstatus = CMRX_RISCV_INITIAL_MSTATUS; - /* s0-s11 are all zero (callee-saved, will be restored by context switch) */ - - /* Return SP pointing to where context switch expects it */ - return &stack[stack_size - 20]; + return (uint32_t *)frame; } /** Platform-specific way of initializing threads. @@ -120,70 +83,33 @@ int os_process_create(Process_t process_id, /* * Boot the first thread. * - * This function never returns. It sets up the CPU state and jumps - * to the thread's entry point. - * - * For RISC-V, we: - * 1. Load the thread's stack pointer - * 2. Pop the initial register values - * 3. Jump to the entry point + * Reads the ExceptionFrame from the thread stack and uses mret to + * enter user code. This mirrors the normal trap-return path: mepc + * and mstatus are restored via CSR writes, then mret jumps to mepc + * while promoting MPIE → MIE (enabling interrupts). */ __attribute__((noreturn)) void os_boot_thread(Thread_t boot_thread) { struct OS_thread_t *thread = os_thread_get(boot_thread); - uint32_t *thread_sp = thread->sp; - - /* - * The stack was set up by os_thread_populate_stack. - * Layout at thread_sp: - * +0: s0 - * +4: s1 - * ... - * +44: s11 - * +48: reserved - * +52: reserved - * +56: reserved - * +60: a0 (argument) - * +64: ra (return address = os_thread_dispose) - * +68: PC (entry point) - * - * We need to: - * 1. Skip past s0-s11 (they're just placeholders for first boot) - * 2. Load a0, ra - * 3. Jump to PC - */ + ExceptionFrame *frame = (ExceptionFrame *)thread->sp; __asm__ volatile( - /* Set stack pointer to thread stack */ - "mv sp, %[sp]\n\t" - - /* Skip past callee-saved registers (s0-s11 = 12*4 = 48 bytes) */ - "addi sp, sp, 48\n\t" - - /* Skip reserved area (4 words = 16 bytes) */ - "addi sp, sp, 16\n\t" - - /* Load a0 (first argument) */ - "lw a0, 0(sp)\n\t" - - /* Load ra (return address) */ - "lw ra, 4(sp)\n\t" - - /* Load entry point into t0 */ - "lw t0, 8(sp)\n\t" - - /* Adjust sp past our boot frame */ - "addi sp, sp, 16\n\t" - - /* Jump to entry point */ - "jr t0\n\t" + "csrw mepc, %[mepc]\n\t" + "csrw mstatus, %[mstatus]\n\t" + "mv sp, %[sp_after]\n\t" + "mv ra, %[ra]\n\t" + "mv a0, %[a0]\n\t" + "mret\n\t" : - : [sp] "r"(thread_sp) + : [mepc] "r"(frame->mepc), + [mstatus] "r"(frame->mstatus), + [sp_after] "r"((uint32_t *)frame + EXCEPTION_FRAME_WORDS), + [ra] "r"(frame->ra), + [a0] "r"(frame->a0) : "memory" ); - /* Never reached */ __builtin_unreachable(); } @@ -229,21 +155,8 @@ int os_set_syscall_return_value(Thread_t thread_id, int32_t retval) return E_INVALID; } - /* - * The thread's SP points to its saved context. - * For RISC-V, the ecall handler saves the exception frame on stack. - * The return value goes in a0. - * - * However, the exact stack layout depends on whether the thread - * was blocked during ecall (exception frame) or IRQ (different frame). - * - * For now, this is a stub - proper implementation needs to understand - * the exact stack layout for each case. - */ - - /* TODO: Proper implementation based on stack frame layout */ - (void)thread; - (void)retval; + ExceptionFrame *frame = (ExceptionFrame *)thread->sp; + riscv_exception_set_retval(frame, retval); return E_OK; } From 9e88ab26a604ba752f285f387cb04eef0bf630c0 Mon Sep 17 00:00:00 2001 From: Tobias Aguiar Date: Sun, 1 Mar 2026 13:58:38 +0100 Subject: [PATCH 7/7] riscv: add SAVE/LOAD_CONTEXT macros, rewrite quirks as thin wrappers Add SAVE_CONTEXT() and LOAD_CONTEXT() inline asm macros to exception_frame.h, mirroring ARM's cortex.h pattern. Rewrite the Pico SDK quirk handlers to use these macros, reducing each to a thin naked wrapper with only SDK-specific wiring (symbol override, mscratch protocol, exception table dispatch). Simplify the ecall handler to plain C receiving ExceptionFrame*. Delete the RP2350-specific exception frame type, superseded by the arch-level ExceptionFrame. --- include/cmrx/arch/riscv/exception_frame.h | 92 +++++++++++++++ quirks/pico-sdk-riscv/CMakeLists.txt | 11 +- quirks/pico-sdk-riscv/cmrx_ecall_handler.c | 100 +++------------- .../cmrx_exception_dispatcher.c | 63 ++++++++++ quirks/pico-sdk-riscv/cmrx_timer_isr.c | 24 ++++ quirks/pico-sdk-riscv/crt0_riscv_cmrx.S | 111 ------------------ .../riscv_rp2350_exception_frame.c | 43 ------- .../riscv_rp2350_exception_frame.h | 52 -------- 8 files changed, 200 insertions(+), 296 deletions(-) create mode 100644 quirks/pico-sdk-riscv/cmrx_exception_dispatcher.c create mode 100644 quirks/pico-sdk-riscv/cmrx_timer_isr.c delete mode 100644 quirks/pico-sdk-riscv/crt0_riscv_cmrx.S delete mode 100644 quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c delete mode 100644 quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h diff --git a/include/cmrx/arch/riscv/exception_frame.h b/include/cmrx/arch/riscv/exception_frame.h index eb20fb4e..8031f09d 100644 --- a/include/cmrx/arch/riscv/exception_frame.h +++ b/include/cmrx/arch/riscv/exception_frame.h @@ -67,6 +67,98 @@ /** Total frame size in bytes. */ #define EXCEPTION_FRAME_SIZE 128 +/* Stringification helpers for use inside inline asm macros. */ +#define _RISCV_EFS(x) #x +#define RISCV_EFS(x) _RISCV_EFS(x) + +/** Save application context. + * Pushes a full ExceptionFrame onto the current stack: all GP registers + * (ra, t0-t6, a0-a7, s0-s11) plus mepc and mstatus. + * Uses t0/t1 as scratch for CSR reads (they are saved beforehand). + * @note This is defined as a macro so it can live inside naked functions. + */ +#define SAVE_CONTEXT() \ +asm volatile( \ + "addi sp, sp, -" RISCV_EFS(EXCEPTION_FRAME_SIZE) "\n\t" \ + "sw ra, " RISCV_EFS(EF_RA) "(sp)\n\t" \ + "sw t0, " RISCV_EFS(EF_T0) "(sp)\n\t" \ + "sw t1, " RISCV_EFS(EF_T1) "(sp)\n\t" \ + "sw t2, " RISCV_EFS(EF_T2) "(sp)\n\t" \ + "sw a0, " RISCV_EFS(EF_A0) "(sp)\n\t" \ + "sw a1, " RISCV_EFS(EF_A1) "(sp)\n\t" \ + "sw a2, " RISCV_EFS(EF_A2) "(sp)\n\t" \ + "sw a3, " RISCV_EFS(EF_A3) "(sp)\n\t" \ + "sw a4, " RISCV_EFS(EF_A4) "(sp)\n\t" \ + "sw a5, " RISCV_EFS(EF_A5) "(sp)\n\t" \ + "sw a6, " RISCV_EFS(EF_A6) "(sp)\n\t" \ + "sw a7, " RISCV_EFS(EF_A7) "(sp)\n\t" \ + "sw t3, " RISCV_EFS(EF_T3) "(sp)\n\t" \ + "sw t4, " RISCV_EFS(EF_T4) "(sp)\n\t" \ + "sw t5, " RISCV_EFS(EF_T5) "(sp)\n\t" \ + "sw t6, " RISCV_EFS(EF_T6) "(sp)\n\t" \ + "sw s0, " RISCV_EFS(EF_S0) "(sp)\n\t" \ + "sw s1, " RISCV_EFS(EF_S1) "(sp)\n\t" \ + "sw s2, " RISCV_EFS(EF_S2) "(sp)\n\t" \ + "sw s3, " RISCV_EFS(EF_S3) "(sp)\n\t" \ + "sw s4, " RISCV_EFS(EF_S4) "(sp)\n\t" \ + "sw s5, " RISCV_EFS(EF_S5) "(sp)\n\t" \ + "sw s6, " RISCV_EFS(EF_S6) "(sp)\n\t" \ + "sw s7, " RISCV_EFS(EF_S7) "(sp)\n\t" \ + "sw s8, " RISCV_EFS(EF_S8) "(sp)\n\t" \ + "sw s9, " RISCV_EFS(EF_S9) "(sp)\n\t" \ + "sw s10, " RISCV_EFS(EF_S10) "(sp)\n\t" \ + "sw s11, " RISCV_EFS(EF_S11) "(sp)\n\t" \ + "csrr t0, mepc\n\t" \ + "csrr t1, mstatus\n\t" \ + "sw t0, " RISCV_EFS(EF_MEPC) "(sp)\n\t" \ + "sw t1, " RISCV_EFS(EF_MSTATUS) "(sp)\n\t" \ + : : : "memory" \ +) + +/** Load application context. + * Restores all GP registers and CSRs (mepc, mstatus) from the + * ExceptionFrame on the current stack and pops the frame. + * Does NOT execute mret — the caller handles that. + * @note This is defined as a macro so it can live inside naked functions. + */ +#define LOAD_CONTEXT() \ +asm volatile( \ + "lw t0, " RISCV_EFS(EF_MEPC) "(sp)\n\t" \ + "lw t1, " RISCV_EFS(EF_MSTATUS) "(sp)\n\t" \ + "csrw mepc, t0\n\t" \ + "csrw mstatus, t1\n\t" \ + "lw s0, " RISCV_EFS(EF_S0) "(sp)\n\t" \ + "lw s1, " RISCV_EFS(EF_S1) "(sp)\n\t" \ + "lw s2, " RISCV_EFS(EF_S2) "(sp)\n\t" \ + "lw s3, " RISCV_EFS(EF_S3) "(sp)\n\t" \ + "lw s4, " RISCV_EFS(EF_S4) "(sp)\n\t" \ + "lw s5, " RISCV_EFS(EF_S5) "(sp)\n\t" \ + "lw s6, " RISCV_EFS(EF_S6) "(sp)\n\t" \ + "lw s7, " RISCV_EFS(EF_S7) "(sp)\n\t" \ + "lw s8, " RISCV_EFS(EF_S8) "(sp)\n\t" \ + "lw s9, " RISCV_EFS(EF_S9) "(sp)\n\t" \ + "lw s10, " RISCV_EFS(EF_S10) "(sp)\n\t" \ + "lw s11, " RISCV_EFS(EF_S11) "(sp)\n\t" \ + "lw ra, " RISCV_EFS(EF_RA) "(sp)\n\t" \ + "lw t0, " RISCV_EFS(EF_T0) "(sp)\n\t" \ + "lw t1, " RISCV_EFS(EF_T1) "(sp)\n\t" \ + "lw t2, " RISCV_EFS(EF_T2) "(sp)\n\t" \ + "lw a0, " RISCV_EFS(EF_A0) "(sp)\n\t" \ + "lw a1, " RISCV_EFS(EF_A1) "(sp)\n\t" \ + "lw a2, " RISCV_EFS(EF_A2) "(sp)\n\t" \ + "lw a3, " RISCV_EFS(EF_A3) "(sp)\n\t" \ + "lw a4, " RISCV_EFS(EF_A4) "(sp)\n\t" \ + "lw a5, " RISCV_EFS(EF_A5) "(sp)\n\t" \ + "lw a6, " RISCV_EFS(EF_A6) "(sp)\n\t" \ + "lw a7, " RISCV_EFS(EF_A7) "(sp)\n\t" \ + "lw t3, " RISCV_EFS(EF_T3) "(sp)\n\t" \ + "lw t4, " RISCV_EFS(EF_T4) "(sp)\n\t" \ + "lw t5, " RISCV_EFS(EF_T5) "(sp)\n\t" \ + "lw t6, " RISCV_EFS(EF_T6) "(sp)\n\t" \ + "addi sp, sp, " RISCV_EFS(EXCEPTION_FRAME_SIZE) "\n\t" \ + : : : "memory" \ +) + #ifndef __ASSEMBLER__ #include diff --git a/quirks/pico-sdk-riscv/CMakeLists.txt b/quirks/pico-sdk-riscv/CMakeLists.txt index 2c7eefdc..d67a38c0 100644 --- a/quirks/pico-sdk-riscv/CMakeLists.txt +++ b/quirks/pico-sdk-riscv/CMakeLists.txt @@ -1,7 +1,8 @@ # CMRX quirk for Pico SDK RISC-V (RP2350) # # Provides: -# - Replacement external IRQ handler that calls CMRX context switch safe-point +# - Machine timer ISR with full ExceptionFrame save/restore +# - Exception dispatcher with full ExceptionFrame save/restore # - ecall exception handler for CMRX syscalls if(NOT DEFINED PICO_PLATFORM) @@ -14,20 +15,16 @@ endif() message(STATUS "CMRX: Pico SDK RISC-V quirk enabled") -# Add the CMRX RISC-V handlers to the OS library target_sources(os PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/cmrx_timer_isr.c + ${CMAKE_CURRENT_LIST_DIR}/cmrx_exception_dispatcher.c ${CMAKE_CURRENT_LIST_DIR}/cmrx_ecall_handler.c - ${CMAKE_CURRENT_LIST_DIR}/riscv_rp2350_exception_frame.c ) -# Ensure headers are accessible: -# - CMRX context switch header -# - Pico SDK headers for the assembly file (pico.h, hardware/regs/rvcsr.h) target_include_directories(os PRIVATE ${CMRX_ROOT_DIR}/include ) -# Link against pico_base_headers to get Pico SDK includes target_link_libraries(os PRIVATE pico_base_headers hardware_regs diff --git a/quirks/pico-sdk-riscv/cmrx_ecall_handler.c b/quirks/pico-sdk-riscv/cmrx_ecall_handler.c index 8456030e..723bdf5b 100644 --- a/quirks/pico-sdk-riscv/cmrx_ecall_handler.c +++ b/quirks/pico-sdk-riscv/cmrx_ecall_handler.c @@ -1,99 +1,33 @@ /* * CMRX RISC-V ecall (syscall) handler for Pico SDK (RP2350). * - * Implements isr_riscv_machine_ecall_mmode_exception to handle syscalls - * triggered by the ecall instruction from M-mode code. + * Called by the CMRX exception dispatcher with a pointer to the full + * ExceptionFrame. Advances mepc past the ecall instruction + * (RISC-V Privileged Specification, Section 3.1.14), dispatches the + * syscall, and writes the return value into the frame's a0. + * + * The context switch safe-point is handled by the exception dispatcher, + * not here. * * SPDX-License-Identifier: BSD-3-Clause */ #include -#include -#include "riscv_rp2350_exception_frame.h" +#include -/* Forward declaration - defined in kernel */ extern int os_system_call(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint32_t arg3, uint8_t syscall_id); -/* - * RISC-V Privileged Specification: - * - Table 3.6: mcause=11 identifies environment call from M-mode - * - Section 3.1.14: mepc points at ecall; software advances by 4 to continue - */ - -/* - * C helper to dispatch the syscall. - * Called from the assembly handler with a pointer to the exception frame. - * - * @param frame Pointer to the exception frame on stack - * @return syscall return value (to be written to a0 in frame) - */ -int cmrx_ecall_dispatch(RiscvRp2350ExceptionFrame *frame) -{ - uint32_t arg0 = riscv_rp2350_exception_get_arg(frame, 0); - uint32_t arg1 = riscv_rp2350_exception_get_arg(frame, 1); - uint32_t arg2 = riscv_rp2350_exception_get_arg(frame, 2); - uint32_t arg3 = riscv_rp2350_exception_get_arg(frame, 3); - uint8_t syscall_id = riscv_rp2350_exception_get_syscall_id(frame); - - return os_system_call(arg0, arg1, arg2, arg3, syscall_id); -} - -void cmrx_ecall_dispatch_writeback(RiscvRp2350ExceptionFrame *frame) -{ - int retval = cmrx_ecall_dispatch(frame); - riscv_rp2350_exception_set_retval(frame, retval); -} - -/* - * RISC-V ecall exception handler. - * - * This overrides the weak isr_riscv_machine_ecall_mmode_exception - * from Pico SDK's exception_table_riscv.S. - * - * When called by the exception dispatch code: - * - sp points to the exception frame (caller-saved regs already saved) - * - mepc points to the ecall instruction - * - We must increment mepc by 4 before returning - * - * After dispatching the syscall, we call the context switch safe-point - * to allow a pending context switch to occur (e.g., after usleep()). - * - * This handler is marked naked to have full control over the stack. - * - * See RISC-V Privileged Specification, Section 3.1.14 for mepc behavior. - */ -__attribute__((naked)) void isr_riscv_machine_ecall_mmode_exception(void) +void isr_riscv_machine_ecall_mmode_exception(ExceptionFrame *frame) { - __asm__ volatile( - /* Save ra - we'll make calls */ - "addi sp, sp, -16\n\t" - "sw ra, 0(sp)\n\t" - "sw s0, 4(sp)\n\t" - - /* Save exception frame pointer in s0 */ - /* Exception frame is at sp + 16 (our frame size) */ - "addi s0, sp, 16\n\t" - - /* Increment mepc by 4 to skip past ecall instruction */ - "call riscv_rp2350_exception_advance_mepc_by_4\n\t" - - /* Call C helper to dispatch syscall and write a0 into frame */ - /* a0 = exception frame pointer */ - "mv a0, s0\n\t" - "call cmrx_ecall_dispatch_writeback\n\t" + frame->mepc += 4; - /* Call context switch safe-point */ - /* This allows pending context switches after syscalls like usleep() */ - "call os_riscv_context_switch_safe_point\n\t" + uint32_t arg0 = riscv_exception_get_arg(frame, 0); + uint32_t arg1 = riscv_exception_get_arg(frame, 1); + uint32_t arg2 = riscv_exception_get_arg(frame, 2); + uint32_t arg3 = riscv_exception_get_arg(frame, 3); + uint8_t syscall_id = riscv_exception_get_syscall_id(frame); - /* Restore ra and s0, return to exception dispatcher */ - "lw ra, 0(sp)\n\t" - "lw s0, 4(sp)\n\t" - "addi sp, sp, 16\n\t" - "ret\n\t" - : - : - : "memory" - ); + int retval = os_system_call(arg0, arg1, arg2, arg3, syscall_id); + riscv_exception_set_retval(frame, retval); } diff --git a/quirks/pico-sdk-riscv/cmrx_exception_dispatcher.c b/quirks/pico-sdk-riscv/cmrx_exception_dispatcher.c new file mode 100644 index 00000000..97711ed5 --- /dev/null +++ b/quirks/pico-sdk-riscv/cmrx_exception_dispatcher.c @@ -0,0 +1,63 @@ +/* + * CMRX RISC-V exception dispatcher for Pico SDK (RP2350). + * + * Overrides the weak isr_riscv_machine_exception from + * exception_table_riscv.S. Uses the arch-level SAVE_CONTEXT / + * LOAD_CONTEXT macros for the full ExceptionFrame, adding only the + * SDK-specific mscratch protocol and exception table dispatch. + * + * Individual exception handlers receive the ExceptionFrame pointer in a0. + * + * This file is derived from pico-sdk exception_table_riscv.S. + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +__attribute__((naked, section(".time_critical.cmrx_exception_handler"))) +void isr_riscv_machine_exception(void) +{ + /* Pico SDK mscratch protocol: swap ra with mscratch for nested detection */ + asm volatile( + "csrrw ra, mscratch, ra\n\t" + "bnez ra, __halt_on_unhandled_exception\n\t" + ::: "memory" + ); + + SAVE_CONTEXT(); + + /* Fix ra: SAVE_CONTEXT stored ra=0; overwrite with actual ra from mscratch */ + asm volatile( + "csrr t0, mscratch\n\t" + "sw t0, " RISCV_EFS(EF_RA) "(sp)\n\t" + + "csrr t0, mcause\n\t" + "li t1, 11\n\t" + "bgtu t0, t1, 1f\n\t" + + "la t1, __riscv_exception_table\n\t" + "slli t0, t0, 2\n\t" + "add t0, t0, t1\n\t" + "lw t0, (t0)\n\t" + "mv a0, sp\n\t" + "jalr ra, t0\n\t" + + "call os_riscv_context_switch_safe_point\n\t" + "j 2f\n\t" + + "1:\n\t" + "csrr ra, mscratch\n\t" + "j __halt_on_unhandled_exception\n\t" + + "2:\n\t" + ::: "memory" + ); + + LOAD_CONTEXT(); + asm volatile( + "csrw mscratch, zero\n\t" + "mret\n\t" + ::: "memory" + ); +} diff --git a/quirks/pico-sdk-riscv/cmrx_timer_isr.c b/quirks/pico-sdk-riscv/cmrx_timer_isr.c new file mode 100644 index 00000000..052331ce --- /dev/null +++ b/quirks/pico-sdk-riscv/cmrx_timer_isr.c @@ -0,0 +1,24 @@ +/* + * CMRX RISC-V machine timer ISR for Pico SDK (RP2350). + * + * Overrides the weak isr_riscv_machine_timer from crt0_riscv.S. + * Saves a full ExceptionFrame so that a context switch during the + * safe-point restores the new thread's complete register state. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +__attribute__((naked, section(".time_critical.cmrx_timer_handler"))) +void isr_riscv_machine_timer(void) +{ + SAVE_CONTEXT(); + asm volatile( + "call cmrx_machine_timer_handler\n\t" + "call os_riscv_context_switch_safe_point\n\t" + ::: "memory" + ); + LOAD_CONTEXT(); + asm volatile("mret\n\t" ::: "memory"); +} diff --git a/quirks/pico-sdk-riscv/crt0_riscv_cmrx.S b/quirks/pico-sdk-riscv/crt0_riscv_cmrx.S deleted file mode 100644 index 7e712765..00000000 --- a/quirks/pico-sdk-riscv/crt0_riscv_cmrx.S +++ /dev/null @@ -1,111 +0,0 @@ -/* - * CMRX RISC-V IRQ handler wrapper for Pico SDK (RP2350). - * - * Provides a replacement for isr_riscv_machine_external_irq that calls the - * CMRX context switch safe-point at the IRQ exit boundary. - * - * This file is derived from pico-sdk crt0_riscv.S (BSD-3-Clause). - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "pico.h" -#include "hardware/regs/rvcsr.h" - -.section .time_critical.cmrx_irq_handler, "ax" - -/* - * External IRQ handler with CMRX safe-point hook. - * - * This replaces the weak isr_riscv_machine_external_irq from crt0_riscv.S. - * The structure mirrors the original but adds a call to the context switch - * safe-point before returning from the IRQ. - */ -.global isr_riscv_machine_external_irq -isr_riscv_machine_external_irq: - addi sp, sp, -80 - sw ra, 0(sp) - sw t0, 4(sp) - sw t1, 8(sp) - sw t2, 12(sp) - sw a0, 16(sp) - sw a1, 20(sp) - sw a2, 24(sp) - sw a3, 28(sp) - sw a4, 32(sp) - sw a5, 36(sp) - sw a6, 40(sp) - sw a7, 44(sp) - sw t3, 48(sp) - sw t4, 52(sp) - sw t5, 56(sp) - sw t6, 60(sp) - csrr a0, mepc - csrr a1, mstatus - sw a0, 64(sp) - sw a1, 68(sp) -cmrx_save_meicontext: - csrrsi a2, RVCSR_MEICONTEXT_OFFSET, RVCSR_MEICONTEXT_CLEARTS_BITS - sw a2, 72(sp) - -cmrx_get_first_irq: - csrrsi a0, RVCSR_MEINEXT_OFFSET, RVCSR_MEINEXT_UPDATE_BITS - bltz a0, cmrx_no_more_irqs -cmrx_dispatch_irq: - csrsi mstatus, 0x8 - lui a1, %hi(__soft_vector_table) - add a1, a1, a0 - lw a1, %lo(__soft_vector_table)(a1) - jalr ra, a1 - csrci mstatus, 0x8 -cmrx_get_next_irq: - csrrsi a0, RVCSR_MEINEXT_OFFSET, RVCSR_MEINEXT_UPDATE_BITS - bgez a0, cmrx_dispatch_irq - -cmrx_no_more_irqs: - /* - * All IRQs dispatched. Before restoring context and returning, - * call the CMRX context switch safe-point. This allows a pending - * context switch (requested by the scheduler during IRQ handling) - * to be performed now, at a safe boundary. - * - * The safe-point function preserves s0-s11 per RISC-V calling convention. - * Caller-saved registers (a0, t0, etc.) may be clobbered, but we restore - * them from the stack frame below, so this is safe. - */ - call os_riscv_context_switch_safe_point - - /* Restore saved context and return from IRQ */ - lw a0, 64(sp) - lw a1, 68(sp) - lw a2, 72(sp) - csrw mepc, a0 - csrw mstatus, a1 - csrw RVCSR_MEICONTEXT_OFFSET, a2 - lw ra, 0(sp) - lw t0, 4(sp) - lw t1, 8(sp) - lw t2, 12(sp) - /* skip a0 for now */ - lw a1, 20(sp) - lw a2, 24(sp) - lw a3, 28(sp) - lw a4, 32(sp) - lw a5, 36(sp) - lw a6, 40(sp) - lw a7, 44(sp) - lw t3, 48(sp) - lw t4, 52(sp) - lw t5, 56(sp) - lw t6, 60(sp) - -cmrx_check_irq_before_exit: - /* - * Optimization: check for new IRQ before completing the mret sequence. - * If an IRQ arrived while we were in the safe-point, handle it now - * rather than taking the mret->enter->save latency. - */ - csrr a0, RVCSR_MEINEXT_OFFSET - bgez a0, cmrx_save_meicontext - lw a0, 16(sp) - addi sp, sp, 80 - mret diff --git a/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c b/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c deleted file mode 100644 index cb2aa8d0..00000000 --- a/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.c +++ /dev/null @@ -1,43 +0,0 @@ -#include "riscv_rp2350_exception_frame.h" - -uint32_t riscv_rp2350_exception_get_arg(const RiscvRp2350ExceptionFrame *frame, unsigned argno) -{ - switch (argno) { - case 0: - return frame->a0; - case 1: - return frame->a1; - case 2: - return frame->a2; - case 3: - return frame->a3; - default: - return 0; - } -} - -uint8_t riscv_rp2350_exception_get_syscall_id(const RiscvRp2350ExceptionFrame *frame) -{ - return (uint8_t)frame->a7; -} - -void riscv_rp2350_exception_set_retval(RiscvRp2350ExceptionFrame *frame, int32_t retval) -{ - frame->a0 = (uint32_t)retval; -} - -void riscv_rp2350_exception_advance_mepc_by_4(void) -{ - uint32_t mepc; - - __asm__ volatile( - "csrr %0, mepc\n\t" - : "=r"(mepc) - ); - mepc += 4u; - __asm__ volatile( - "csrw mepc, %0\n\t" - : - : "r"(mepc) - ); -} diff --git a/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h b/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h deleted file mode 100644 index bea86ac5..00000000 --- a/quirks/pico-sdk-riscv/riscv_rp2350_exception_frame.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include - -/* - * RP2350 (Hazard3) machine exception frame as saved by Pico SDK - * exception entry in external/pico-sdk/src/rp2_common/hardware_exception/exception_table_riscv.S. - * - * The layout below matches stores at offsets 0..60 bytes: - * 0 : unused (ra is held in mscratch during handler execution) - * 4 : t0 - * 8 : t1 - * 12 : t2 - * 16 : a0 - * 20 : a1 - * 24 : a2 - * 28 : a3 - * 32 : a4 - * 36 : a5 - * 40 : a6 - * 44 : a7 - * 48 : t3 - * 52 : t4 - * 56 : t5 - * 60 : t6 - */ -typedef struct { - uint32_t ra_slot_unused; - uint32_t t0; - uint32_t t1; - uint32_t t2; - uint32_t a0; - uint32_t a1; - uint32_t a2; - uint32_t a3; - uint32_t a4; - uint32_t a5; - uint32_t a6; - uint32_t a7; - uint32_t t3; - uint32_t t4; - uint32_t t5; - uint32_t t6; -} RiscvRp2350ExceptionFrame; - -_Static_assert(sizeof(RiscvRp2350ExceptionFrame) == (16u * sizeof(uint32_t)), - "RP2350 exception frame must be 16 words"); - -uint32_t riscv_rp2350_exception_get_arg(const RiscvRp2350ExceptionFrame *frame, unsigned argno); -uint8_t riscv_rp2350_exception_get_syscall_id(const RiscvRp2350ExceptionFrame *frame); -void riscv_rp2350_exception_set_retval(RiscvRp2350ExceptionFrame *frame, int32_t retval); -void riscv_rp2350_exception_advance_mepc_by_4(void);