diff --git a/crates/synth-backend-riscv/src/lib.rs b/crates/synth-backend-riscv/src/lib.rs index 0273a9f..d0de09e 100644 --- a/crates/synth-backend-riscv/src/lib.rs +++ b/crates/synth-backend-riscv/src/lib.rs @@ -17,15 +17,19 @@ pub mod backend; pub mod elf_builder; pub mod encoder; +pub mod linker_script; pub mod pmp; pub mod register; pub mod riscv_op; pub mod selector; +pub mod startup; pub use backend::RiscVBackend; pub use elf_builder::{ElfMode, RiscVElfBuilder, RiscVElfFunction}; pub use encoder::{RiscVEncoder, RiscVEncodingError}; +pub use linker_script::{LinkerScriptConfig, RiscVLinkerScriptGenerator}; pub use pmp::{PMPAllocator, PMPEntry, PMPError, PMPMode, PMPPermissions}; pub use register::{Reg, RegClass}; pub use riscv_op::{Branch, Csr, RiscVOp}; pub use selector::{RiscVSelection, SelectorError, select_simple}; +pub use startup::{RiscVStartupGenerator, StartupConfig}; diff --git a/crates/synth-backend-riscv/src/linker_script.rs b/crates/synth-backend-riscv/src/linker_script.rs new file mode 100644 index 0000000..3cb98d3 --- /dev/null +++ b/crates/synth-backend-riscv/src/linker_script.rs @@ -0,0 +1,289 @@ +//! GNU `ld` linker script generator for RISC-V targets. +//! +//! Mirrors `synth-backend::linker_script` (ARM) but produces RISC-V-flavored +//! sections: `.text._reset` is placed at the reset entry, `mtvec`'s alignment +//! is honored (4-byte for direct mode, 64-byte minimum for vectored), and +//! `__global_pointer$` / `__linear_memory_base` are exposed as weak symbols +//! so the startup code can `la` them with no relocation hassle. +//! +//! Memory model assumed: +//! +//! ```text +//! ┌──────────────────┐ flash_size +//! │ .data load │ ──▶ copied to _data_start at reset +//! │ .rodata │ +//! │ .text │ +//! │ .text._trap_entry│ +//! │ .text._reset │ flash_origin (entry point) +//! └──────────────────┘ +//! +//! ┌──────────────────┐ ram_origin + ram_size (= _stack_top) +//! │ stack ↓ │ +//! │ … │ +//! │ __linear_memory_base +//! │ .bss │ +//! │ .data │ ram_origin +//! └──────────────────┘ +//! ``` +//! +//! The wasm linear-memory window lives between the `.bss` end and the stack — +//! the firmware author sets its size at link time via `--defsym` or by +//! editing the generated script. + +use synth_core::HardwareCapabilities; + +#[derive(Debug, Clone)] +pub struct LinkerScriptConfig { + /// Base address of the executable section (typically 0x0 for embedded + /// boot, 0x80000000 for SiFive HiFive-style boards). + pub flash_origin: u64, + /// Base address of writable RAM. + pub ram_origin: u64, + /// Number of bytes reserved for the wasm linear memory (multiple of 64 KiB). + /// 0 disables linear-memory pre-reservation entirely. + pub linear_memory_size: u64, + /// Number of bytes reserved for the call stack. + pub stack_size: u64, +} + +impl Default for LinkerScriptConfig { + fn default() -> Self { + Self { + flash_origin: 0x0, + ram_origin: 0x8000_0000, + linear_memory_size: 64 * 1024, // 1 wasm page + stack_size: 4096, + } + } +} + +pub struct RiscVLinkerScriptGenerator { + pub hw_caps: HardwareCapabilities, + pub config: LinkerScriptConfig, +} + +impl RiscVLinkerScriptGenerator { + pub fn new(hw_caps: HardwareCapabilities) -> Self { + Self { + hw_caps, + config: LinkerScriptConfig::default(), + } + } + + pub fn with_config(mut self, config: LinkerScriptConfig) -> Self { + self.config = config; + self + } + + pub fn generate(&self) -> String { + let mut out = String::new(); + out.push_str("/* RISC-V Linker Script\n"); + out.push_str(" * Generated by synth-backend-riscv\n"); + out.push_str(&format!( + " * Flash: 0x{:08X} ({} KB), RAM: 0x{:08X} ({} KB)\n", + self.config.flash_origin, + self.hw_caps.flash_size / 1024, + self.config.ram_origin, + self.hw_caps.ram_size / 1024, + )); + out.push_str(&format!( + " * Linear memory: {} KB, stack: {} KB\n", + self.config.linear_memory_size / 1024, + self.config.stack_size / 1024, + )); + out.push_str(" */\n\n"); + + out.push_str("OUTPUT_ARCH(\"riscv\")\n"); + out.push_str("ENTRY(_reset)\n\n"); + + out.push_str("MEMORY {\n"); + out.push_str(&format!( + " flash (rx) : ORIGIN = 0x{:08X}, LENGTH = {}K\n", + self.config.flash_origin, + self.hw_caps.flash_size / 1024 + )); + out.push_str(&format!( + " ram (rwx) : ORIGIN = 0x{:08X}, LENGTH = {}K\n", + self.config.ram_origin, + self.hw_caps.ram_size / 1024 + )); + out.push_str("}\n\n"); + + out.push_str("SECTIONS {\n"); + + // ── .text — code in flash, with the reset vector first ─────── + out.push_str(" .text : {\n"); + out.push_str(" KEEP(*(.text._reset))\n"); + // The trap entry must be 4-byte aligned for mtvec direct mode. + // For vectored mode (mtvec.MODE = 1) it must be 64-byte aligned — + // the conservative default below covers both. + out.push_str(" . = ALIGN(64);\n"); + out.push_str(" KEEP(*(.text._trap_entry))\n"); + out.push_str(" *(.text*)\n"); + out.push_str(" } > flash\n\n"); + + // ── .rodata — read-only constants in flash ────────────────── + out.push_str(" .rodata : {\n"); + out.push_str(" *(.rodata*)\n"); + out.push_str(" *(.srodata*)\n"); + out.push_str(" . = ALIGN(4);\n"); + out.push_str(" PROVIDE(_data_load = .);\n"); + out.push_str(" } > flash\n\n"); + + // ── .data — initialized RW data; load addr in flash, run addr in RAM ─ + out.push_str(" .data : AT(_data_load) {\n"); + out.push_str(" . = ALIGN(4);\n"); + out.push_str(" PROVIDE(_data_start = .);\n"); + out.push_str(" *(.data*)\n"); + out.push_str(" *(.sdata*)\n"); + out.push_str(" . = ALIGN(4);\n"); + out.push_str(" PROVIDE(_data_end = .);\n"); + out.push_str(" } > ram\n\n"); + + // ── .bss — zero-initialized RW data ──────────────────────── + out.push_str(" .bss (NOLOAD) : {\n"); + out.push_str(" . = ALIGN(4);\n"); + out.push_str(" PROVIDE(_bss_start = .);\n"); + out.push_str(" *(.bss*)\n"); + out.push_str(" *(.sbss*)\n"); + out.push_str(" *(COMMON)\n"); + out.push_str(" . = ALIGN(4);\n"); + out.push_str(" PROVIDE(_bss_end = .);\n"); + out.push_str(" } > ram\n\n"); + + // ── Linear memory base — where the wasm heap starts ───────── + if self.config.linear_memory_size > 0 { + out.push_str(" /* Wasm linear memory — selector loads/stores use s11 = this */\n"); + out.push_str(" .linear_memory (NOLOAD) : ALIGN(16) {\n"); + out.push_str(" PROVIDE(__linear_memory_base = .);\n"); + out.push_str(&format!(" . += {};\n", self.config.linear_memory_size)); + out.push_str(" PROVIDE(__linear_memory_end = .);\n"); + out.push_str(" } > ram\n\n"); + } else { + out.push_str(" PROVIDE(__linear_memory_base = _bss_end);\n\n"); + } + + // ── Stack ──────────────────────────────────────────────── + out.push_str(" /* Stack grows down from end of RAM */\n"); + out.push_str(" .stack (NOLOAD) : ALIGN(16) {\n"); + out.push_str(&format!(" . += {};\n", self.config.stack_size)); + out.push_str(" PROVIDE(_stack_top = .);\n"); + out.push_str(" } > ram\n\n"); + + // ── GP — small-data linker-relaxation anchor ───────────── + out.push_str( + " /* Global pointer — points 0x800 above .sdata for short-form RV access */\n", + ); + out.push_str(" PROVIDE(__global_pointer$ = _data_start + 0x800);\n\n"); + + out.push_str("}\n"); + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + use synth_core::{HardwareCapabilities, RISCVVariant, TargetArch}; + + fn rv32imac_caps() -> HardwareCapabilities { + HardwareCapabilities { + arch: TargetArch::RISCV(RISCVVariant::RV32IMAC), + has_mpu: false, + mpu_regions: 0, + has_pmp: true, + pmp_entries: 16, + has_fpu: false, + fpu_precision: None, + has_simd: false, + simd_level: None, + xip_capable: true, + flash_size: 64 * 1024, + ram_size: 64 * 1024, + } + } + + #[test] + fn generates_well_formed_script() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()); + let s = g.generate(); + assert!(s.contains("OUTPUT_ARCH(\"riscv\")")); + assert!(s.contains("ENTRY(_reset)")); + assert!(s.contains("MEMORY {")); + // Flash and RAM regions present + assert!(s.contains("flash (rx)")); + assert!(s.contains("ram (rwx)")); + } + + #[test] + fn places_reset_first_in_text() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()); + let s = g.generate(); + let text_start = s.find(".text :").unwrap(); + let reset_first = s[text_start..].find(".text._reset").unwrap(); + let trap_entry = s[text_start..].find(".text._trap_entry").unwrap(); + let general_text = s[text_start..].find("*(.text*)").unwrap(); + assert!(reset_first < trap_entry); + assert!(trap_entry < general_text); + } + + #[test] + fn data_load_in_flash_run_in_ram() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()); + let s = g.generate(); + assert!(s.contains(".data : AT(_data_load)")); + } + + #[test] + fn provides_global_pointer() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()); + let s = g.generate(); + assert!(s.contains("__global_pointer$")); + // GP convention: _data_start + 0x800 to put .sdata in short-form range + assert!(s.contains("_data_start + 0x800")); + } + + #[test] + fn provides_linear_memory_base() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()); + let s = g.generate(); + assert!(s.contains("__linear_memory_base")); + } + + #[test] + fn linear_memory_size_zero_falls_back_to_bss_end() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()).with_config(LinkerScriptConfig { + linear_memory_size: 0, + ..Default::default() + }); + let s = g.generate(); + assert!(s.contains("PROVIDE(__linear_memory_base = _bss_end);")); + } + + #[test] + fn trap_entry_aligned_64_bytes() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()); + let s = g.generate(); + // The 64-byte ALIGN comes immediately before the trap entry KEEP + let trap_idx = s.find("KEEP(*(.text._trap_entry))").unwrap(); + let preamble = &s[..trap_idx]; + assert!(preamble.rfind(". = ALIGN(64);").is_some()); + } + + #[test] + fn stack_top_provided() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()); + let s = g.generate(); + assert!(s.contains("PROVIDE(_stack_top")); + } + + #[test] + fn custom_flash_origin() { + let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()).with_config(LinkerScriptConfig { + flash_origin: 0x2000_0000, + ..Default::default() + }); + let s = g.generate(); + assert!(s.contains("ORIGIN = 0x20000000")); + } +} diff --git a/crates/synth-backend-riscv/src/startup.rs b/crates/synth-backend-riscv/src/startup.rs new file mode 100644 index 0000000..3fe39f4 --- /dev/null +++ b/crates/synth-backend-riscv/src/startup.rs @@ -0,0 +1,315 @@ +//! RISC-V bare-metal startup code generator. +//! +//! Mirrors `synth-backend::arm_startup` but emits a RISC-V reset vector, +//! mtvec setup, and trap handler skeleton. The output is C source that +//! a bare-metal toolchain (gcc / clang + GNU `ld`) can link with the +//! `.text` produced by `synth-backend-riscv::elf_builder`. +//! +//! What the generated code does on reset: +//! +//! 1. Set the stack pointer (`sp`) to `_stack_top` (linker-defined). +//! 2. Set the global pointer (`gp`) to `__global_pointer$` (linker-defined). +//! 3. Set the linear-memory base register (`s11`) to `__linear_memory_base` +//! so the selector's load/store sequences work without a per-callee +//! re-load. +//! 4. Initialize `mtvec` to point at the trap handler (direct mode). +//! 5. Copy `.data` from flash to RAM. +//! 6. Zero `.bss`. +//! 7. Jump to `main()`. +//! +//! The trap handler is intentionally minimal — it saves the architectural +//! state, marshals `mcause` / `mepc` into args, and calls a C function +//! `synth_trap_handler` that the firmware can override. If the firmware +//! doesn't provide one, the default implementation spins (so a fault is +//! visible under a debugger). + +use synth_core::HardwareCapabilities; + +/// Knobs that control startup-code generation. +#[derive(Debug, Clone, Default)] +pub struct StartupConfig { + /// Number of PLIC-routed external interrupts. The default is 0 + /// (no platform-level interrupt controller wiring) — bump this when + /// targeting a real PLIC chip. + pub external_irq_count: u32, + /// Whether to emit an `mret` from the default trap handler. If false, + /// the handler busy-loops (useful for debugging). + pub trap_returns: bool, + /// Whether to enable the FPU (sets mstatus.FS=1). Only relevant if the + /// target advertises FPU support — this generator just emits the + /// preamble; the firmware is responsible for any per-FP-reg setup. + pub enable_fpu: bool, +} + +pub struct RiscVStartupGenerator { + pub hw_caps: HardwareCapabilities, + pub config: StartupConfig, +} + +impl RiscVStartupGenerator { + pub fn new(hw_caps: HardwareCapabilities) -> Self { + Self { + hw_caps, + config: StartupConfig::default(), + } + } + + pub fn with_config(mut self, config: StartupConfig) -> Self { + self.config = config; + self + } + + /// Generate assembly + C startup code as a single string. + /// The output uses GCC inline-asm syntax (`__asm__("...")`) so the file + /// can be passed to `riscv64-unknown-elf-gcc` directly. + pub fn generate(&self) -> String { + let mut out = String::new(); + out.push_str("/* RISC-V Bare-Metal Startup\n"); + out.push_str(" * Generated by synth-backend-riscv\n"); + out.push_str(&format!( + " * Target: {} ({} PMP entries, FPU: {})\n", + self.hw_caps_summary(), + self.hw_caps.pmp_entries, + self.hw_caps.has_fpu + )); + out.push_str(" */\n\n"); + + out.push_str("#include \n\n"); + + // Symbols the linker script must define. + out.push_str("extern uint32_t _stack_top;\n"); + out.push_str("extern uint32_t __global_pointer$;\n"); + out.push_str("extern uint32_t __linear_memory_base;\n"); + out.push_str("extern uint32_t _data_start, _data_end, _data_load;\n"); + out.push_str("extern uint32_t _bss_start, _bss_end;\n"); + out.push_str("extern int main(void);\n\n"); + + out.push_str("/* Default trap handler — overridable by user code. */\n"); + out.push_str("__attribute__((weak)) void synth_trap_handler(\n"); + out.push_str(" uint32_t mcause, uint32_t mepc, uint32_t mtval) {\n"); + if self.config.trap_returns { + out.push_str(" (void)mcause; (void)mepc; (void)mtval;\n"); + out.push_str(" /* Default: return from trap (mret in caller). */\n"); + } else { + out.push_str(" (void)mcause; (void)mepc; (void)mtval;\n"); + out.push_str(" /* Default: spin so debuggers can inspect the fault. */\n"); + out.push_str(" while (1) { __asm__ volatile(\"wfi\"); }\n"); + } + out.push_str("}\n\n"); + + out.push_str("__attribute__((naked, noreturn, section(\".text._reset\")))\n"); + out.push_str("void _reset(void) {\n"); + out.push_str(" __asm__ volatile(\n"); + out.push_str(" /* Disable interrupts during boot. */\n"); + out.push_str(" \"csrw mie, zero\\n\"\n"); + out.push_str(" \"csrw mip, zero\\n\"\n"); + out.push('\n'); + out.push_str(" /* Initialize sp, gp, and the wasm linear-memory base. */\n"); + out.push_str(" \".option push\\n\"\n"); + out.push_str(" \".option norelax\\n\"\n"); + out.push_str(" \"la sp, _stack_top\\n\"\n"); + out.push_str(" \"la gp, __global_pointer$\\n\"\n"); + out.push_str(" \"la s11, __linear_memory_base\\n\"\n"); + out.push_str(" \".option pop\\n\"\n"); + out.push('\n'); + out.push_str(" /* Install the trap vector (direct mode). */\n"); + out.push_str(" \"la t0, _trap_entry\\n\"\n"); + out.push_str(" \"csrw mtvec, t0\\n\"\n"); + out.push('\n'); + + if self.config.enable_fpu { + out.push_str(" /* Enable the FPU (mstatus.FS = 0b01 'initial'). */\n"); + out.push_str(" \"li t0, 0x2000\\n\"\n"); + out.push_str(" \"csrs mstatus, t0\\n\"\n"); + out.push('\n'); + } + + out.push_str(" /* Copy .data from flash to RAM. */\n"); + out.push_str(" \"la t0, _data_load\\n\"\n"); + out.push_str(" \"la t1, _data_start\\n\"\n"); + out.push_str(" \"la t2, _data_end\\n\"\n"); + out.push_str(" \"1:\\n\"\n"); + out.push_str(" \"bgeu t1, t2, 2f\\n\"\n"); + out.push_str(" \"lw t3, 0(t0)\\n\"\n"); + out.push_str(" \"sw t3, 0(t1)\\n\"\n"); + out.push_str(" \"addi t0, t0, 4\\n\"\n"); + out.push_str(" \"addi t1, t1, 4\\n\"\n"); + out.push_str(" \"j 1b\\n\"\n"); + out.push_str(" \"2:\\n\"\n"); + out.push('\n'); + out.push_str(" /* Zero .bss. */\n"); + out.push_str(" \"la t0, _bss_start\\n\"\n"); + out.push_str(" \"la t1, _bss_end\\n\"\n"); + out.push_str(" \"3:\\n\"\n"); + out.push_str(" \"bgeu t0, t1, 4f\\n\"\n"); + out.push_str(" \"sw zero, 0(t0)\\n\"\n"); + out.push_str(" \"addi t0, t0, 4\\n\"\n"); + out.push_str(" \"j 3b\\n\"\n"); + out.push_str(" \"4:\\n\"\n"); + out.push('\n'); + out.push_str(" /* Jump to main. If main returns, spin. */\n"); + out.push_str(" \"call main\\n\"\n"); + out.push_str(" \"5: wfi\\n\"\n"); + out.push_str(" \"j 5b\\n\"\n"); + out.push_str(" );\n"); + out.push_str("}\n\n"); + + out.push_str("/* Trap entry — saves the caller-saved register set, marshals\n"); + out.push_str(" * mcause/mepc/mtval into a0/a1/a2, and dispatches to the C handler.\n"); + out.push_str(" */\n"); + out.push_str("__attribute__((naked, aligned(4), section(\".text._trap_entry\")))\n"); + out.push_str("void _trap_entry(void) {\n"); + out.push_str(" __asm__ volatile(\n"); + out.push_str(" \"addi sp, sp, -64\\n\"\n"); + out.push_str(" \"sw ra, 0(sp)\\n\"\n"); + out.push_str(" \"sw t0, 4(sp)\\n\"\n"); + out.push_str(" \"sw t1, 8(sp)\\n\"\n"); + out.push_str(" \"sw t2, 12(sp)\\n\"\n"); + out.push_str(" \"sw a0, 16(sp)\\n\"\n"); + out.push_str(" \"sw a1, 20(sp)\\n\"\n"); + out.push_str(" \"sw a2, 24(sp)\\n\"\n"); + out.push_str(" \"sw a3, 28(sp)\\n\"\n"); + out.push_str(" \"sw a4, 32(sp)\\n\"\n"); + out.push_str(" \"sw a5, 36(sp)\\n\"\n"); + out.push_str(" \"sw a6, 40(sp)\\n\"\n"); + out.push_str(" \"sw a7, 44(sp)\\n\"\n"); + out.push_str(" \"sw t3, 48(sp)\\n\"\n"); + out.push_str(" \"sw t4, 52(sp)\\n\"\n"); + out.push_str(" \"sw t5, 56(sp)\\n\"\n"); + out.push_str(" \"sw t6, 60(sp)\\n\"\n"); + out.push('\n'); + out.push_str(" \"csrr a0, mcause\\n\"\n"); + out.push_str(" \"csrr a1, mepc\\n\"\n"); + out.push_str(" \"csrr a2, mtval\\n\"\n"); + out.push_str(" \"call synth_trap_handler\\n\"\n"); + out.push('\n'); + out.push_str(" \"lw ra, 0(sp)\\n\"\n"); + out.push_str(" \"lw t0, 4(sp)\\n\"\n"); + out.push_str(" \"lw t1, 8(sp)\\n\"\n"); + out.push_str(" \"lw t2, 12(sp)\\n\"\n"); + out.push_str(" \"lw a0, 16(sp)\\n\"\n"); + out.push_str(" \"lw a1, 20(sp)\\n\"\n"); + out.push_str(" \"lw a2, 24(sp)\\n\"\n"); + out.push_str(" \"lw a3, 28(sp)\\n\"\n"); + out.push_str(" \"lw a4, 32(sp)\\n\"\n"); + out.push_str(" \"lw a5, 36(sp)\\n\"\n"); + out.push_str(" \"lw a6, 40(sp)\\n\"\n"); + out.push_str(" \"lw a7, 44(sp)\\n\"\n"); + out.push_str(" \"lw t3, 48(sp)\\n\"\n"); + out.push_str(" \"lw t4, 52(sp)\\n\"\n"); + out.push_str(" \"lw t5, 56(sp)\\n\"\n"); + out.push_str(" \"lw t6, 60(sp)\\n\"\n"); + out.push_str(" \"addi sp, sp, 64\\n\"\n"); + out.push_str(" \"mret\\n\"\n"); + out.push_str(" );\n"); + out.push_str("}\n"); + + out + } + + fn hw_caps_summary(&self) -> &'static str { + match &self.hw_caps.arch { + synth_core::TargetArch::RISCV(variant) => match variant { + synth_core::RISCVVariant::RV32I => "RV32I", + synth_core::RISCVVariant::RV32IMAC => "RV32IMAC", + synth_core::RISCVVariant::RV32GC => "RV32GC", + synth_core::RISCVVariant::RV64I => "RV64I", + synth_core::RISCVVariant::RV64IMAC => "RV64IMAC", + synth_core::RISCVVariant::RV64GC => "RV64GC", + }, + _ => "non-RISC-V", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use synth_core::{HardwareCapabilities, RISCVVariant, TargetArch}; + + fn rv32imac_caps() -> HardwareCapabilities { + HardwareCapabilities { + arch: TargetArch::RISCV(RISCVVariant::RV32IMAC), + has_mpu: false, + mpu_regions: 0, + has_pmp: true, + pmp_entries: 16, + has_fpu: false, + fpu_precision: None, + has_simd: false, + simd_level: None, + xip_capable: true, + flash_size: 64 * 1024, + ram_size: 64 * 1024, + } + } + + #[test] + fn generates_reset_vector() { + let g = RiscVStartupGenerator::new(rv32imac_caps()); + let code = g.generate(); + assert!(code.contains("_reset")); + assert!(code.contains("la sp, _stack_top")); + assert!(code.contains("la s11, __linear_memory_base")); + assert!(code.contains("csrw mtvec, t0")); + } + + #[test] + fn generates_data_init_loop() { + let g = RiscVStartupGenerator::new(rv32imac_caps()); + let code = g.generate(); + assert!(code.contains("la t0, _data_load")); + assert!(code.contains("_bss_start")); + assert!(code.contains("_bss_end")); + } + + #[test] + fn generates_trap_entry() { + let g = RiscVStartupGenerator::new(rv32imac_caps()); + let code = g.generate(); + assert!(code.contains("_trap_entry")); + assert!(code.contains("csrr a0, mcause")); + assert!(code.contains("csrr a1, mepc")); + assert!(code.contains("csrr a2, mtval")); + assert!(code.contains("call synth_trap_handler")); + assert!(code.contains("mret")); + } + + #[test] + fn fpu_enable_only_when_configured() { + let gen_no_fpu = RiscVStartupGenerator::new(rv32imac_caps()); + assert!(!gen_no_fpu.generate().contains("Enable the FPU")); + + let gen_with_fpu = RiscVStartupGenerator::new(rv32imac_caps()).with_config(StartupConfig { + enable_fpu: true, + ..Default::default() + }); + let code = gen_with_fpu.generate(); + assert!(code.contains("Enable the FPU")); + assert!(code.contains("mstatus")); + } + + #[test] + fn trap_default_spins_when_returns_disabled() { + let g = RiscVStartupGenerator::new(rv32imac_caps()); + let code = g.generate(); + // wfi loop in default trap handler (the C version, not the asm trap_entry) + assert!(code.contains("synth_trap_handler")); + assert!(code.contains("wfi")); + } + + #[test] + fn weak_attribute_on_default_handler() { + let g = RiscVStartupGenerator::new(rv32imac_caps()); + let code = g.generate(); + // weak so user firmware can override the default handler + assert!(code.contains("__attribute__((weak)) void synth_trap_handler")); + } + + #[test] + fn naked_attribute_on_reset() { + let g = RiscVStartupGenerator::new(rv32imac_caps()); + let code = g.generate(); + assert!(code.contains("__attribute__((naked, noreturn")); + } +}