UP | HOME

A GCC/CMake Build environment for the STM32F4 Discovery

Table of Contents

Intro

This is a quick guide for making a complete build environment for programming the STM32F4 Discovery board. It's going to avoid using any IDEs and will just use the library provided by STM. I will try to explain every part of the toolchain but a few files will be copied from the templates STM provides

Note: After getting some feedback online here and here. It seems the Standard Peripheral Library I'm using is depricated. Please look over the comments carefully before you choose to use the setup I describe

The directory layout is very simple

/STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/
 -[..] Files that come from STM
 -CMakLists.txt
/src/
 - main.h
 - main.cpp
-CMakeLists.txt
  • The STM32F4xx_DSP_StdPeriph_Lib_V1.8.0 is simple downloaded from the STM website and unzipped
  • The 2 CMakeLists.txt will be explained later

The toolchain

We're going to try to keep things really bare-bones. I'm building using arm-none-eabi-gcc and it's associated tools. I did this on a Debian system where this version of gcc can be installed from the repository. The build tool I'm using is CMake.

We tell CMake about our toolchain through the a toolchain file see: https://cmake.org/cmake/help/v3.6/manual/cmake-toolchains.7.html

set(CMAKE_SYSTEM_NAME Generic) # 'Generic' is used for embedded systems

set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)

# tells CMake not to try to link executables during its interal checks
# things are not going to link properly without a linker script
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP arm-none-eabi-objdump)
set(CMAKE_SIZE arm-none-eabi-size)
set(CMAKE_DEBUGGER arm-none-eabi-gdb)
set(CMAKE_DEBUGGER arm-none-eabi-gdb)
set(CMAKE_CPPFILT arm-none-eabi-c++filt)

If you skip writing a toolchain file then CMake will default to your system compiler and things will start to slowly go wrong for you

The STM Libraries

To easily write code for this board we will leverage the libraries provided by STM. The libraries come in two main parts:

  • The CMSIS ( Cortex Microcontroller Software Interface Standard ) : This library comes from ARM. It's split into several semi-independent components and provides a common base for all ARM devices (independent of vendor). Since we want to get a basic example running, we'll just focus on CMSIS-CORE. The other components are related to RTOS/DSP/Debugging and are explained in more detail in the documentation STM32F4xx_DSP_StdPeriph_Lib_V1.7.0/Libraries/CMSIS/Documentation/General/html/index.html
    • The core is the interface to the hardware specified by ARM (see STM32F4xx_DSP_StdPeriph_Lib_V1.7.0/Libraries/CMSIS/Include/). In essence this is a header only library. If you look at some of the files you'll see that the functions simply map to one or two lines of ARM assembly. Simple things like "where is the stack pointer?", "add these two numbers", "get a value from this memory address". It allows the developer to omit writing the assembly by hand and to stay in the C/C++ world. All these instructions are standard across ARM chips, so naturally this interface is also standard and portable.
  • The STM32F4xx DSP and Standard Peripherals Library : This provides the libraries for accessing the peripherals provided by STM. These are different between vendors, so this part is not portable. This part is more or less required b/c your chip needs to communicate to the outside world in one way or another (even if it's crunching pi, you'll need to somehow display the result). You could look up the peripheral registers and do this manually, but the code resulting assembly would still not be portable.
  • Finally there is one last file that glues everything together. This is stm32f4xx.h which lives in /STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Libraries/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h In short, it specifies (and isolates) all the model specific parameters of the chip you are using. This will affect both libraries so it's sorta what needs to be defined before anything else. Conveniently stm32f4xx.h will include the CMSIS (line 817) and the Peripheral Library (if enabled) itself. So later in our main.h we will just have a:
#include "stm32f4.h"

and everything will get included

If you look at the opening comment in stm32f4xx.h you might be a bit confused, but naturally you're required to select a device using the preprocessor - otherwise you will get an error on build (see lines 68-123). You also need to pass in a value to enable the peripheral library. At the end of stm32f4xx.h you will see a conditional include

#ifdef USE_STDPERIPH_DRIVER
  #include "stm32f4xx_conf.h"
#endif /* USE_STDPERIPH_DRIVER */

This is the header that actually includes all the peripheral library header available for your device. If you look around for this header you won't find it in the library. STM provides one in: STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Project/STM32F4xx_StdPeriph_Templates/

Why is this file here and not with the others? I'm not really sure. For the sake of simplicity in our build I will add this template directory to the include path, but in a real project maybe you want to create a copy in your source directory.

Now that we have all the pieced, the final step is to write a cmake file that will build the library into a .so file.

cmake_minimum_required(VERSION 3.0)
project(STM32F4xx_DSP_StdPeriph_Lib-for-STM32F40_41xxx VERSION 1.8.0 LANGUAGES C)

# include the standard library implementation
set(src 
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_wwdg.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_usart.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_tim.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_syscfg.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_spi.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_spdifrx.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_sdio.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_sai.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_rtc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_rng.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_rcc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_qspi.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_pwr.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_ltdc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_lptim.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_iwdg.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_i2c.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_hash.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_hash_sha1.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_hash_md5.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_gpio.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_fsmc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_fmpi2c.c
  #Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_fmc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_flash.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_flash_ramfunc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_exti.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_dsi.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_dma2d.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_dma.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_dfsdm.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_dcmi.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_dbgmcu.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_dac.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_cryp.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_cryp_tdes.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_cryp_des.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_cryp_aes.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_crc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_cec.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_can.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/stm32f4xx_adc.c
  Libraries/STM32F4xx_StdPeriph_Driver/src/misc.c)

add_library(stm32f4 ${src})

# set the chip model number
target_compile_definitions(stm32f4 PUBLIC STM32F40_41xxx)
# turn on using the peripherals
target_compile_definitions(stm32f4 PUBLIC USE_STDPERIPH_DRIVER)

# the chip specific configurations
target_include_directories(stm32f4 
  PUBLIC
  Libraries/CMSIS/Device/ST/STM32F4xx/Include/)
# the peripheral configuration file 
#(again: normally you'd have a copy in your src directory)
target_include_directories(stm32f4 
  PUBLIC Project/STM32F4xx_StdPeriph_Templates/)
# the CMSIS interface
target_include_directories(stm32f4 
  PUBLIC Libraries/CMSIS/Include)
# the standard library headers
target_include_directories(stm32f4 
  PUBLIC Libraries/STM32F4xx_StdPeriph_Driver/inc/)

# The PUBLIC keyword sets these flags to be part of the interface.
# So any executable that links this library will have to use these flags as well
target_compile_options(stm32f4 PUBLIC
  -mcpu=cortex-m4 
  -mthumb 
  -mthumb-interwork 
  -mlittle-endian 
  -mfloat-abi=hard 
  -mfpu=fpv4-sp-d16)

Note how one line in the file list is commented out! There is a major annoyance that depending on the model you will include different peripherals, which means some peripheral source files will no longer make sense. For example, the STM32F40_41xxx group of chips' stm32f4xx_conf.h will not include stm32f4xx_fmc.h b/c that peripheral is no available on these models. So you will also need to be careful to then go and remove it from this sources list as well. Otherwise this will blow up in a weird and confusing way (found out how to fix this here: https://sourceforge.net/p/gnuarmeclipse/support-requests/108/)

Building a template/example

Now that we have the library built, lets build an example. For simplicity and convenience I recommend just working with the template provided in STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Project/STM32F4xx_StdPeriph_Templates/ We want to copy over the main.c and main.h in to our source directory. If you look in to these these templates you will see that they set up some clock "stuff" and GPIO variables - but will otherwise do nothing. There is a little section labeled Add your application code here on line 66 where you can start writing your own code.

The Startup File

This first catch with programming the microcontroller is that you can't simply start at the top of main(). When the chip is powered off, the program is stored permanently in the Flash memory (ROM). Because there is no operating system to loading the program into RAM we need to do that ourselves in addition to initializing system clocks and event handlers.

This process is independent of the actual program itself - so the standard way of doing this is by separating it out into a "startup file". This is generally written in assembly and reused between projects.

STM provides us with examples in /STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Libraries/CMSIS/Device/ST/STM32F4xx/Source/Templates If you compare the one in gcc_ride7/ and arm/ they're quite different.. I'm not sure why embedds.com provides more details on how to write one from scratch if you're interested For our purposes, we'll just use the one in the gcc_ride7/ directory

Inside the file you can see a Reset_Handler label which is the actual start point of your program. As the name suggests, this is where the chip will jump to when it gets reset or just powered on.

The way STM has arranged things in these templates is that the startup file sets up the memory and event handler but places system clock configuration in a separate file STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Libraries/CMSIS/Device/ST/STM32F4xx/Source/Templates/system_stm32f4xx.c So, in spite of the name, your startup process is actually spread across these two files. Again, it's not clear to me why it's arranged this way

This clock configuration file can also be generated using STM's wacky Excel spreadsheet More details are on Matthew Mucker's webpage

The Linker Script

When an application normally runs on a desktop machine it's generally running using virtual memory in a virtual application-specific address space. From the applications point of view it can manipulate it's own memory however it wants - and it's the operating system that then translates that into safe operations on the actually memory (for instance to insure that the applications doesn't touch any memory region it shouldn't)

On a simple microcontroller there is no operating system to manage the memory, and the memory is shared with other functionality. As we saw in the startup script, some addresses are reserved for peripherals, other addresses are for interrupts and reset bits, the stack and heap are allocated some place and there is also a split between ROM and RAM. So we can't just use the default linker and let it do whatever it wants. We need to specify the address space it can use via a linker script

Again, STM provides us with one in STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Project/STM32F4xx_StdPeriph_Templates/TrueSTUDIO/STM32F40_41xxx/STM32F417IG_FLASH.ld and it's the one we copy over

Building with CMake

Now that we have all the pieces we can glue it all together with a little CMake

cmake_minimum_required(VERSION 3.0)
enable_language(ASM)

set(src 
  STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Libraries/CMSIS/Device/ST/STM32F4xx/Source/Templates/gcc_ride7/startup_stm32f40_41xxx.s
  STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Libraries/CMSIS/Device/ST/STM32F4xx/Source/Templates/system_stm32f4xx.c
  src/main.c)

add_executable(example.elf ${src})
target_include_directories( example.elf PRIVATE src/ )

add_subdirectory(STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/)
target_link_libraries( example.elf stm32f4 )

file(COPY 
  ${CMAKE_CURRENT_SOURCE_DIR}/STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/Project/STM32F4xx_StdPeriph_Templates/TrueSTUDIO/STM32F40_41xxx/STM32F417IG_FLASH.ld
  DESTINATION
  ${CMAKE_BINARY_DIR})

set_target_properties(
  example.elf 
  PROPERTIES 
  LINK_FLAGS 
  "-TSTM32F417IG_FLASH.ld \
   -mthumb \
   -mcpu=cortex-m4 \
   -mfloat-abi=hard \
   -mfpu=fpv4-sp-d16 \
   -Wl,--gc-sections")

The build flags are inherited from the library we are linking and the link flags are pretty much the same, with the addition of -TSTM32F417IG_FLASH.ld (the one specifying our linker script)

Now we just run

cd some/build/directory
cmake /path/to/CMakeLists.txt
make

are we're done!

Getting it all on the board.. with OpenOCD/GDB

In our build directory we should now see a example.elf . This is the file we want to get on our micrcontroller

OpenOCD

Each development board will generally come with additional hardware for debugging and loading new programs onto the actual chip. On easy-to-use boards such as this one. this chip will talk over USB and will have its own protocols like JTAG and SWD. However, as the user we don't really want to have to deal with these protocols directly, nor do we really want to interact with this helper-chip.

To handle this mess we have OpenOCD. Once we have it setup, it'll do all the connecting and communicating and in turn OpenOCD will provide us with a GDB server - so interacting with any board is "standardized" to simply interacting with GDB.

Fortunately b/c we're using a very vanilla development board OpenOCD provides some existing configuration files that we can use to quickly get up and running. We just need to point to them from a simple master-config-file which we'll put into our build directory.

Each line is pretty self explanatory

# This is an STM32F4 discovery board with a single STM32F407VGT6 chip.
# http://www.st.com/internet/evalboard/product/252419.jsp

source [find interface/stlink-v2.cfg]
transport select hla_swd
source [find target/stm32f4x.cfg]
reset_config srst_only

Save this to an openocd.cfg in our build directory (that's the default file name OpenOCD looks for) and launch openocd as root on a separate console

You'll get something like:


Open On-Chip Debugger 0.9.0 (2017-03-07-13:28)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 2000 kHz
adapter_nsrst_delay: 100
none separate
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : clock speed 1800 kHz
Info : STLINK v2 JTAG v14 API v2 SWIM v0 VID 0x0483 PID 0x3748
Info : using stlink api v2
Info : Target voltage: 2.895868
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints

Though it doesn't spell it out, our GDB server is now running! From the rest of the text we see that there was some clock issue that got corrected and it connected over JTAG/STLINK. We also learn that this chip has 6 breakpoints and 4 watchpoints (this is something we can tell GDB about later so that it won't allow us to use more than we have)

So now lets connect to it over GDB

GDB

In a new console window run

> arm-none-eabi-gdb example.elf

note: make sure you run the arm version of gdb and not run your system's gdb. The system gdb will not give you any errors at first and it will half work until you start getting strange behavior down the line

This will load up the example.elf into the GDB session. To then flash the program onto the board we run a session like this

$ arm-none-eabi-gdb example.elf
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
...
(gdb) monitor reset halt
...
(gdb) load
Loading section .vectors, size 0x100 lma 0x20000000
Loading section .text, size 0x5a0 lma 0x20000100
Loading section .data, size 0x18 lma 0x200006a0
Start address 0x2000061c, load size 1720
Transfer rate: 22 KB/sec, 573 bytes/write.
(gdb) continue
Continuing.
...

(taken from: http://openocd.org/doc/html/GDB-and-OpenOCD.html) This will:

  • connect to the GDB server
  • halt the program currently running on the chip
  • load our example.elf program into the chip
  • let the micro run (with the new program in memory)

and that's it! We're done

We have our template/example on the board :) You can now take any example from online and our framework should continue to work - as long as it doesn't require other libraries.

Other Resources

I personally have almost no experience programming microcontrollers, so most of this guide has beenput together by reading-the-manual and a lot of tid-bits from other resources

  • Matthiew Mucker has a great series setting up a build environment for the STM32F0DISCOVERY on Windows using GCC and Eclipse: part1, part2, part3, part4, part5
  • Geoffrey Brown has a great book called Discovering the STM32 Microcontroller which you can get online. He provides his own templates using Make and CodeSourcery for the STM32 VL Discovery. This seems like a really great book to start with if you want to really learn about programming ARM micros.
  • A more polished CMake/GCC environment is available on github, thanks to Konstantin Oblaukhov. It pretty much does what I did above, but it's written in a way where you can select your model number and cmake will do the rest for you. The CMake code is very clean, but uses the older 2.x style.

This webpage is generated from an org-document (at ./index.org) that also generates all the files described.

Once opened in Emacs:

  • C-c C-e h h generates the webpage
  • C-c C-v C-t exports the code blocks into the appropriate files