journal 2026-05-04

P68 AtomVM port — bare-metal ELF links

p68atomvmbeamerlangbare-metaltoolchainpartial

Follow-up to the scaffolding session. The scaffolding pass ended with everything written but nothing compiled. This session takes it from there to a linked bare-metal ELF with zero undefined references. Boot blob assembly and sim are still NOT RUN because packbeam (the AtomVM tool that bundles .beam -> .avm) needs rebar3, which is a separate devshell yak.

What landed

flake.nix

Added two parallel pieces:

test/atomvm_baremetal/

A small wrapper CMakeLists.txt + toolchain file. The wrapper add_subdirectory()s the upstream vendor/AtomVM/src/libAtomVM without going through the top-level CMakeLists.txt (which hard-fails on any system that isn’t Darwin/Linux/FreeBSD/DragonFly). The wrapper pre-seeds the rp2 platform’s set(HAVE_FOO "" CACHE INTERNAL FORCE) list — those symbols are declared in newlib headers but not implemented, and without the forces the CheckCSourceCompiles probes succeed and we’d compile in code that fails at link.

The toolchain file selects riscv32-none-elf-, sets CMAKE_SYSTEM_NAME=Generic (so libAtomVM/CMakeLists.txt skips its per-system platform link library), and crucially sets the arch flags to -march=rv32imafd_zba_zbb_zicsr_zifencei -mabi=ilp32d.

The “lying about FP” hack

The chip has no FPU. The natural ABI is -mabi=ilp32. But the nixpkgs riscv32-none-elf toolchain is built --disable-multilib against rv32gc/ilp32d (hard-FP), so its libc.a and libgcc.a are tagged “double-float” — and ld refuses to link them against soft-float objects with:

can't link double-float modules with soft-float modules

I went down two dead ends before settling on the right fix:

  1. Provide soft-FP stubs ourselves (__muldf3, __adddf3, etc.) so -lgcc could be dropped. This works for libgcc but newlib’s own libc.a also wants the helpers internally, and dozens of newlib objects are tagged hard-FP regardless of what symbols we provide. Dead end.
  2. Stop linking libc (-nostdlib + reimplement printf/strtoul from scratch). Too much surface to reproduce.
  3. Lie about FP to the compiler: pass -march=rv32imafd_zba_zbb -mabi=ilp32d at every compile site. The libraries are now ABI-compatible with our objects, the link succeeds, and our reachable C never performs floating-point arithmetic so the compiler doesn’t emit FP instructions and the chip (which lacks FP regs) never sees one. This is what committed.

This is a sharp edge. If a future P68 demo actually uses floats the compiler will emit FP instructions and the chip will trap as illegal-instruction. The long-term fix is either (a) a multilib toolchain rebuild targeting rv32ima/ilp32 or (b) a vendored soft-fp compiler-rt + custom newlib. Both are too expensive for hello-world. Documented in the toolchain file and the project Makefile.

What now compiles

[37/37] Linking C static library libAtomVM/liblibAtomVM.a
riscv32-none-elf-gcc ... -o p68_atomvm.elf ... \
  -Wl,--start-group libAtomVM/liblibAtomVM.a -lc -lm -lgcc -Wl,--end-group
ld.bfd: warning: p68_atomvm.elf has a LOAD segment with RWX permissions

The RWX warning is from our linker script lumping .text, .data, and .bss into one segment — fine for bare-metal where there’s no MMU enforcing W^X. Not a blocker.

$ readelf -h p68_atomvm.elf | head -12
  Type: EXEC (Executable file)
  Machine: RISC-V
  Entry point address: 0x0
  Flags: 0x5, RVC, double-float ABI
$ nm p68_atomvm.elf | grep -E "_start|main|globalcontext_run"
00000000 T _start
0000047c T main
00001588 T globalcontext_run
$ nm -u p68_atomvm.elf | wc -l
0

Zero undefined references. Entry at 0x0 is _start (sets sp, mtvec, clears bss, jumps to main). 6 MiB ELF, 728 KiB raw binary.

Honest status

stepstatus
flake.nix toolchain + gperfPASS
vendor-fetch (clones AtomVM at pinned commit)PASS
libatomvm-build (cmake + ninja + 37 obj)PASS
beam (erlc hello.erl)PASS — 880 byte .beam
Bare-metal ELF linkPASS — zero undefined refs
objcopy -O binary -> raw .binPASS — 728 KiB
packbeam host toolNOT RUN — needs rebar3
main.avm AVMPack assemblyNOT RUN
Boot blob (16 MiB image with ELF + AVMPack)NOT RUN
Verilator sim shows AtomVM bannerNOT RUN

Surprises

Next session

In rough order:

  1. Get packbeam working. The AtomVM CMakeLists builds it via rebar3 escriptize, which needs Erlang’s rebar3 package manager. Either add pkgs.rebar3 to flake.nix or vendor a pre-built packbeam escript.
  2. make all to assemble the 16 MiB boot blob.
  3. make verilator-run. If we get to “Found startup beam: hello” the FP stubs hold and the platform shim is wired correctly.
  4. If FP traps on real execution, chase down the call site in the disassembly and see whether it’s a cold path (data init) or actually reachable.

What just happened?

Burned a session on the toolchain question and ended with a bare-metal RV32 ELF that links cleanly against AtomVM. The “compile bare-metal C against newlib via nixpkgs” recipe is now documented in flake.nix + test/atomvm_baremetal/. The remaining work to actually run hello-world on the chip is small in code-volume but blocked on packbeam, which is a rebar3 problem rather than a code problem.