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 somu with holders

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:

  1. Build a program without any OS libraries
  2. Write and run a program to blink the LED
  3. Make the LED demo progressively fancier, utilizing clocks, interrupts, and other hardware features I'll need eventually.
  4. Start porting bits and pieces of libc to the embedded context
  5. libthread for cooperative multitasking
  6. USB serial device for printf-style debugging
  7. ??
  8. 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):

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:

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:

Memory mapping of stm32l432kc

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:

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:

underside of the somu showing four contact pads

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:

M4 exception handler stack entry

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.

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.

  1. STM32 Cortex M4 Programming manual

    ↩︎︎1↩︎︎2
  2. Introduction to system memory boot mode on STM32 MCUs

    ↩︎︎1↩︎︎2
  3. Ultra-low-power Arm ® Cortex ® -M4 32-bit MCU+FPU, 100DMIPS, up to 256KB Flash, 64KB SRAM, USB FS, analog, audio

    ↩︎︎
  4. STM32L41xxx/42xxx/43xxx/44xxx/45xxx/46xxx advanced Arm® -based 32-bit MCUs

    ↩︎︎
  5. STM32 Nucleo-32 boards (MB1180)

    ↩︎︎