Getting started with STM32F401RE Nucleo starter board on FreeBSD

27 Mar 2019 - tsp
Last update 16 Apr 2019
Reading time 4 mins

This is a (really) short summary on how to get started with compiling and uploading code to the STM32F401RE nucleo starter board (for example available via Amazon) without using any of the bloated IDEs or code generators that are normally suggested. This is a simple development board for STM32F401 and an easy starting point for further experiments with this microcontroller.

Required software:

One can either install them from binary package management:

pkg install devel/arm-non-eabi-gcc
pkg install devel/arm-non-eabi-binutils
pkg install devel/arm-non-eabi-newlib
pkg install devel/openocd

or via ports

cd /usr/ports/devel/arm-non-eabi-gcc && make install clean
cd /usr/ports/devel/arm-non-eabi-binutils && make install clean
cd /usr/ports/devel/arm-non-eabi-newlib && make install clean
cd /usr/ports/devel/openocd && make install clean

One should also download the stm32cubef4 package and extract it somewhere in ones home directory or on the local machine. This is primary required for the CMSIS drivers and templates (of course it’s possible to develop applications without them but they make life much easier)

Basic project structure

The basic STM32F4 project requires an SystemInit() function that gets called from the startup_stm32f401xe.s assembly file before libc initialization and an main() function that will be called after libc initialization.

If you want to use startup_stm32f401xe.s copy that file from the stm32cubef4 packages Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/arm/ directory into your project directory.

One can disable initialization of libc by commenting the line bl __libc_init_array. With the previously mentioned newlib it was crucial to disable these initializations. In this case one could also merge SystemInit and main into a single function. The system initialization function should at least enable basic clocks, configure the main PLL, set the clock source to the PLL, and configure the APB clock prescalers.

An example function could look like the following and might be saved into system.c:

void SystemInit() {
        // Enable the power interface clock

        PWR->CR |= PWR_CR_VOS_1;

        while ((RCC->CR & RCC_CR_HSIRDY) == 0);

        PWR->CR |= (uint32_t)(16 << 3);

            PLL configuration
            VCO=336 MHz, PLLOUT=84 MHz, USB: 48 MHz, M:16, N:336, P:4, Q:7

        // Disable PLL and wait till it's disabled
        RCC->CR &= ~(RCC_CR_PLLON);
        while ((RCC->CR & RCC_CR_PLLRDY) != 0);
        // Load configuration as described above
        RCC->PLLCFGR = 0x27005410 | RCC_PLLCFGR_PLLP_0;
        // Enable PLL and wait till it's locked
        RCC->CR |= RCC_CR_PLLON;
        while ((RCC->CR & RCC_CR_PLLRDY) == 0);

        // Configure flash memory
                for(;;) { /* Endless loop in case of error */ }

        // Select PLL as clock source
        while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL);

        // Set prescaler
        RCC->CFGR &= ~(RCC_CFGR_HPRE);
        RCC->CFGR &= ~(RCC_CFGR_PPRE2);

One can then write a main program into main.c, for example this small blinking test program:

#include <stm32f4xx.h>

// Simple busy-waiting sleep routine. Note that scaling is not fully accurate
void sleep_ms(uint32_t ms) {
	volatile uint32_t i;
	for (i = ms*1000; i != 0; i--) { }

int main() {
        // Enable AHB1 clock for GPIO bank
        // Configure as fast output
        GPIOA->MODER |= (1 << (LED_PIN << 1));
        GPIOA->OSPEEDR |= (3 << (LED_PIN << 1));

        // Enable LED on A5 / D15
        GPIOA->BSRR = 0x20;

        for(;;) {
	        GPIOA->BSRR = 0x200000;
	        GPIOA->BSRR = 0x20;

or the same program using timer interrupts:

#include <stm32f4xx.h>

#define LED_PIN 5
#define LED_ON() GPIOA->BSRR |= (1 << LED_PIN)
#define LED_OFF() GPIOA->BSRR |= (1 << (16+LED_PIN))

void sleep_ms(uint32_t ms) {
  volatile uint32_t i;
  for (i = ms*1000; i != 0; i--) {

void TIM3_IRQHandler(void);

int main() {
        /* Enbale GPIOB clock */
        /* Configure GPIOA pin 5 as output */
        GPIOA->MODER |= (1 << (LED_PIN << 1));
        /* Configure GPIOA pin 5 in max speed */
        GPIOA->OSPEEDR |= (3 << (LED_PIN << 1));

        /* Turn on the LED */
        GPIOA->BSRR = 0x20;

        // Configure our timebase
        TIM3->PSC = 599;
        TIM3->ARR = 60000;
        TIM3->DIER |= 0x01;
        NVIC->ISER[0] |= 1 << 29;
        TIM3->CR1 = TIM_CR1_CEN;

        for(;;) { }

volatile int blink = 0;
void TIM3_IRQHandler(void) {
        if(TIM3->SR & TIM_SR_UIF) {
                if(blink == 0) {
                        blink = 1;
                } else {
                        blink = 0;
                TIM3->SR &= ~TIM_SR_UIF;

Compiling and linking

Comiling and linking code is done with arm-none-eabi-gcc:

arm-none-eabi-gcc -Wall -mcpu=cortex-m4 -mlittle-endian -mthumb -I${stm32cubef4Home}/Drivers/CMSIS/Device/ST/STM32F4xx/Include -I${stm32cubef4Home}/Drivers/CMSIS/Include -DSTM32F401xE -Os -c system.c -o system.o
arm-none-eabi-gcc -Wall -mcpu=cortex-m4 -mlittle-endian -mthumb -I${stm32cubef4Home}/Drivers/CMSIS/Device/ST/STM32F4xx/Include -I${stm32cubef4Home}/Drivers/CMSIS/Include -DSTM32F401xE -Os -c main.c -o main.o
arm-none-eabi-gcc -Wall -mcpu=cortex-m4 -mlittle-endian -mthumb -I${stm32cubef4Home}/Drivers/CMSIS/Device/ST/STM32F4xx/Include -I${stm32cubef4Home}/Drivers/CMSIS/Include -DSTM32F401xE -Os -c startup_stm32f401xe.s -o startup_stm32f401xe.o
arm-none-eabi-gcc -mcpu=cortex-m4 -mlittle-endian -mthumb -DSTM32F401xE -T${stm32cubef4Home}/Projects/STM32F401RE-Nucleo/Templates/TrueSTUDIO/STM32F4xx-Nucleo/STM32F401CE_FLASH.ld -Wl,--gc-sections system.o main.o startup_stm32f401xe.o -o main.elf
arm-none-eabi-objcopy -Oihex main.elf main.hex


The resulting hex file can now be written by using openocd:

openocd -f /usr/local/share/openocd/scripts/board/st_nucleo_f4.cfg -c init -c "reset halt" -c "flash write_image erase main.hex" -c "verify_image main.hex" -c reset -c shutdown

Depending on your USB devfs configuration you might be required to run that command with elevated privileges or change access permission on USB devices (the latter one of course being the favored way)

This article is tagged:

Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support