Mike's blog about random software stuff

STM32 microblink

Disclaimer: The following text is written by someone who knows next to nothing about microcontrollers. The writer was also drinking beer. Please don’t use it as a source for anything other than entertainment purposes.

If you’ve ever fiddled around with microcontroller development boards, you’ve probably noticed that blinking an LED is pretty much the hello world of the microcontroller world. By no means am I an expert in bare-metal programming, but to me, it seems that the usual procedure for creating an MCU hello world is the following:

  1. Get a compiler and programmer suitable for your MCU.
  2. Download some huge library or even an IDE for the MCU.
  3. Navigate to the template code, and add a couple of lines of code there.
  4. Compile and flash by some simple command.

At this point, there hopefully is a blinking LED on your table. So now you can modify the source code just like any other program, e.g., change the blinking frequency or maybe blink some other LED on the board. Great, that wasn’t too bad!

But if you’re like me, you’ll quickly wonder why I needed this library of more than 100,000 lines of code just to blink an LED? Maybe I just take the source file with the main function and replace the library headers with my own code. It shouldn’t be too much work, right? I’ll just check the programmer manual for the address of the I/O port attached to the LED and write a one there. That should light up the LED. The MCU development board I was using was an STM32 Value line Discovery board. I managed to get its lights blinking using the miniblink example from the open-source ARM-cortex firmware library libopencm3.

Removing the library headers

After a bit of googling, reading the MCU manual, studying the library, and hacking with the miniblink example, I started removing everything that came from the header files. I first had to learn how to write to a specific memory address without using the library functions. Being someone who hasn’t programmed much outside of an operating system, that was something I never had to do before. After studying, I found out that the solution was using a pointer. You just cast the memory address to the pointer!

volatile uint32_t* const pointer_to_memory = (uint32_t*) (address_of_memory);

I don’t know how much it matters on this example program, but we also want to make the pointer volatile. At least for me, this is a C keyword that I haven’t needed to use that much. It means (as far as I know) that the value in the address can change at any time, and we let the compiler know that. The funny thing here is that we can make it const too. So the value can change at any time but is also constant? The const means here that the address stays constant, but the data in the address is volatile.

Now we can start reading the manuals and find the memory locations we wish to modify. From the Discovery board manual we find that a blue LED is connected to the General purpose I/O register C (GPIOC) port 8 of the MCU.

First, we’ll need to enable the clock in I/O port C. By reading the manual and the libopencm3 source code, we find out that this is done through the reset and clock control (RCC) APB2 peripheral clock enable register (RCC_APB2ENR). The memory map tells us that the RCC begins from address 0x40021000, and the APB2ENR is offset 0x18 bytes from that. The I/O port C clock is enabled by setting bit number 4 in the RCC_APB2ENR to one.

To use the GPIOC port 8 as a voltage source for the LED, we also need to set some bits to the correct locations of the GPIOC configuration register. The configuration register we need to use is the GPIOC_CRH register. By reading the MCU manual, we find that the GPIOC register begins from address 0x40011000, and the high control register is offset 0x04 bytes from that. The I/O port 8 is configured in the register bits [0-3]. To set it to output mode, we’ll need to write anything other than zero the bits [0-1], and to set the general purpose output push-pull mode, the bits [2-3] should be zero.

From this information, we can write the following code that enables us to control the LED blinking:

#include <stdint.h>

const uint32_t GPIO_C = 0x40011000;
const uint32_t GPIO_PUSH_PULL = 0x00;
volatile uint32_t *GPIOC_CRH = (uint32_t*) (GPIO_C + 0x04);

volatile uint32_t* const RCC_APB2ENR = (uint32_t*) (0x40021000 + 0x18);
const uint32_t RCC_APB2ENR_IOPCEN = (1 << 4);

int main(void)
{
	*RCC_APB2ENR = RCC_APB2ENR_IOPCEN;

    *GPIOC_CRH = GPIO_PUSH_PULL << 2;
    *GPIOC_CRH |= 0x02;

    ...

Now we can control the GPIOC I/O port output with the “Bit Set/Reset Register” (GPIOC_BSRR). Once again, from the MCU manual, we find that the GPIOC_BSRR address is offset 0x10 bytes from the GPIOC beginning. In that register, the bit number 8 is used to set the output bit on and bit number 24 to reset it. Using this information, we can write the following loop to blink the LED:

    ...
	volatile uint32_t* const GPIOC_BSRR = (uint32_t*) (GPIO_C + 0x10);

	while (1) {
		*GPIOC_BSRR = (1<<8);
		for (int i = 0; i < 500000; i++) {
		    __asm__("nop");
		}
		*GPIOC_BSRR = (1<<24);
		for (int i = 0; i < 500000; i++) {
		    __asm__("nop");
		}
	}
	return 0;
}

Now let’s compile this code and flash it to the MCU! So I’ll just write tbe following to my terminal:

arm-none-eabi-gcc main.c

Now I was expecting a nice binary file that we could flash to the MCU, but instead, we get:

/usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/bin/ld: main.o:(.rodata+0x0): multiple definition of `GPIO_C'; /tmp/ccizFeBX.o:(.rodata+0x10): first defined here
/usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/bin/ld: main.o:(.rodata+0x4): multiple definition of `GPIO_PUSH_PULL'; /tmp/ccizFeBX.o:(.rodata+0xc): first defined here
/usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/bin/ld: main.o:(.rodata+0x8): multiple definition of `GPIOC_CRH'; /tmp/ccizFeBX.o:(.rodata+0x8): first defined here
/usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/bin/ld: main.o:(.rodata+0xc): multiple definition of `RCC_APB2ENR'; /tmp/ccizFeBX.o:(.rodata+0x4): first defined here
/usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/bin/ld: main.o:(.rodata+0x10): multiple definition of `RCC_APB2ENR_IOPCEN'; /tmp/ccizFeBX.o:(.rodata+0x0): first defined here
/usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/bin/ld: main.o: in function `main':
main.c:(.text+0x0): multiple definition of `main'; /tmp/ccizFeBX.o:main.c:(.text+0x0): first defined here
/usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/bin/ld: /usr/lib/gcc/arm-none-eabi/12.1.0/../../../../arm-none-eabi/lib/libc.a(lib_a-exit.o): in function `exit':
/build/arm-none-eabi-newlib/src/build-newlib/arm-none-eabi/newlib/libc/stdlib/../../../../../newlib-4.2.0.20211231/newlib/libc/stdlib/exit.c:64: undefined reference to `_exit'
collect2: error: ld returned 1 exit status

Why would this fail? I’ve compiled programs a thousand times like that when programming on Linux, and it has worked. Now I realized that the library and its makefiles were doing a bit more than just providing easy-to-use functions for moving bits around.

The linker script

One important bit of creating an executable program is the linking process. When compiling something for an operating system, we rarely need to pay attention to the linker. But for a program that should run on an MCU without any operating system, things are different. Besides just linking the object files together, the linker also places our code sections in the places in memory they belong to.

So let’s create a linker script:

MEMORY
{
    flash(rx) : ORIGIN = 0x08000000, LENGTH = 128K
    SRAM(rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}

Here we’ll define where and what kind of memory our MCU has. Once again, we open the manual and navigate to the section about memory. From there, we find that our MCU has 128KB of flash memory starting from address 0x08000000 and 8KB of SRAM starting at address 0x20000000.

Another thing that we are missing is the startup code of the microcontroller. Because we don’t have the luxury of an operating system handling stuff in the background, we’ll need to handle some things at the startup. This part requires a bit of work both in the linker script and a C source file.

We’ll need to define a vector table that contains functions for different interrupts and events happening in the controller. If we wanted a truly functioning firmware library, we probably would want to create some handler for most interrupts and events defined for the MCU. But because that is not our goal, we can get away with just creating the reset handler. Also, our linker script will need sections for our executable code and some space for our variables. For this, let’s add the following to our linker script:

/* The vectors is defined in our vector source files */
EXTERN(vectors)
/*  I guess this is the name of the symbol where the code should start executing. */
ENTRY(reset_handler)

_stack = ORIGIN(SRAM) + LENGTH(SRAM);

SECTIONS
{
    /* The text section contains our executable code. */
    .text : {
        *(.vectors) /* First we should have the vector table */
        *(.text)    /* Executable code goes here */
        . = ALIGN(4);
        *(.rodata*) /* Global constants go here */
        . = ALIGN(4);
    } > flash AT> flash

    /* The data section contains variables we have initialized in our code */
    .data : {
        _data = .;
        *(.data)
        . = ALIGN(4);
        _edata = .; /* _edata just means end of data */
    } > SRAM AT> flash
	_data_loadaddr = LOADADDR(.data);

    /* And here's our uninitialized data */
    .bss : {
        _bss = .;
        *(.bss)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .; /* _ebss just means end of bss */
    } > SRAM

    . = ALIGN(4);
    end = .;
}

This linker script is the part that I understand the least. I think you should read something else if you want to know more about it.

The startup script

In the linker script, we mentioned the vector table. So, for our minimal MCU code, let’s define the following header file vector.h:

typedef void (*const handler_t)(void);

typedef struct
{
    uint32_t *stackPointer;
    handler_t reset;
} vectors_t;

extern uint32_t _data_loadaddr, _data, _edata, _ebss, _stack;
extern vectors_t vectors;

void reset_handler(void);

The vectors_t struct represents a datatype for our vector table. In our example, we only define the stack pointer and the reset handler to our vector table. We also define some external variables that appear in the linker script. In our vector.c file, we should define the reset function and initialize our vector table.

#include "vector.h"

int main(void);

void reset_handler(void)
{
	volatile uint32_t *src, *dest;

    /* Copy the data to SRAM */
	for (src = &_data_loadaddr, dest = &_data; dest < &_edata; src++, dest++) {
		*dest = *src;
	}
    /* Set bss to zeros */
	while (dest < &_ebss) {
		*dest++ = 0;
	}
	/* Continue to the main function. */
	(void)main();
}

__attribute__ ((section(".vectors")))
vectors_t vectors = {
    .stackPointer       = &_stack,
    .reset              = reset_handler,
};

I mostly copied the reset_handler from the libopencm3 library, removing all the bits I thought I didn’t need. One new thing to me in the startup code was those __attribute__ compiler directives (Or are they directives? Not sure what they are exactly). The attribute section tells our compiler that this variable must be placed in this section.

The startup code is a bit fuzzy to me. I assume the point is to initialize the data section with their initial values, set the data in bss to zero, and then call main. But the thing I don’t fully understand is how the reset/startup code knows to load the data from flash. Perhaps it has something to do with the "> SRAM AT> flash" line in our linker script. Maybe I’ll find out one day.

Creating the binary

Now we have all the source files that we need to blink a light. The last thing we need is a Makefile for easy compilation and flashing. My Makefile looks like this:

CC=arm-none-eabi-gcc
OBJCOPY= arm-none-eabi-objcopy
CFLAGS= -c -mthumb -mcpu=cortex-m3 -o0
LDFLAGS= -nostdlib -T linker.ld

all:main.o vector.o final.elf final.bin

main.o:main.c
	$(CC) $(CFLAGS) -o $@ $^

vector.o:vector.c
	$(CC) $(CFLAGS) -o $@ $^

final.elf:main.o startup.o vector.o
	$(CC) $(LDFLAGS) -o $@ $^

final.bin:final.elf
	$(OBJCOPY) -O binary $^ $@

clean:
	rm *.o *.elf *.bin

flash:
	st-flash write final.bin 0x08000000

One thing to note is the -nostdlib flag we are using. Because of our limited linker script, we can’t use the C standard library in this project. If creating the flashable binary isn’t clear to you, I hope you can figure it out by studying that Makefile.

Now, by running make and make flash I can finally blink the LED on my STM32 board with code that I can actually read through! Of course, you’d probably want to use that 100,000 line library for any real work, but I think having a very minimal example program is excellent for learning.

Link to the full source code:
https://github.com/salmmike/microblink/tree/master