Skip to content

feat(riscv): bare-metal startup + linker script generators#89

Merged
avrabe merged 1 commit into
mainfrom
feat/riscv-startup-mtvec
May 10, 2026
Merged

feat(riscv): bare-metal startup + linker script generators#89
avrabe merged 1 commit into
mainfrom
feat/riscv-startup-mtvec

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 3, 2026

Track B3 — adds the runtime infrastructure that turns synth's RISC-V .text into a bootable firmware ELF. Stacked on #88.

What this adds

`startup.rs` — `RiscVStartupGenerator`

Emits C source (with inline RISC-V asm) that:

  1. Disables interrupts (mie/mip = 0)
  2. Sets sp/gp/s11 (linear-memory base) from linker symbols
  3. Installs `_trap_entry` into mtvec (direct mode)
  4. Optionally enables the FPU
  5. Copies .data from flash to RAM, zeroes .bss
  6. Calls main(); spins via wfi if it returns

Trap entry saves caller-saved registers, marshals mcause/mepc/mtval into a0/a1/a2, dispatches to a weak `synth_trap_handler` (firmware-overridable).

`linker_script.rs` — `RiscVLinkerScriptGenerator`

Emits a GNU-`ld` script that:

  • Places `.text._reset` at the entry point, then `.text._trap_entry` (64-byte aligned for vectored-mode compatibility), then general `.text*`.
  • Splits `.data` between flash (load) and RAM (run) for the startup copy loop.
  • Provides `_stack_top`, `_data_start/_end/_load`, `_bss_start/_end`, `__linear_memory_base/_end`, `__global_pointer$`.

Memory model

The wasm linear-memory base lives in s11 — chosen because the RV psABI marks it callee-saved, so all selector load/store sequences (`add tmp, s11, addr; lw rd, offset(tmp)`) work without per-callee re-loading.

Test summary

  • 16 new tests (8 startup, 8 linker_script). RISC-V tests now total 98 (was 82).
  • All workspace tests pass; clippy/fmt clean.

Out of scope (B4)

  • CLI `--link` integration with riscv64-unknown-elf-gcc
  • Calculator demo running on Renode RV32 firmware
  • Kiln-builtins-style host bridge for RV32

🤖 Generated with Claude Code

@avrabe avrabe force-pushed the feat/riscv-i32-selector branch from 9e891b1 to 0abbecb Compare May 10, 2026 05:21
@avrabe avrabe force-pushed the feat/riscv-startup-mtvec branch from 6a7df86 to 6e25fef Compare May 10, 2026 05:21
@avrabe avrabe changed the base branch from feat/riscv-i32-selector to main May 10, 2026 05:24
@avrabe avrabe closed this May 10, 2026
@avrabe avrabe reopened this May 10, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

Codecov Report

❌ Patch coverage is 98.01489% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-backend-riscv/src/startup.rs 96.42% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe avrabe force-pushed the feat/riscv-startup-mtvec branch 2 times, most recently from 4eac182 to adb57b1 Compare May 10, 2026 12:34
Track B3 — adds the runtime infrastructure that turns synth's RISC-V
.text into a bootable firmware ELF.

## startup.rs — RiscVStartupGenerator

Emits C source (with inline RISC-V asm) that:
1. Disables interrupts (mie, mip = 0)
2. Sets sp/gp/s11 (linear-memory base) from linker symbols
3. Installs `_trap_entry` into mtvec (direct mode)
4. Optionally enables the FPU via mstatus.FS = 1
5. Copies .data from flash to RAM
6. Zeroes .bss
7. Calls main(); spins via `wfi` if main returns

The trap entry saves the AAPCS-style caller-saved set (ra, t0..t6, a0..a7),
marshals mcause/mepc/mtval into a0/a1/a2, and dispatches to a weak C
function `synth_trap_handler` that the firmware can override.

`StartupConfig` exposes three knobs: external_irq_count (PLIC wiring),
trap_returns (mret vs spin in default), enable_fpu (mstatus FPU enable).

## linker_script.rs — RiscVLinkerScriptGenerator

Emits a GNU-`ld` script that:
- Places `.text._reset` at the entry point so the reset vector is at the
  start of flash.
- Aligns `.text._trap_entry` to 64 bytes (works for both mtvec direct and
  vectored modes — vectored requires 64-byte alignment).
- Splits `.data` between flash (load address) and RAM (run address) so
  the startup copy loop has well-defined bounds.
- Provides `_stack_top`, `_data_start`/`_data_end`/`_data_load`,
  `_bss_start`/`_bss_end`, `__linear_memory_base`/`__linear_memory_end`,
  and `__global_pointer$` (= `_data_start + 0x800`, the standard RV
  small-data anchor).
- `LinkerScriptConfig` configures flash/ram origins, linear-memory size,
  and stack size.

## Memory model

The wasm linear-memory base lives in **s11** — chosen because the RV
psABI marks it callee-saved, so all selector-emitted load/store sequences
(`add tmp, s11, addr; lw rd, offset(tmp)`) work without per-callee
re-loading. The startup code pre-loads s11 from `__linear_memory_base`,
which the linker script positions in RAM.

## Test summary

* 16 new tests (8 in startup, 8 in linker_script). Total RISC-V tests
  now 98 (was 82).
* All workspace tests still pass.
* `cargo clippy --workspace --all-targets -- -D warnings` clean.

## Out of scope (B4)

* CLI integration of `--link` for RISC-V (riscv64-unknown-elf-gcc invocation)
* PLIC wiring (external_irq_count is exposed but not yet emitted)
* Calculator demo running on Renode RV32 with full firmware
* Kiln-builtins-style host bridge for RV32

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avrabe avrabe force-pushed the feat/riscv-startup-mtvec branch from adb57b1 to 08203f9 Compare May 10, 2026 15:17
@avrabe avrabe merged commit 244634e into main May 10, 2026
7 checks passed
@avrabe avrabe deleted the feat/riscv-startup-mtvec branch May 10, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant