Bare-metal hello
profile bare is for kernels, firmware and bootloaders: code that runs with no operating system and no runtime. This tutorial builds one program for three machines and boots it in QEMU.
The program
profile bare
// ANCHOR: putc
fn putc(c: Int) ! mmio, div
= if arch() == "riscv64" then do
lsr = mmio[U8](0x10000005)
while lsr.read.band(0x20) == 0
()
mmio[U8](0x10000000).write(c)
else if arch() == "thumbv7em" then do
mmio[U32](0x40004008).write(1)
while mmio[U32](0x40004004).read.band(1) != 0
()
mmio[U32](0x40004000).write(c)
else do
fr = mmio[U32](0x09000018)
while fr.read.band(0x20) != 0
()
mmio[U32](0x09000000).write(c)
// ANCHOR_END: putc
fn puts(s: Str) ! mmio, div
= do
for i in 0..s.byte_len
putc(s.byte(i))
fn main() ! mmio, div
= puts("hello from sspur\n")
test bytes = "hi".byte(1) == 105 and "hi".byte_len == 2
profile bareon the first line turns on the bare rules. There is no garbage-collected heap, so lists, sum types, lambdas, string building,logand errors are compile errors (E_PROFILE_BARE). Records, tuples, fixed arraysArray[T, N], static strings and integers remain, plus everything fromprofile sys.mmio[U8](addr)is a volatile hardware register..readand.write(v)perform themmioeffect, and the polling loops performdiv, so both are in every signature.arch()compared with a literal is folded at compile time: each build keeps only its own UART branch.- Tests run on the host. Pure code like the
bytestest works as usual.
$ sspur test hello.ssp
1 passed, 0 failed
$ sspur run hello.ssp
runtime error: mmio needs a bare target: build with 'sspur build --target riscv64-qemu|aarch64-qemu'
Build and boot
sspur build --target writes freestanding C, a startup file and a linker script, compiles them with clang and links with ld.lld. It prints the QEMU command to boot the result:
$ sspur build --target aarch64-qemu hello.ssp -o kernel.elf
built kernel.elf (aarch64-qemu; sources in kernel.elf.build)
boot: qemu-system-aarch64 -machine virt -cpu cortex-a53 -smp 2 -m 64M -semihosting -nographic -monitor none -no-reboot -kernel kernel.elf
Run that command and the kernel prints hello from sspur, then main returns, which powers the machine off with exit status 0. The same file builds for a Cortex-M4:
$ sspur build --target thumbv7em-mps2 hello.ssp -o fw.elf
built fw.elf (thumbv7em-mps2; sources in fw.elf.build)
boot: qemu-system-arm -machine mps2-an386 -cpu cortex-m4 -semihosting -nographic -monitor none -no-reboot -kernel fw.elf
The riscv64-qemu target works the same way, but needs a clang with the RISC-V backend: Apple’s clang has none, so set SSPUR_BARE_CC to Homebrew’s LLVM.
Where to go from here
| Feature | How |
|---|---|
| Interrupts | fn tick() ! mmio, div with an interrupt timer line after the signature; timer_start(ticks), wait_irq() |
| Module state | static count: Int = 0, with count.add(1), .load, .cas(old, new); each access is atomic with respect to interrupts, and racy read-then-store is E_STATIC_RACE |
| Several cores | start_core(id) and fn core_main(id: Int) on aarch64-qemu |
| Traps | Define fn on_trap(code: Int); a trap halts with status 64 + code |
| Inline assembly | asm "template" in(x: e) out(r: Int) clobber("memory"), which performs unsafe |
The language reference has the full rules and the memory map of each target, ADR 0017 the design, and tests/bare/ has a timer, statics, floats and a two-core program.