Setting up an STM32F4 embedded project using GNU tools (gcc, ld, etc.) involves a few things – a makefile, a linker script and some startup code. This process is very similar for most or maybe all STM32 ARM microcontrollers. Creating a project template will greatly speed up development since we don’t have to go through the project creation process manually each time we create a new project. In this post we’ll explore how to set up a very simple project template that we can use as a starting point for future STM32F4 projects.

Motivation

ST provides a very easy to use IDE to use when developing for the STM32 family of microcontrollers. I have used the IDE before and I particularly enjoy using the code configuration tool to set up the peripherals. I wanted to explore the stm32f4 family of MCUs a bit more and decided that setting up a GCC project template will be a nice place to start and learn.

Building a project almost from scratch (I borrowed a few ST files) is a great aid to understanding any particular microcontroller. So here’s what I found after a little reading online. I’ll describe how to setup this project on a linux system.

An Overview

Let’s take a minute to explore the various files we’ll need here. So we’ll need a linker script to tell our compiler (GCC) how to arrange our code in memory, we’ll also need some startup code to take care of some initialization, we’ll need a makefile to automate the compilation process – we don’t want to be typing in long commands in the terminal, and finally we can add our project code.

Installing GCC On Linux

We can download the latest ARM GCC toolchain for linux from the ARM website. After downloading the archive file, we’ll extract it to our home folder – you can actually extract it anywhere you want! We can type in the following bash command to extract the file – replace the name of the archive file with the name of the file you downloaded.

tar -xvf gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux.tar.bz2

After extracting the file, we’ll have to add the path of the gcc tools to the “PATH” environment variable. Hence we don’t have to type in the full path of the tools in the terminal. We can open our “.bashrc” file in our favorite editor and add the following command to the end of the file. Replace “user_name” in the command with your actual user name.

export PATH="$PATH:/home/user_name/gcc-arm-none-eabi-10-2020-q4-major/bin"

After this we’ll save our file and run the commands in the “.bashrc” file so they take immediate effect by running the following shell command.

source .bashrc

To find out if our installation was successful we can run the following command in the terminal.

arm-none-eabi-gcc --version

If we did everything right we should see the following output or something similar depending on the version installed.

arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10-2020-q4-major) 10.2.1 20201103 (release)
Copyright (C) 2020 Free Software Foundation, Inc.
...

With our installation complete we can start coding up the project.

The Linker Script

The final step of the compilation process is linking. This involves combining object files, relocating their data and tying up symbol references. A linker script simply tells the GNU linker which physical memory addresses of the microcontroller to put various sections of the compiled code.

Firstly, we’ll start by specifying the entry point of our program as shown below. The first function called upon system startup is the entry point.

/* Program entry point */
ENTRY(Reset_Handler)

Secondly we’ll define the stack pointer to be the lowest region in RAM memory. For the ARM cortex-M architecture the stack top is at the lowest system address, and the stack grows downwards by decreasing the memory address.

/* Highest address of the user mode stack */
/* End of "RAM" type memory */
__stack_end__ = 0x2001FFFF;

Memory Layout

Next, we’ll specify the memory layout of our microcontroller – this involves specifying the types and sizes of memory available on the chip. For the stm32f407vgt6 microcontroller used for this project we get 1Mbytes of flash and a total of 192Kbytes of RAM including core coupled memory. The stm32f407vgt6 has 64Kbytes of core coupled memory. We won’t use the core coupled memory in this project.

The code below shows the memory layout. Note that we also specify in the memory layout what functions the memory can perform – this can be a combination of reading, writing, or code execution. The flash memory is specified as “rx” for reading and code execution only.

/* Memory model/layout of the system */
/* r - readable, w - writeable, x - executable */
MEMORY
{
    FLASH   (rx)  : ORIGIN = 0x08000000,  LENGTH = 1024K
    CCMRAM  (rwx) : ORIGIN = 0x10000000,  LENGTH = 64K
    RAM     (rwx) : ORIGIN = 0x20000000,  LENGTH = 128K
}

After specifying the entry point, the stack address and the memory model, we’ll specify the various sections of our code and where the linker should allocate those sections. For example, we can run certain high performance portions of code from RAM or even core coupled memory. In this project we’ll store our application code in flash memory. Initialized and uninitialized variables go into the RAM.

But wait! Whenever we program a microcontroller, we are writing data into the microcontroller’s flash memory. This means we do not have a way to specify the contents of the microcontroller’s RAM during programming. Even if we had a means to do so, RAM is volatile memory and the initialized variables will be lost upon the very first reset or loss of power. We solve this by loading all initialized variables into flash memory (ROM) and copying them to the appropriate RAM locations upon startup.

Memory Allocation

The linker organizes the program in sections. Here are a few sections to keep in mind. The “.text” section stores application code including the main function. The “.rodata” section contains read only data – this includes strings and constants. The “.data” section contains initialized variables and the “.bss” section contains declared but uninitialized variables.

Since the “.rodata” section contains read only data, we can store this section in flash. We’ll also store the “.text” section for application code in flash memory.

Now here’s where things get a bit interesting. As for uninitialized variables, the linker will just allocate memory for them in RAM. We’ll zero all unitialized variables in the startup code to comply with the C standard. Initialized variables are first stored in flash (ROM) and copied to RAM upon startup.

Sections are specified as shown in the code below.

.data :
{
    . = ALIGN(4);
    __data_start__ = .;
    *(.data)
    *(.data*)
    . = ALIGN(4);
    __data_end__ = .;
} > RAM AT> FLASH

The Startup Code

As soon as our microcontroller boots, it doesn’t go straight to main. The microcontroller has to do a little housekeeping before heading to main. The startup code copies the contents of initialized variables from their ROM addresses where they are loaded, to their RAM addresses where they are referenced from. We also have to zero out all uninitialized variables. Finally we have to setup the interrupt vector table and initialize the stack pointer. The startup code is typically written in assembly language for optimum performance. For this project I reused the startup code generated by the stm32 IDE for an earlier project.

The Makefile

Even for a fairly simple project, there are a lot of commands to type for compilation! A makefile will automate this process and execute the appropriate shell commands when invoked.

Typically to compile a program with multiple source files, we’ll first compile the individual source files into object files first and then we’ll link them all together to get an executable.

We tell make how to compile our program by specifying rules. A rule is made up of a target, prerequisites and a recipe. Basically, make needs to know when to update the target and how to update it.

How “Make” Works

Make updates the target file by comparing the timestamp of the target to that of the prerequisites. Make updates targets that are older than their prerequisites. Here is an example:

main.o: main.c 
    gcc -c -o main.o main.c

Here main.o is the target and the prerequisite is main.c. If main.o doesn’t exist or is older than main.c, make invokes the shell command “gcc -c -o main.o main.c” to update main.o.

In our makefile, we set a default goal which is the ultimate file we want to create – in this case we want to create a “.bin” file to load into our microcontroller. Make will figure out when to rebuild updated files so our target file is up to date using the rules we specify.

This is just a gist of how make works. You can read more about it here.

Getting To Main

Here, we’ll just write some simple code to turn on some LEDs. I used the stm32f4 discovery development board to test this project. The development board has 4 LEDs and we’ll just light them all up!

int main(void)
{
    .
    .
    .

    // Turn on LEDs on PD15, PD14, PD13 and PD12.
    while (1)
    {
        GPIOD->BSRR |= GPIO_BSRR_BS15;
        GPIOD->BSRR |= GPIO_BSRR_BS14;
        GPIOD->BSRR |= GPIO_BSRR_BS13;
        GPIOD->BSRR |= GPIO_BSRR_BS12;
    }
}

Programming The Microcontroller

The stm32f4 discovery board has an st-link/v2 programmer on the board. So we can write a simple makefile command to program the microcontroller. You can follow the instructions here to install the stlink software that we’ll use for programming.

.PHONY: burn
burn:
	st-flash --reset write $(PROJECT).bin 0x8000000

To compile our project we’ll just run the following command in our project directory.

make

And to program our microcontroller, we’ll just have to run this command.

make burn

Summary

This post is an introduction to setting up an stm32 project with GCC. I hope you find it useful when building your next project. You can find the project on Github.

References

I found a bunch of awesome resources online which helped me put this project together. You can check them out for more information:

Vaughn Kottler’s youtube series on ARM development with GCC and make.

Thea’s blog post on linker scripts.