Blinking an LED (again)

Back in January last year I wrote a simple blinking LED example for an STM32 microcontroller using Inferno's C compiler. Since then I've been working on getting Inferno itself to run on the microcontroller. That was one of the reasons I was interesting in getting into microcontrollers — the other being that the MicroMod processor boards are so cute!

A long break

I tried building a kernel in February last year, starting by getting the clock functions set up and enabling a UART to help with debugging. This relied heavily on the bare metal work I'd put in beforehand. I put a lot of effort into getting support for preemption into what I hoped was a basically working state. Despite this, I kept on running into problems where the memory allocator would fall over with what looked like corruption. Tweaking memory usage didn't really help, and I put the whole effort on hold while I went back to bare metal coding.

Later in 2022, I picked up the port again and tried to figure out if I could make it work. Even though I'd had a sort-of-running system, it didn't encourage me to play with it when it could crash any time I ran a program. After much flailing around, I found that I hadn't done enough work on the preemption code to preserve processor state, and fixing this made the system a lot more stable. With something a bit more robust, it became possible to bring some device drivers over from my bare metal code and write some Limbo programs.

Namespace and devices

When Inferno boots on the development board, the init process sets up a namespace that includes a fairly minimal set of devices in the /dev directory:

Initial Dis: "/osinit.dis"
** Inferno
** Vita Nuova
stm32f405$ ls /dev | mc
ink            hostowner      memory         sysname
cons           keyboard       msec           time
consctl        klog           null           user
sysctl         kprint         random         jit
drivers        scancode       notquiterandom leds

You can see the commands that can be used to reconstruct this namespace by running the ns command:

stm32f405$ ns
bind /dev /dev
bind -a '#c' /dev
bind -a '#L' /dev
bind /dev/ink /dev/ink
bind -a '#u' /dev/ink
bind -c '#e' /env
bind '#p' /prog
bind '#d' /fd
cd /

The /dev/leds file can be read from and written to. When you read from it, you get the current state of the status LED on the development board.

stm32f405$ cat /dev/leds

You can also write a number to it to change its state: 0 for off, 1 for on. Like this:

stm32f405$ echo 1 > /dev/leds
stm32f405$ echo 0 > /dev/leds

With this kind of interface to the LED, we can write a Limbo program to make it blink on and off.


This is what the blink program looks like:

implement Blink;

include "draw.m";
include "sys.m";
sys: Sys;

Blink: module
    init:   fn(nil: ref Draw->Context, args: list of string);

init(nil: ref Draw->Context, args: list of string)
    sys = load Sys Sys->PATH;

    off := array of byte sys->sprint("%d\n", 0);
    on := array of byte sys->sprint("%d\n", 1);
    states := array[2] of { off, on };
    i := 0;

    for (;;) {
        i = 1 - i;

set_state(s: array of byte)
    fd := sys->open("/dev/leds", sys->OWRITE);
    if (fd != nil)
        sys->write(fd, s, len s);

In the first version of the program I changed the state of the LED in the loop in the main function. This had the unexpected behaviour of not blinking the LED at all. I soon realised that the device would only receive the data after the file was closed, and there's no explicit way to do that in Limbo. The result is the use of the set_state function to ensure that the file descriptor is garbage-collected.

The result of this is that the status LED should change state every second or so.

David Boddie
23 February 2023