In an unrelated discussion on the 9front mailing list, the following idea was floated:
I'[d] like to have a factotum usb drive, where the secrets never leave the usb device. It would talk 9p directly over the serial bus.
I thought this was a brilliant idea, so I shopped around for a microcontroller with a yubikey nano form factor. Eventually I found the Tomu family of devices; tiny computers which sit flush inside a USB type-A port. I chose the Somu, a security-oriented variant, not so much for any specific security features, but because it was more powerful and had more memory and storage than the original Tomu device.
The Somu is an STM32L432KC microcontroller with one ARM Cortex-M4 core, 64KiB of SRAM, and 256KiB of persistent flash memory. It comes with the solokeys solo1 firmware. I ordered the "hacker" variant from CrowdSupply, which allows the firmware to be reprogrammed; the consumer variant.
Setting up the device
The device arrived in the mail in a hilariously oversized box (seriously, it could have been sent in an envelope). It came with three silicone rubber holders, designed to press the device securely against the USB contacts. I suppose having multiple holders would allow you to color-code your keys if you have more than one.
The device is detected upon inserting it to my laptop:
[90179.747756] usb 1-1: new full-speed USB device number 43 using xhci_hcd
[90179.885514] usb 1-1: New USB device found, idVendor=0483, idProduct=a2ca, bcdDevice= 1.00
[90179.885535] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[90179.885545] usb 1-1: Product: Solo DiceKeys 5.0.0
[90179.885552] usb 1-1: Manufacturer: SoloKeys
[90179.885559] usb 1-1: SerialNumber: REDACTED
[90179.890275] hid-generic 0003:0483:A2CA.000F: hiddev3,hidraw5: USB HID v1.11 Device [SoloKeys Solo DiceKeys 5.0.0] on usb-0000:00:14.0-1/input0
As an exercise & sanity check, I can try to load a debug build of the latest firmware onto the device.
Debug builds initialize a USB serial interface where log statements are emitted.
For the time being I will build the existing firmware with GCC.
I use Guix, which has several toolchains defined in the
(gnu packages embedded) module, all bootstraped from source, so I can write a toolchain.scm file:
(use-modules
(gnu packages embedded))
(make-arm-none-eabi-nano-toolchain-12.3.rel1)
And execute guix shell -f toolchain.scm to get a shell with the appropriate compiler and linker available.
The firmware has other dependencies, however; one of the dependencies is written in Rust, so we need an entire Rust toolchain as well.
There is also a companion tool, solo, that is used to flash the device.
After adding an external channel for building rust toolchains, I can collect all the dependencies in a slightly larger file called a manifest.
Nailing down all the dependencies took some trial and error:
(use-modules
(rustup build toolchain)
(gnu packages embedded))
(concatenate-manifests
(list
(specifications->manifest
(list "libsodium"))
(packages->manifest
(list (rustup "stable" #:targets (list 'thumbv7em-none-eabihf))
(make-arm-none-eabi-nano-toolchain-12.3.rel1)))))
Once the toolchain is built (which takes quite a long time), I can build the firmware according to the instructions in the README:
$ guix shell -m toolchain.scm
[env]$ cd targets/stm32l432
[env]$ make cbor
[env]$ make build-hacker
[env]$ make firmware-debug-1
Only, well, I couldn't. When compiling the crypto/salty library, which is written in Rust, I got the error:
error: linker `cc` not found
I tried defining the CC variable, then the CC_thumbv7em_none_eabihf variable according to the documentation, but nothing seemed to work.
I ended up defining a symlink in my PATH:
[env]$ ln -s $(which arm-none-eabi-gcc) ~/bin/cc
Then I got a new error:
= note: cc: error: unrecognized command-line option '-m64'
cargo was passing an amd64-specific flag, -m64. I travelled down the rabbit hole for quite some time, squashing one error only to find another.
Eventually, I gave up and installed rustup, downloaded the cross-compilers distributed by ARM, and ran the build from a container with guix shell -CFN.
I had to pin the fido2 dependency of the solo command-line tool to get it to work properly as reported in this issue.
Eventually, I was able to flash a different version of the same firmware onto the device, but that was just an exercise. My whole plan from the start was to write my own firmware from scratch, using Plan 9's C compilers! Muahaha!
Writing new firmware from scratch
Soured from the experience of working with pre-existing firmware, I decided to try building something from scratch. I was very quickly faced with a petrifying degree of freedom. This is my first embedded project since college, and my programming career up until this point has been dominated by constraints; there was always some API, or an operating system to obey, an existing code base with style guidelines and naming conventions to conform to.
Embedded is so different. There's no OS unless you choose to use one.
The standard library? optional.
You want to define your own read function?
That name's available.
Do you hate null-terminated strings?
You don't have to use them.
The only limitations you have to deal with are the capabilities of your target device.
Small steps
I felt paralyzed for awhile. I decided to break up the problem into smaller steps that feel less scary and more achievable:
- Build a program without any OS libraries
- Write and run a program to blink the LED
- Make the LED demo progressively fancier, utilizing clocks, interrupts, and other hardware features I'll need eventually.
- Start porting bits and pieces of libc to the embedded context
- libthread for cooperative multitasking
- USB serial device for printf-style debugging
- ??
- Factotum!
Building a program without libc
I'm writing this program on 9front. It has a suite of C compilers that support over half a dozen architectures, including the Thumb-1 instruction set, also known as T16, supported by this Cortex M4 device.
The Plan 9 C compilers are well-documented; their code is decently readable and well-organized into portable and machine-specific portions, and the following papers describe their architecture and usage well, including their usage with mk(1):
- Plan 9 C Compilers
- How to Use the Plan 9 C Compiler
- A Manual for the Plan 9 assembler
- Plan 9 Mkfiles
Also useful is a.out.h(6) for a description of the standard header for executable files.
Plan 9 does not use the standard C library; instead it has its own libc which feels familiar but generally more nicely designed. However, libc assumes the existence of several operating system concepts that simply do not exist on a bare-metal system. For example:
- There are no files, no file descriptors, and no file systems.
- There is no
fork(), no processes, and no users. - There is no MMU, just one global address space.
Let's start with a simple program, foo.c that does nothing:
#include <u.h>
#include <libc.h>
void main(int, char **) {}
I can compile this program like so:
% tc foo.c
% tl foo.t
It produces the file t.out, which pulls in a few functions from libc:
% nm t.out
1038 T _callmain
1148 T _div
11e0 T _divu
10ac L _exits
1024 L _main
11a4 T _mod
11f8 T _modu
...
If we try removing the includes, which instruct the linker on what libraries to pull in via some #pragma lines, we get an error during link time:
% tl foo.t
??none??: entry not text: _main
??none??: _main: not defined
The symbol _main is documented in the compiler manual; it is the entry point of the generated executable file, akin to _start in Unix, and is defined in /sys/src/libc/$objtype/main9.s. This is the definition of _main for ARM, for example:
TEXT _main(SB), 1, $0
SUB $8, R(sp)
MOVW $setR12(SB), R(sb)
MOVW R(arg), _tos(SB)
MOVW $main(SB), R(arg)
MOVW $0, R(lr)
B _callmain(SB)
BL _div(SB) /* force loading of div */
We can define our own _main, in an assembly file, conventionally named l.s and called a "loader":
TEXT _main(SB), $-4
MOVW $setR12(SB), R12
B ,reset(SB)
I've replaced main with reset in my C file, a function of no arguments.
Adapting the mkfile from there for my own use, I can try building again,
this time adapting the mkfile from the GBA emulator:
% mk
tc -FTVw foo.c
5a -t l.s
tl -R0x4 l.t foo.t
The -R4 flag pads the text segment to the next multiple of 4, which will be needed on the target device, where some types of access will fail unless they are word-aligned.
We've made the compiler happy, but we're producing binaries meant to run on a Plan 9 system; our target hardware expects binaries to be laid out in a specific way.
Startup sequence
Since this is all new to me, the biggest hurdle to overcome is simply figuring out how to structure my binary, load it, and get feedback. For that I can reference the Programming manual[1], ST's application notes on boot modes[2], and the datasheet[3] for this device. The data sheet provides a diagram showing how the 32 bits of address space are used on this chip:

The diagram shows that flash memory addresses start at address
0x08000000 and end at address 0x08040000-1 (256KiB). From the
Programming Manual, we can learn that on reset, the system will load
into the stack pointer (SP) register from address 0, and into the
program counter (PC) register from address 4, and that this is part
of a larger, 1KiB structure called the "Vector table" which encodes
the location of interrupt service routines (ISRs).
Addresses 0x0000-0x4000 are mapped to flash when booting from flash,
which is the default for my configuration. So, on a normal boot (e.g.
when we're not updating the firmware), the CPU will use the first two
words of our binary to jump into the program.
This is covered with in a
FAQ
on the ST community site.
9front's Thumb loader, tl, accepts a -T argument which is the
address of the start of the code (text) segment. While I could rely
on the aliasing that takes place when booting from flash, and assume
code starts at 0, I think I would rather reference the "true"
addresses of my instructions, so I can add the loader flag:
LDFLAGS += -T0x08000000
The -T flag was already present in the mkfile I used as a template, but I wanted to understand why it was there before I used it.
Laying out the vector table
The binary I download to the stm32 must start with a 1KiB Vector Table, an array of addresses to handlers for various hardware events. In the rest of these posts, I will interchangeably refer to these bits of code interrupt service routines, ISRs, or exception handlers.
The GNU C Compiler toolchain provides "linker scripts", an entire language that allows you to control where and how the linker arranges different objects (functions, data, symbols, etc) in the output binary. It is powerful, but also yet another language to learn.
The Plan 9 loaders do something simpler: they lay out code in the order they appear on the command line, and, with some exceptions, in the order they appear in each file.
If I pass my loader file as the first argument to the linker, the first item in the output file should be placed at the beginning of the code section.
The WORD, LONG, and BYTE pseudo-operations allow us to place arbitrary
values at the current offset in the file. So we can lay out the vector table like so:
TEXT vtable(SB), 1, $-4
WORD $0x10004000 /* SP */
WORD $_main(SB) /* PC */
/* TODO */
TEXT _main(SB), $-4
MOVW $setR12(SB), R12
B ,reset(SB)
The TEXT pseudo-instruction will not appear in the code; it defines a symbol in the symbol table, which can be used as a target for branch instructions.
For the stack pointer, my target device has two memory banks:
- SRAM1:
0x20000000 - 0x2000C000(48 KiB) - SRAM2:
0x10000000 - 0x10004000(16 KiB)
The banks are accessed on different buses, and SRAM2 persists in some low-power modes that SRAM1 does not. SRAM2 is also aliased to the space after SRAM1, so they could be used as one contiguous memory region. For now, I am only going to use SRAM2.
Passing the -a flag to the linker will print the output in a human-
readable format:
% tc foo.c
% 5a -t l.s
% tl -a -R 0x4 -T 0x08000000 l.t foo.t
08000000: (11) TEXT vtable+0(SB),R1,$-4
08000000: 10004000 WORD 268451840
08000004: 08000008 WORD 0
08000008: (15) TEXT _main+0(SB),$-4
08000008: e51fc004 (17) MOVW $setR12+0(SB),R12
0800000c: 08001014 WORD 0
08000010: (98) TEXT reset+0(SB),R4,$-4
08000010: 000046f7 (100) MOVW R14,R15
We got the first two words where we want them. I'm not sure what to make of the instruction at address offset 0x0800000c; it should have been B ,reset(SB).
I'm going to ignore that for now, and define the rest of the vector table. For now, I will define an exception handler that just loops forever, as a placeholder:
TEXT interrupt(SB), $-4
B ,interrupt(SB)
And my vector table can be:
TEXT vtable(SB), 1, $-4
WORD $0x10004000 /* SP */
WORD $_main(SB) /* PC */
WORD $interrupt(SB) /* NMI */
WORD $interrupt(SB) /* Hard fault */
...
but the manual also states that these addresses must have the least significant bit (LSB) set to indicate that the instructions at the address are using the 16-bit Thumb encoding:
...
WORD $_main+1(SB) /* PC */
WORD $interrupt+1(SB) /* NMI */
WORD $interrupt+1(SB) /* Hard fault */
...
You may think it's a chore to type out all 256 entries of the vector table, but it's nothing a little shell script can't fix!
seq -14 239 | sed 's/.*/ WORD $interrupt+1(SB) \/* IRQ & *\//'
The range [-14, 239] was taken from the manual; interrupts <0 are system exceptions, and interrupts >= 0 are interrupts from general-purpose IRQ channels used by peripherals and DMAs and such.
Blinking the LED
The only visual piece of feedback the device has is an RGB LED. My
first milestone will be to write and load a firmware that simply
blinks the LED. I assume it is connected to one of the many
general-purpose IO (GPIO) pins that this board has, but I do not know
which, so I will have to go through the source of the firmware this device shipped with. I
can find targets/stm32l432/src/led.h:
#define LED_PIN_G LL_GPIO_PIN_0
#define LED_PIN_B LL_GPIO_PIN_1
#define LED_PIN_R LL_GPIO_PIN_2
#define LED_PORT GPIOA
and in src/cmsis/stm32l432xx.h:
#define GPIOA_BASE (AHB2PERIPH_BASE + 0x0000UL)
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
together with the datasheet, I can tell that the LED is connected to
the first 3 pins of GPIOA which is on the "Advanced High-performance
Bus" (AHB) 2, with register addresses in the range [0x48000000, 0x48000400).
The firmware was evidently generated by STM32CubeIDE, and is 70K lines of code and macros, designed to be as general as possible across ST's different 32-bit ARM boards. After awhile, I lost patience and instead found this article showing a simple blinking LED program for a similar device, so I decided to follow along.
The article is a nice read; I recommend it. To keep things a little
more tidy, I'm going to describe blocks of related registers with
structs. For example, the Reset and Clock Control register (RCC)
can look like this:
typedef struct
{
u32int cr;
/* ... */
u32int apb1enr;
u32int apb2enr;
/* ... */
u32int ccipr2;
} RCCReg;
#define RCC ((RCCReg*)0x40021000)
To generate the structure, I used pdftotext to dump section 6.4 of
the Reference Manual[4], then used sam to convert the text into a struct definition
with a series of regular expressions.
Plan 9's C compilers do not have static_assert, but I can try to
compile a program
with a series of expressions like this:
static char rccreg_ahb1rstr[offsetof(RCCreg, ahb1rstr) == 0x28 ? 1 : -1];
If the position of the register in the struct doesn't match the documented offset, such a program will fail to compile. I initially skipped doing this, and learned the hard way how frustrating it can be to spend a long time debugging your program only to find you were writing to the wrong register.
Here's my first attempt at blinking the LED:
#define GPIOA = ((GPIOReg*)0x48000000)
enum {
Modein = 0b00,
Modeout = 0b01,
Modealt = 0b11,
Modeanalog = 0b10,
};
void
reset(void)
{
enum {
RGB = 0b111111,
};
int i, color;
RCC->ahb2enr |= 1; /* Enable GPIOA */
/* we have to wait 2 clock cycles for effect */
RCC->ahb2enr;
RCC->ahb2enr;
GPIOA->mode = (GPIOA->mode & ~RGB)
| Modeout << 0 /* G */
| Modeout << 2 /* B */
| Modeout << 4; /* R */
color = 0;
while(1){
color++;
GPIOA->od = (color & RGB);
/* 80 Mhz = 80 million ticks per second */
for(i = 0; i < 80000000; i++);
}
}
Unlike the example from the article, my hardware has an RGB LED, with green, blue, and red wired to the first 3 GPIO pins. My program takes advantage of that by cycling the LED between every color combination instead of flashing on and off. Like the example in the article, I'm using a busy loop to "sleep"; in "real" code I would be writing to the LED pins from a timer, which would allow me to control the perceived brightness.
There is a lot wrong with this program, but I'm really itching to get something running.
The scary part; programming the device
I am at the point where I have some code that I think has a chance of running on this device. Right now it's programmed with a bootloader from SoloKeys. This bootloader accepts custom commands over USB to enter "bootloader mode", which can receive new firmware using solokeys' bootloader protocol/format.
From bootloader mode, it can also modify itself to use the bootloader present on the ROM portion of this device, which solokeys refers to as "DFU" mode. It allows the device flash to be modified using the Device Firmware Upgrade (DFU) protocol, a standard for updating device firmware over USB. In this mode, you can fully overwrite the flash with a bootloader of your choice.
What I am afraid of is deleting the bootloader, uploading a buggy program which doesn't work, and having no way to re-enter DFU mode to download new firmware. The manual[2] provides a set of patterns for entering boot mode, but these involve raising the voltage on different pins, or writing to different registers on the device while it's running. There are four pads on the underside of the somu:

but I could not find a pinout of the Somu which shows me which one is wired to the boot0 pin, if any, or the SWD pins. The device will be "soft bricked"; it's not permanently broken, but it would be a pain to unbrick it, and I do not have the equipment for it;
So I ordered a dev board. This is the same chip as the one found on the somu, but the USB port hooks up to an on-board debugger. Using tools from the stlink project I can attach a debugger, step through my program, inspect the state of the processor interactively, set registers, and wipe the flash completely without fear of rendering the device unbootable. For now, I will do my development using this board instead.
It comes with a USB connection to the on-board debugger, which I plugged into my Linux workstation. My system uses (e)logind, so I added the following udev rule to grant permission to use it to the user sitting at the keyboard:
SUBSYSTEM=="usb", ATTRS{product}=="STM32 STLink", TAG+="uaccess"
after which I can see the device using st-info:
$ st-info --probe
2026-02-04T16:17:19 WARN usb.c: skipping ST device : 0x483:0x5129)
Found 1 stlink programmers
version: V2J31S21
serial: REDACTED
flash: 262144 (pagesize: 2048)
sram: 49152
chipid: 0x435
dev-type: STM32L41x_L42x
Using the -H0 flag for tl(1) will tell it to generate the output binary
without a header, so it will start with the vector table that I laid out in the
l.s file:
tc blink.c
5a -t l.s
tl -a -R 0x4 -T0x08000000 -H0 -o blink.bin l.t blink.t
Then, I can use the st-flash tool from my Linux workstation to write
this file into the device flash. First I'll just do a sanity check; can I write
the firmware and read the same bytes back out?
$ st-flash write somu.bin 0x08000000
file somu.bin md5 checksum: 2a45ab81db52ddc83fc9ad0cc371251, stlink checksum: 0x00007a26
2026-02-04T18:50:02 INFO common_flash.c: Attempting to write 1112 (0x458) bytes to stm32 address: 134217728 (0x8000000)
...
2026-02-04T18:50:02 INFO common_flash.c: Flash written and verified! jolly good!
$ st-flash read flash.out 0x08000000 $(wc -c <somu.bin)
st-flash 1.8.0
2026-02-04T18:51:52 INFO common.c: STM32L41x_L42x: 48 KiB SRAM, 256 KiB flash in at least 2 KiB pages.
2026-02-04T18:51:52 INFO common.c: read from address 0x08000000 size 1112
$ md5sum somu.bin flash.out
2a45ab81db52ddc803fc9ad0cc371251 somu.bin
2a45ab81db52ddc803fc9ad0cc371251 flash.out
That seems to work. Now I can try resetting:
$ st-flash reset
st-flash 1.8.0
2026-02-04T18:57:21 INFO common.c: STM32L41x_L42x: 48 KiB SRAM, 256 KiB flash in at least 2 KiB pages.
The debug LED winked, but nothing else happened. If my firmware were running, the LED would blink on and off forever. I can run the gdbserver in one window:
$ st-util
st-util 1.8.0
2026-02-04T18:58:22 INFO common.c: STM32L41x_L42x: 48 KiB SRAM, 256 KiB flash in at least 2 KiB pages.
2026-02-04T18:58:22 INFO gdb-server.c: Listening at *:4242...
And gdb in another:
$ arm-none-eabi-gdb
(gdb) target extended-remote localhost:4242
Remote debugging using localhost:4242
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0x08000454 in ?? ()
(gdb) info registers pc sp lr
pc 0x8000454 0x8000454
sp 0x10003fe0 0x10003fe0
lr 0xfffffff9 -7
(gdb) print 0x10004000 - 0x10003fe0
$3 = 32
I won't be able to see symbols in gdb output as it doesn't understand
plan9's a.out format, and the -H0 flag doesn't include headers or a symbol table anyway.
But 0x08000454 is the address of my interrupt routine, and there's
32 bytes of data on the stack. Section 2.3.7 of the programming
manual[1] describes how exceptions are handled and the calling
convention for the exception handlers. The following 32 byte (8
4-byte words) structure is pushed onto the stack:

The CPU will then lookup the exception handler from the vector table, load a special return address into the Link Register (LR), and jump to the exception handler. The exception is considered serviced when the CPU jumps to the address stored in LR, and the CPU resumes execution from where it left off before it was preempted.
I see the start of the vector table sets the top of stack I specified
and the address of _main, with the lsb set to indicate that it's a
Thumb function:
(gdb) x/2zw 0x08000000
0x8000000: 0x10004000 0x08000401
And the start of the main function at 0x0800408:
(gdb) x/4i 0x08000408
0x8000408: sub sp, #12
0x800040a: ldr r5, [pc, #60] @ (0x8000448)
0x800040c: movs r4, #9
0x800040e: lsls r4, r4, #27
It's a bit challenging to match these statements with the output of tl -a,
because tl uses ATT-style src, dst operand order, and because tl -a
is printing the pseudo-assembly, rather than the specific instruction;
depending on the operands, MOVW becomes movs, ldr, str, or
something else.
Referencing the diagram above, the old value of PC before the interrupt
should be stored in memory, 7 words up from SP:
(gdb) x/8zw 0x10003fe0
0x10003fe0: 0x00000004 0x00000000 0x00000000 0x00000000
0x10003ff0: 0x00000000 0xffffffff 0x07fffe44 0x01000000
The old PC is not in the address range for flash. What happened? Let's start
over and watch. I can set a breakpoint at _main:
(gdb) break *0x08000400
Breakpoint 1 at 0x8000400
Note: automatically using hardware breakpoints for read-only addresses.
Then I can press the button on the board to reset the device:
Breakpoint 1, 0x08000400 in ?? ()
(gdb) info registers pc sp lr
pc 0x8000400 0x8000400
sp 0x10004000 0x10004000
lr 0xffffffff -1
(gdb) display/i $pc
1: x/i $pc
=> 0x8000400: stmia r0!, {r2}
(gdb) si
1: x/i $pc
=> 0x8000402: b.n 0x7fffe44
(gdb) si
0x07fffe44 in ?? ()
1: x/i $pc
(gdb) si
0x08000454 in ?? ()
1: x/i $pc
=> 0x8000454: @ <UNDEFINED> instruction: 0xfffeeaff
There's the problem; we are jumping to an invalid address!
=> 0x8000402: b.n 0x7fffe44
If we look at the output of tl -a for this part of the program, it's in the
loader, _main:
08000400: (267) TEXT _main+0(SB),$-4
08000400: e51fc004 (269) MOVW $setR12+0(SB),R12
08000404: 08001ffc WORD 0 <--- here
There is something wrong; Thumb instructions are 2 bytes wide but the
loader is showing us two 4-byte instructions. Here is the line from
the original l.s assembly:
B ,main(SB)
It doesn't seem to be generating the instruction I want. In this
case, the address of main is 0x08000408. Let's take a look at the
loader code. Since I'm not yet familiar with it I decided to step
through it in the acid debugger. Luckily, the problematic instruction
occurs early in the binary, so I didn't have to do anything fancy.
The asmb function in /sys/src/cmd/tl/asm.c loops through the
object file and calls one of two asmout functions:
pc = INITTEXT;
for(p = firstp; p != P; p = p->link) {
/* ... */
if(thumb)
thumbasmout(p, o);
else
asmout(p, o);
pc += o->size;
}
and I saw that, at the start of the loading, asmout was called
instead of thumbasmout. The tc compiler and tl loader were, I
think, initially written for the ARM7TDMI chips, or something close to
them. These chips have the ability to switch back and forth between
the ARM and Thumb instruction set. So the tl loader starts emitting
ARM instructions, and tracks whether or not it should be in ARM or
Thumb mode based on the last instruction that was emitted.
However, the Cortex-M* line of chips are Thumb-only processors. They
won't be able to execute any ARM instructions. I have to specify the
ALLTHUMBS flag as an argument to the TEXT pseudo-instruction in
the entry point of my loader:
#define ALLTHUMBS 4
/* ... */
TEXT _main(SB), ALLTHUMBS, $-4
Once I do this, I no longer have to set the LSB manually for the function addresses in the vector table. Then I am met with the error during compilation:
_main: illegal combination MOVW 11 0 43
(269) MOVW $setR12+0(SB),R12
What does this mean? $setR12 is just a big number that's kept in a
high register (R12 on arm, R30 on mips) that's called the "static
base" (SB) register. For many instruction sets, but especially Thumb,
there are only so many bits available for address operands, certainly
not enough to address the full 32-bit address space of this microcontroller.
For values in the text segment, loads can be made relative to the program
counter. However, if the data segment is far away from the text segment,
those values will be out of reach, and require more instructions to load.
So loads from the data segment are made relative to SB, at the linker's
discretion.
The Plan 9 linkers are largely data-driven. There is a table for each instruction set, with the operation, the type of each operand, and flags for the linker to decide what to do. Here are some examples:
Optab thumboptab[] = {
/* ... */
/* inst src op2 dst,
{ AMOVW, C_SCON, C_NONE, C_REG, 5, 2, 0 },
{ AMOVW, C_BCON, C_NONE, C_REG, 47, 4, 0 },
{ AMOVW, C_LCON, C_NONE, C_REG, 38, 2, 0, LFROM },
{ AMOVW, C_REG, C_NONE, C_HREG, 8, 2, 0 },
{ AMOVW, C_HREG, C_NONE, C_REG, 8, 2, 0 },
{ AMOVW, C_HREG, C_NONE, C_HREG, 8, 2, 0 },
/* ... */
Missing from the list is an entry for loading a 4-byte constant
(C_LCON) into a High Register (C_HREG), where a high register is
R8 or above. This is because the Thumb-1 load instructions only
allow for 3 bits of register space.
So, to load a constant into a high register, we have to bounce it through a low register first:
TEXT _main(SB), $-4
MOVW $setR12(SB), R1
MOVW R1,R12
B ,main(SB)
The linker cannot do this for us automatically because it does not
know what registers are available; register allocation is done by the compiler.
After rebuilding and loading the program, it seems to be running,
with the PC register staying within this loop:
while(1){
color++;
GPIOA->od = (color & RGB);
/* 80 Mhz = 80 million ticks per second */
for(i = 0; i < 80000000; i++);
}
But the LEDs are wired up differently on this board. There are 3 in total, with LEDS 1 and 2 being used as indicators for debugger activity. I can update the program to wink LED 3, which is unused, and one color only (green). Per the manual[5]:
User LD3: the green LED is a user LED connected to ARDUINO ® Nano signal D13 corresponding to the STM32 I/O PB3 (pin 26)
After a bit of digging, this maps to the pin 3 (counting from 0) of GPIOB. And it's only one color (green).
void
main(void)
{
int i, r, m;
RCC->ahb2enr |= 0b10;
RCC->ahb2enr;
RCC->ahb2enr;
GPIOB->mode &= ~(0b11 << 6);
GPIOB->mode |= Mout << 6;
m = 0x00080008;
r = 0x00000008;
while(1){
GPIOB->bsr = r;
r ^= m;
for(i = 0; i < 8000000; i++);
}
}
Because the pin was already in use for a different function, I had to
clear the mode register. I also switched from using the read-write
Output Data register (od) to the write-only Bit
Set/Reset register. This register allows you to set (low 16
bits) or unset (high 16 bits) a set of pins without having to first
read the state of other pins, which can be racy on a system with
interrupts like this one.
We. Have. Blinking!
Here is the state of the project at this point. I'll pause here to take stock of where I am and where I'm going.
- I've got a debug board I can run code on that I've compiled with the Plan 9 toolchain.
- I am able to monitor its execution with a debugger.
Most of all, I've learned a lot about the target microcontroller, and the Plan 9 compiler suite. I've already created a couple patches for the Thumb linker, and I feel a (perhaps misplaced) sense of confidence that if something is wrong with the toolchain, I'll be able to fix it. I've also become more comfortable thinking about embedded platforms. Microcontrollers seem less mystical and more like the pile of registers, memory maps, peripherals, and documentation that they are. I'm finding a lot of this experience is about finding and reading reference documentation.
The goal of the next post will be to improve the debugging experience so I am not stuck reading assembly.
- See also
-
A factotum in your USB port, part 2
Aug 2026
a better debugging story