No. 31 / project of 147 on the ladder

RV32I C runtime probe

introduces — Freestanding C main, ABI smoke, C-visible interrupt flags, and MMIO from C

harden statelast run2026-05-01

P31 keeps the P30 platform and changes the software shape again. The reset entry is still assembly, but it now clears .bss, installs mtvec, and calls a freestanding C main().

Status: RTL pass. The compiled C runtime test passes, and the scoped ACT4/Sail rv32i/I batch still passes with PASS=39 FAIL=0 NOT RUN=0. Hardening and official privileged tests are NOT RUN for this rung.

The Result

Run it:

make -C projects/31_rv32i_c_runtime_probe/test

Result: PASS

checkresult
Build linked C runtime ELFPASS
UART-load compiled imagePASS
Clear .bss before main()PASS
Call C main() from reset assemblyPASS
Exercise C function calls and globalsPASS
MMIO UART writes C, T, EPASS
MMIO timer interrupt visible to CPASS
External interrupt visible to CPASS
Halt with x5 = 1PASS

ACT4/Sail rv32i/I: PASS=39 FAIL=0 NOT RUN=0

Harden status: NOT RUN

Runtime Shape

piecebehavior
runtime/link.ldplaces text, data, and BSS in low external RAM
runtime/start.Sreset entry, BSS clear, trap glue, call into C
runtime/main.cfreestanding C platform probe
runtime/build/c_runtime.elfgenerated linked ELF
runtime/build/c_runtime.memhgenerated byte image for the UART loader

The runtime targets rv32i_zicsr: base integer plus CSR instructions. The C file itself is normal freestanding C, but the startup/trap glue still needs machine CSR access.

What Changed

P30 stopped hand-encoding the program. P31 stops keeping the program entirely in assembly.

P31 adds:

  • a C main() reached through the assembly reset path;
  • C-visible BSS globals used by the trap handler;
  • volatile MMIO access from C;
  • simple stack/function-call behavior from compiler output;
  • a linker map artifact for inspecting placement;
  • a no-RWX-segment linker layout so the bare-metal build is quiet.

The generated C runtime binary is small: 540 bytes in the current build. That is intentionally modest. This rung is about proving the path, not writing a firmware library.

What This Proves

The compiled runtime:

  1. boots from address zero after the UART loader releases the CPU;
  2. clears .bss before main();
  3. uses C globals and volatile scratch storage in external RAM;
  4. writes C to the MMIO UART;
  5. programs the MMIO timer from C;
  6. takes a machine timer interrupt and returns through mret;
  7. observes the timer flag from C, writes T, then enables external IRQ;
  8. observes the external IRQ flag from C and writes E;
  9. returns 1, which the assembly glue copies into x5 before halting.

The scoped ACT4/Sail run still matters because the RTL shell is the same base-integer core lineage. The base integer batch still passes: PASS=39 FAIL=0 NOT RUN=0.

Scope

Still unsupported: libc, heap allocation, constructors, broad ABI stress, supervisor mode, vectored mtvec, delegation CSRs, satp, an MMU, and official privileged architecture tests.

So the honest label is narrow: PASS for the compiled C-runtime RTL test, PASS for scoped ACT4/Sail rv32i/I, NOT RUN for hardening, and NOT RUN for privileged compliance.