Needs Mica 7.5.1. The code on this page uses the
heaplessword, 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.
heaplessroutines 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 systemWithout 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 stackThe 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 boardStage 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:17Stage 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 0Stage 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 0Stage 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 stoppedkernel: 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, stoppingheapless 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
| Part | Lines of code |
|---|---|
| boot code, assembly | 33 |
| vector table and interrupt entry, assembly | 133 |
| linker script | 28 |
| serial port, exit and memory routines, C | 85 |
| the five kernels, the interrupt controller and the kernel’s API, C | 141 |
| the runtime floor beneath Mica, C | 68 |
| the small operating system, together | 488 |
| Mica: a library, a kernel, an application and an interrupt handler | 183 |
| changes to the Mica compiler | 0 |
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.