Needs Mica 7.5.1. The code on this page uses the heapless word, which arrives with the next compiler release, Mica 7.5.1 — not released yet. Everything shown here ran with that release’s development build.

What is left of a programming language when the operating system is gone? No Linux, no C library, no process to start in — just a processor, some memory, and a handful of hardware registers.

On 15 September we tried it with Mica. In one afternoon, on QEMU’s emulated ARM board, Mica ran as a library inside a small kernel, as the kernel itself, as a program using that kernel’s API, and as the handler of a hardware timer interrupt. The whole small operating system beneath it took 488 lines of C and assembly. The Mica compiler did not change at all.

This was an experiment, not a feature you can switch on today. But it answers a question with a real run instead of a guess: yes, the code Mica produces runs where there is no operating system.

Why it worked without touching the compiler

None of the pieces was built for bare metal. They were built so that the parts of Mica stay apart — and parts that stay apart can be swapped:

  • The standard library is replaceable. The Dragon SDK already lets a compiler link emitted programs without Mica’s standard library.
  • The runtime beneath a program is chosen per build. Every build picks the runtime archive for its memory class and tasking; the kernel simply supplied its own.
  • Memory can come from a fixed block. The fixed arena needs nothing from an operating system.
  • heapless routines never touch the heap. The compiler checks that promise, so a routine carrying the word needs no allocator at all.

The picture

Each layer stands on the one below it. Everything above the board is the lab’s own code.

  Mica code       a library · the kernel · an application · an interrupt handler
  runtime floor   the three small things Mica code really uses from its host
  C kernel        serial port · exit · interrupt controller · the kernel's API
  boot code       floating point switched on · a stack · memory cleared
  the board       an emulated ARM computer with no operating system

Without an operating system, every address is a physical one. The devices sit at fixed places on the board, and a linker script of 28 lines decides where the kernel lives:

  0x0800_0000   interrupt controller
  0x0900_0000   serial port
  0x4000_0000   RAM begins
  0x4008_0000   the kernel's code: the boot code first, then C and Mica
  0x4008_0800   the vector table the processor jumps into on an interrupt
  0x4008_1338   OnTick, the interrupt handler written in Mica
  0x4008_18e0   text and constant data
  0x4008_2450   32 bytes of variables
  0x4008_2470   256 KiB of stack

The compiler built the Mica code exactly as it does for Linux. The lab kept the objects and linked them with its own memory layout:

  Mica source                        boot code and kernel C
      │                                  │
      ▼                                  ▼
  mica compiler, unchanged           gcc -ffreestanding
      │                                  │
      ▼                                  ▼
  Mica objects                       kernel objects
      │                                  │
      └────────────────┬─────────────────┘
           gcc -nostdlib -T link.ld
           a kernel image of 691 bytes to 8.3 KB
           QEMU's emulated ARM board

Stage one: a kernel calls Mica

The first kernel is written in C, and it calls Mica routines compiled into an ordinary library archive:

exp heapless function SumOfSquares(n : int64) : int64;
var
    k : int64;
    acc : int64;
begin
    acc := 0;
    for k := 1 to n do
        acc := acc + Square(k);
    SumOfSquares := acc;
end;

exp heapless function Divide(a : int64, b : int64) : int64;
begin
    Divide := a / b;
end;

To the kernel they are plain functions. The last call divides by zero on purpose, and the checked build still catches it: the report names the exact Mica source position, even with no operating system anywhere.

kernel: calling Mica
  SumOfSquares(10) = 385
  Tick x3: ticks 3, peak 42
  Divide(84, 2) = 42
  Divide(1, 0) at the checked tier ...
Mica runtime failure: division by zero at BareLib.mica:38:17

Stage two: Mica is the kernel

In the second kernel, C does nothing but call the Mica program’s main. From there on, Mica runs the machine. The serial port’s data register is an ordinary Mica variable at a fixed address, so every assignment to it sends a character:

var
    uart : uint32 absolute 0x0900_0000;

heapless procedure Put(text : string);
var
    k : int64;
begin
    for k := 0 to Length(text) - 1 do
        uart := Ord(text[k]) as uint32;
end;
boot: entering the Mica program
floor: process initialization, no command line on bare metal
hello from a Mica kernel
sum of squares 1..10 = 385
boot: the Mica program returned 0

Stage three: a Mica program on the kernel’s API

A real operating system offers its programs an API. The third kernel offers three C functions, and a Mica program imports them through a contract, exactly the way Mica imports the C library on Linux:

imp
    Length, Ord : std;
    PutRune, Ticks, TickFrequency : kernel;
void kernel_put_rune(int64_t rune) {
    if (rune == '\n') {
        uart_putc('\r');
    }

    uart_putc((char)rune);
}

The contract promises that none of the three functions uses the heap, so the program’s heapless routines may call them. The program counts a million steps and times itself with the processor’s own clock:

kernel: starting the Mica program
floor: process initialization, no command line on bare metal
hello from a Mica program on the small OS
a million steps, total 2999998
timed with the kernel's clock: 5043 microseconds (varies per run)
kernel: the Mica program returned 0

Stage four: an interrupt handled in Mica

The last kernel arms the timer and waits. When the timer fires, the processor leaves whatever it was doing, and a Mica routine handles the interrupt:

exp heapless procedure OnTick(state : pointer TickState, now : int64);
begin
    state.ticks := state.ticks + 1;
    PutText("interrupt handled in Mica: tick ");
    PutNumber(state.ticks);
    if state.last > 0 then begin
        PutText(", ");
        PutNumber(now - state.last);
        PutText(" counter ticks after the previous one");
    end;
    PutRune(10);
    state.last := now;
end;

Between the timer and the Mica routine sit a few steps of assembly and C:

    the timer fires
    the interrupt controller signals the processor
    the processor jumps into the vector table
    the assembly entry saves every register the interrupted code may hold
    the C dispatcher acknowledges the interrupt and calls OnTick
    OnTick, a heapless Mica routine, counts and prints the tick
    the C dispatcher re-arms the timer and ends the interrupt
    the assembly entry restores the registers and returns
    the kernel's waiting loop continues exactly where it stopped
kernel: the timer interrupts every 100 ms, and a Mica routine handles it
interrupt handled in Mica: tick 1
interrupt handled in Mica: tick 2, 6613143 counter ticks after the previous one
interrupt handled in Mica: tick 3, 6628604 counter ticks after the previous one
interrupt handled in Mica: tick 4, 6307229 counter ticks after the previous one
interrupt handled in Mica: tick 5, 6615365 counter ticks after the previous one
kernel: five interrupts handled, stopping

heapless is what makes this safe to write: an interrupt can arrive in the middle of anything, and a routine that promises never to use the heap cannot trip over an allocation the interrupted code left half done.

How much it took

PartLines of code
boot code, assembly33
vector table and interrupt entry, assembly133
linker script28
serial port, exit and memory routines, C85
the five kernels, the interrupt controller and the kernel’s API, C141
the runtime floor beneath Mica, C68
the small operating system, together488
Mica: a library, a kernel, an application and an interrupt handler183
changes to the Mica compiler0

Nearly half of the 133 lines of interrupt assembly is the entry that saves and restores the interrupted code’s registers. The kernel images are tiny: 691 bytes for the first, 8.3 KB for the one with interrupts.

What it means

For a Mica developer, this opens a door that sounds almost too good: the same language, the same compiler, the same checked builds, on a machine with nothing underneath — firmware, a teaching kernel, the inside of a device.

For embedded work, it makes a long-standing design goal tangible. Mica programs could already run without a heap; now we have watched Mica code run without an operating system, too, handle a hardware interrupt, and report a bug with its source line while doing it.

The lab did the last step by hand: it linked the compiler’s objects with its own memory layout. Teaching the compiler and the Dragon SDK to take that step themselves is the next piece of work, followed by real boards beyond the emulator. We could not be happier with where this afternoon left us.