Introduction

As you might have noticed, Rust is already quite popular in the embedded space. And getting started with embedded Rust, even on a bare-metal level, isn’t all that difficult. However, I’m not a fan of the standard method as it has a bit too much magic for me. When it comes to embedded, I like to know exactly what is going on and that includes knowing the very first code which is run and “kicks off” the main code. As such, I have explored how to do this for the STM32F4-Discovery using C code which then fires up a Rust main function. In this blog post, I will give an overview of the process that I followed.

The reason I have decided to write the “bring up code” in C rather than in Rust is simply because this code is highly unsafe. Therefore, Rust would have very little advantage in this case. Also, when you’re writing code like that, it tends actually to be less readable in Rust. The other reason is that typically when you try porting to some new platform, the example code you get will be in C, so it might just be easier to build on top of that rather than to explain to some nice Rust embedded framework what exactly is going on.

Anyway, let’s get on with the post. Unfortunately, this will not be a carefully considered educational piece which describes exactly every step I went through in detail. Instead I will present all the files I created and try to give an overview of the most interesting parts.

Linker script

ENTRY(Reset_Handler)

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

__max_heap_size = 0x400;
__max_stack_size = 0x200;

SECTIONS
{
    .text :
    {
        *(.isr_vector)
        *(.text)
        *(.rodata)
        . = ALIGN(4);
        _etext = .;
    }> FLASH AT> FLASH

    .data :
    {
        _sdata = .;
        *(.data)
        . = ALIGN(4);
        _edata = .;
    }> SRAM AT> FLASH

    .bss :
    {
        _sbss = .;
        *(.bss)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;
    }> SRAM AT> SRAM
}

The goal of the linker, in very simple terms, is to squash all of the object files that make up an application into one big binary blob which can then be executed by the processor. Just in case you are not aware, object files are what get created by the compiler from source files. These files contain the compiler generated machine code in a format where all addresses are still relocatable. Only after all of the object files have been linked do addresses become absolute. And the way that you tell a linker what it should do, in other words, where it should place everything, you need to give it a linker script.

This is very important in an embedded context since you are not working with virtual memory, you are instead working with physical memory and embedded devices typically require that different parts of your application binary live in different memory regions with very specific addresses. For our STM32, you can extract this information from Figure 18. in the chip’s data sheet.

The linker script that I have created here is fairly simple. It lists the two memory locations that the microcontroller has, namely SRAM and flash; and then defines the code necessary to squash the text, data and bss sections of all object files together into a single ELF file. The most important thing to note here is that is that .isr_vector is placed at the very top of FLASH. This means that the interrupt vector table, that we will declare in a C file soon, will be placed exactly where the processor expects to find it.

Another interesting thing to note is that the text section is defined differently from the other two sections. This is because the text section is static and will only ever exist in flash, whereas the other two sections actually need to be copied from flash into RAM when the program starts up. This copying will be handled by C code that we will write. Just in case you didn’t know, the reason that that the text section is static is because it contains the compiled code of our application and not any variables.

Startup file

#include <stdint.h>

#define SRAM_START 0x20000000U
#define SRAM_SIZE (128 * 1024)
#define SRAM_END ((SRAM_START) + (SRAM_SIZE))

#define STACK_START SRAM_END

// Defined in linker script
extern uint32_t _etext;
extern uint32_t _sdata;
extern uint32_t _edata;
extern uint32_t _sbss;
extern uint32_t _ebss;

// Forward declaration, will come from Rust
void main(void);

void Reset_Handler(void);
void NMI_Handler(void) __attribute__((weak, alias("Default_handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_handler")));
void MemManage_Handler(void) __attribute__((weak, alias("Default_handler")));
void BusFault_Handler(void) __attribute__((weak, alias("Default_handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_handler")));
void SVCall_Handler(void) __attribute__((weak, alias("Default_handler")));
void Debug_Monitor_Handler(void) __attribute__((weak, alias("Default_handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_handler")));
void WWDG_Handler(void) __attribute__((weak, alias("Default_handler")));
void PVD_Handler(void) __attribute__((weak, alias("Default_handler")));
void TAMP_STAMP_Handler(void) __attribute__((weak, alias("Default_handler")));
void RTC_WKUP_Handler(void) __attribute__((weak, alias("Default_handler")));
void FLASH_Handler(void) __attribute__((weak, alias("Default_handler")));
void RCC_Handler(void) __attribute__((weak, alias("Default_handler")));
void EXTI0_Handler(void) __attribute__((weak, alias("Default_handler")));
void EXTI1_Handler(void) __attribute__((weak, alias("Default_handler")));
void EXTI2_Handler(void) __attribute__((weak, alias("Default_handler")));
void EXTI3_Handler(void) __attribute__((weak, alias("Default_handler")));
void EXTI4_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream0_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream1_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream2_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream3_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream4_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream5_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream6_Handler(void) __attribute__((weak, alias("Default_handler")));
void ADC_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN1_TX_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN1_RX0_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN1_RX1_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN1_SCE_Handler(void) __attribute__((weak, alias("Default_handler")));
void EXTI9_5_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM1_BRK_TIM9_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM1_UP_TIM10_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM1_TRG_COM_TIM11_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM1_CC_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM2_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM3_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM4_Handler(void) __attribute__((weak, alias("Default_handler")));
void I2C1_EV_Handler(void) __attribute__((weak, alias("Default_handler")));
void I2C1_ER_Handler(void) __attribute__((weak, alias("Default_handler")));
void I2C2_EV_Handler(void) __attribute__((weak, alias("Default_handler")));
void I2C2_ER_Handler(void) __attribute__((weak, alias("Default_handler")));
void SPI1_Handler(void) __attribute__((weak, alias("Default_handler")));
void SPI2_Handler(void) __attribute__((weak, alias("Default_handler")));
void USART1_Handler(void) __attribute__((weak, alias("Default_handler")));
void USART2_Handler(void) __attribute__((weak, alias("Default_handler")));
void USART3_Handler(void) __attribute__((weak, alias("Default_handler")));
void EXTI15_10_Handler(void) __attribute__((weak, alias("Default_handler")));
void RTC_Alarm_Handler(void) __attribute__((weak, alias("Default_handler")));
void OTG_FS_WKUP_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM8_BRK_TIM12_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM8_UP_TIM13_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM8_TRG_COM_TIM14_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM8_CC_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA1_Stream7_Handler(void) __attribute__((weak, alias("Default_handler")));
void FSMC_Handler(void) __attribute__((weak, alias("Default_handler")));
void SDIO_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM5_Handler(void) __attribute__((weak, alias("Default_handler")));
void SPI3_Handler(void) __attribute__((weak, alias("Default_handler")));
void UART4_Handler(void) __attribute__((weak, alias("Default_handler")));
void UART5_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM6_DAC_Handler(void) __attribute__((weak, alias("Default_handler")));
void TIM7_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream0_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream1_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream2_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream3_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream4_Handler(void) __attribute__((weak, alias("Default_handler")));
void ETH_Handler(void) __attribute__((weak, alias("Default_handler")));
void ETH_WKUP_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN2_TX_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN2_RX0_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN2_RX1_Handler(void) __attribute__((weak, alias("Default_handler")));
void CAN2_SCE_Handler(void) __attribute__((weak, alias("Default_handler")));
void OTG_FS_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream5_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream6_Handler(void) __attribute__((weak, alias("Default_handler")));
void DMA2_Stream7_Handler(void) __attribute__((weak, alias("Default_handler")));
void USART6_Handler(void) __attribute__((weak, alias("Default_handler")));
void I2C3_EV_Handler(void) __attribute__((weak, alias("Default_handler")));
void I2C3_ER_Handler(void) __attribute__((weak, alias("Default_handler")));
void OTG_HS_EP1_OUT_Handler(void) __attribute__((weak, alias("Default_handler")));
void OTG_HS_EP1_IN_Handler(void) __attribute__((weak, alias("Default_handler")));
void OTG_HS_WKUP_Handler(void) __attribute__((weak, alias("Default_handler")));
void OTG_HS_Handler(void) __attribute__((weak, alias("Default_handler")));
void DCMI_Handler(void) __attribute__((weak, alias("Default_handler")));
void CRYP_Handler(void) __attribute__((weak, alias("Default_handler")));
void HASH_RNG_Handler(void) __attribute__((weak, alias("Default_handler")));
void FPU_Handler(void) __attribute__((weak, alias("Default_handler")));

uint32_t vectors[] __attribute__((section(".isr_vector"))) = {
    STACK_START,
    (uint32_t)&Reset_Handler,
    (uint32_t)&NMI_Handler,
    (uint32_t)&HardFault_Handler,
    (uint32_t)&MemManage_Handler,
    (uint32_t)&BusFault_Handler,
    (uint32_t)&UsageFault_Handler,
    0,
    0,
    0,
    0,
    (uint32_t)&SVCall_Handler,
    (uint32_t)&Debug_Monitor_Handler,
    0,
    (uint32_t)&PendSV_Handler,
    (uint32_t)&SysTick_Handler,
    (uint32_t)&WWDG_Handler,
    (uint32_t)&PVD_Handler,
    (uint32_t)&TAMP_STAMP_Handler,
    (uint32_t)&RTC_WKUP_Handler,
    (uint32_t)&FLASH_Handler,
    (uint32_t)&RCC_Handler,
    (uint32_t)&EXTI0_Handler,
    (uint32_t)&EXTI1_Handler,
    (uint32_t)&EXTI2_Handler,
    (uint32_t)&EXTI3_Handler,
    (uint32_t)&EXTI4_Handler,
    (uint32_t)&DMA1_Stream0_Handler,
    (uint32_t)&DMA1_Stream1_Handler,
    (uint32_t)&DMA1_Stream2_Handler,
    (uint32_t)&DMA1_Stream3_Handler,
    (uint32_t)&DMA1_Stream4_Handler,
    (uint32_t)&DMA1_Stream5_Handler,
    (uint32_t)&DMA1_Stream6_Handler,
    (uint32_t)&ADC_Handler,
    (uint32_t)&CAN1_TX_Handler,
    (uint32_t)&CAN1_RX0_Handler,
    (uint32_t)&CAN1_RX1_Handler,
    (uint32_t)&CAN1_SCE_Handler,
    (uint32_t)&EXTI9_5_Handler,
    (uint32_t)&TIM1_BRK_TIM9_Handler,
    (uint32_t)&TIM1_UP_TIM10_Handler,
    (uint32_t)&TIM1_TRG_COM_TIM11_Handler,
    (uint32_t)&TIM1_CC_Handler,
    (uint32_t)&TIM2_Handler,
    (uint32_t)&TIM3_Handler,
    (uint32_t)&TIM4_Handler,
    (uint32_t)&I2C1_EV_Handler,
    (uint32_t)&I2C1_ER_Handler,
    (uint32_t)&I2C2_EV_Handler,
    (uint32_t)&I2C2_ER_Handler,
    (uint32_t)&SPI1_Handler,
    (uint32_t)&SPI2_Handler,
    (uint32_t)&USART1_Handler,
    (uint32_t)&USART2_Handler,
    (uint32_t)&USART3_Handler,
    (uint32_t)&EXTI15_10_Handler,
    (uint32_t)&RTC_Alarm_Handler,
    (uint32_t)&OTG_FS_WKUP_Handler,
    (uint32_t)&TIM8_BRK_TIM12_Handler,
    (uint32_t)&TIM8_UP_TIM13_Handler,
    (uint32_t)&TIM8_TRG_COM_TIM14_Handler,
    (uint32_t)&TIM8_CC_Handler,
    (uint32_t)&DMA1_Stream7_Handler,
    (uint32_t)&FSMC_Handler,
    (uint32_t)&SDIO_Handler,
    (uint32_t)&TIM5_Handler,
    (uint32_t)&SPI3_Handler,
    (uint32_t)&UART4_Handler,
    (uint32_t)&UART5_Handler,
    (uint32_t)&TIM6_DAC_Handler,
    (uint32_t)&TIM7_Handler,
    (uint32_t)&DMA2_Stream0_Handler,
    (uint32_t)&DMA2_Stream1_Handler,
    (uint32_t)&DMA2_Stream2_Handler,
    (uint32_t)&DMA2_Stream3_Handler,
    (uint32_t)&DMA2_Stream4_Handler,
    (uint32_t)&ETH_Handler,
    (uint32_t)&ETH_WKUP_Handler,
    (uint32_t)&CAN2_TX_Handler,
    (uint32_t)&CAN2_RX0_Handler,
    (uint32_t)&CAN2_RX1_Handler,
    (uint32_t)&CAN2_SCE_Handler,
    (uint32_t)&OTG_FS_Handler,
    (uint32_t)&DMA2_Stream5_Handler,
    (uint32_t)&DMA2_Stream6_Handler,
    (uint32_t)&DMA2_Stream7_Handler,
    (uint32_t)&USART6_Handler,
    (uint32_t)&I2C3_EV_Handler,
    (uint32_t)&I2C3_ER_Handler,
    (uint32_t)&OTG_HS_EP1_OUT_Handler,
    (uint32_t)&OTG_HS_EP1_IN_Handler,
    (uint32_t)&OTG_HS_WKUP_Handler,
    (uint32_t)&OTG_HS_Handler,
    (uint32_t)&DCMI_Handler,
    (uint32_t)&CRYP_Handler,
    (uint32_t)&HASH_RNG_Handler,
    (uint32_t)&FPU_Handler,
};

void Default_handler(void)
{
    while (1)
        ;
}

void Reset_Handler(void)
{
    // Copy .data section to SRAM

    uint32_t size = (uint32_t)&_edata - (uint32_t)&_sdata;

    uint8_t *pDst = (uint8_t *)&_sdata; // SRAM
    uint8_t *pSrc = (uint8_t *)&_etext; // FLASH

    for (uint32_t i = 0; i < size; ++i)
    {
        *pDst++ = *pSrc++;
    }

    // Init the .bss section to zero in SRAM

    size = &_edata - &_sdata;

    pDst = (uint8_t *)&_sbss; // SRAM

    for (uint32_t i = 0; i < size; ++i)
    {
        *pDst++ = 0;
    }

    // Get things going

    main();
}

The first piece of code that we want to put in our startup C file is not really code in the sense that it will be executed, instead it is simply an array that will be placed in the interrupt vector table. The code won’t copy this data at runtime, instead the linker will simply place the static data in the correct memory location.

Essentially what is going on here is that whenever an interrupt occurs, the processor looks at the value at a predetermined memory address and then starts executing the code at this address. So what we need to do is we need to provide function pointers for our functions that we want handling these interrupt events. And just in case it wasn’t clear, the startup event itself can be seen as an interrupt. Specifically, it is for the first interrupt which is known as “reset”.

For our STM32, this table is provided in Table 61. of the chip’s reference manual.

The reason that this file is so long is because unfortunately you need to provide an entry for every single interrupt that the processor supports. However, as you can see in this file, we have only specified one special handler (the reset handler). All the other interrupts are sent to our default handler which simply hangs forever. We have created weak aliases for all other IRQ handlers which point to the default later. This means that you can add a dedicated IRQ handler for other interrupts that you care about simply by explicitly declaring a function body with the relevant name.

The goal of the reset handler in this case is fairly simple. It starts by copying all the initial variable values from flash into SRAM. Then it hands over to the main application function which will be defined in a Rust source file.

Main application file

#![no_std]
#![no_main]

use panic_halt as _; // you can put a breakpoint on `rust_begin_unwind` to catch panics

use cortex_m::asm;

#[no_mangle]
fn main() -> ! {
    asm::nop(); // To not have main optimize to abort in release mode, remove when you add code

    loop {
        // your code goes here
    }
}

Here it is, the moment that we’ve all been waiting for… our Rust code! In this Rust file we declare a function called main. Which, you guessed it, does absolutely nothing, it is just an endless loop for now… The important thing to note here is that we had to add the #[no_mangle] attribute to the main function. This is because by default, Rust mangles function names to create symbols and C doesn’t. For this specific function, that is not an option as our C startup code specifically calls a function with the symbol name “main”.

Cargo.toml file

[package]
edition = "2021"
name = "app"
version = "0.1.0"

[lib]
name = "app"
crate_type = ["staticlib"]
test = false
bench = false

[dependencies]
cortex-m = "0.7.4"
panic-halt = "0.2.0"

[profile.release]
codegen-units = 1 # better optimizations
debug = true # symbols are nice and they don't increase the size on Flash
lto = true # better optimizations

As we will be linking the final application binary ourselves, it is important that we use Cargo to compile our Rust code into a static library rather than an application. That is exactly what this Cargo.toml does, given that we specify crate_type = ["staticlib"]. Because we are creating a static lib named “app”, we will get a file named “libapp.a” as output from Cargo. This file can then be treated by the linker as essentially just another object file.

Makefile

CC = arm-none-eabi-gcc
MACH = cortex-m4
CFLAGS = -c -mcpu=$(MACH) -mthumb -mfloat-abi=hard -std=gnu11 -O0 -Wall -g
LDFLAGS = -nostdlib -T stm32_ls.ld
RSTARGET = thumbv7em-none-eabihf

all: final.elf

stm32_startup.o: src/stm32_startup.c
	$(CC) $(CFLAGS) $^ -o $@

libapp.a: FORCE
	Cargo build --target $(RSTARGET)
	cp target/$(RSTARGET)/debug/libapp.a .

final.elf: stm32_startup.o libapp.a
	$(CC) $(LDFLAGS) $^ -o $@

clean:
	rm -rf *.o *.elf *.a
	Cargo clean

FORCE:

Our Makefile is also quite simple. It starts by compiling the startup C file using GCC. Next it compiles “libapp.a” using Cargo and finally it links the two resulting files into a single ELF file that we can flash to our STM32.

An interesting thing to note here is that we need to use a bit of a trick to force make to run Cargo every single time since make doesn’t really understand how to respond properly to file changes on the Rust side. This is not a big deal as Cargo doesn’t do much if it’s run on unchanged files. I have previously employed a similar trick without too many negative side effects when I designed the original CMake-based build system for cxx-qt.

Next steps

The next step will be to test this code using an emulator to make sure that it actually works. Spoiler alert, I have already done this using the Renode emulator and I can confirm that it does indeed work 🥳. In a follow up post, I will discuss how this can be done. The next step after this will be to test it with a physical board. This part I admittedly still have to do. Thumbs crossed that nothing goes wrong here…

Of course, all I plan on doing here is to set debugger breakpoints and see if they can be hit. The long term plan is to go way more advanced and turn on an LED 😲.

References

Most of my startup and C code essentially comes verbatim from the Bare metal embedded lecture series on YouTube. I watched that series to get an understanding for how this stuff works myself. So if anything non-Rust in this post isn’t quite clear, I recommend you watch that series.

The Rust code here is largely inspired by code from the initial parts of The Embedded Rust Book which is also an excellent read for anyone looking to get into embedded Rust.

Comments

Comments are hosted on GitHub