Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 bare on the first line turns on the bare rules. There is no garbage-collected heap, so lists, sum types, lambdas, string building, log and errors are compile errors (E_PROFILE_BARE). Records, tuples, fixed arrays Array[T, N], static strings and integers remain, plus everything from profile sys.
  • mmio[U8](addr) is a volatile hardware register. .read and .write(v) perform the mmio effect, and the polling loops perform div, 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 bytes test 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

FeatureHow
Interruptsfn tick() ! mmio, div with an interrupt timer line after the signature; timer_start(ticks), wait_irq()
Module statestatic 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 coresstart_core(id) and fn core_main(id: Int) on aarch64-qemu
TrapsDefine fn on_trap(code: Int); a trap halts with status 64 + code
Inline assemblyasm "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.