EFM32 Cortex-M firmware project from scratch - step by step
Most popular microcontrollers come with IDEs and tools provided by the manufacturer, like NXP, STM, TI or Silicon Labs. IDEs are commonly based on Eclipse and creating a project for almost any chip in those IDEs is usually just a click away so why would you ever want to make such a project from scratch, gather all header files, libraries and scripts? Read to find out why and how :-)
This project available on Github.
To build your project in an Eclipse-based IDEs with proprietary add-ons you have to rely on the supplier. Embedded systems often have very long support lifetimes (10+ years). Some problems that may pop up when the customer asks you to develop a new, small feature (or fix a bug):
- Will the IDE still still run on my operating system in 10 years from now?
- Will the IDE still support my (obsolete) chip after 10 years?
- Will this particular SDK and libraries still be compatible with rest of my project after 10 years?
- Where do I download all this stuff from after 10 years?
- If only the latest version of the tools is available - will it open my project at all?
- Will the code compile with a new toolchain?
- How will the device behave when built with new/different tools? Will it have to be re-certified? Is there even a budget for that?
I have seen at work many of the problems listed above, like engineers scrambling to find a Windows 2000 machine or VM (because it was the latest OS that the exotic tools could run on and they were lucky enough to have the original installation discs).
Once, I was involved in a project using EFM32 (this it the main reason for this article!). Of course I started by downloading the official tools, setting up a project, implementing some stuff, pushing it to the repository. Some time later I was transferred to a different project and another developer took over. Of course he also used the official tools. After some more time I got back to the project, pulled the latest version only to find out that it does not build. It took me and him some time to analyze the problem - it turned out that the SDK on his machine was newer and there were some API changes. SDK was not part of the project but part of the IDE. The IDE downloaded the SDK the first time it was run and it always downloaded the latest version. It was a mild hiccup (and some lost hours) that made me realize that, for long-term serious projects, I had to rely on my own tools.
Benefits of a standalone project and own build system
- You control the whole situation - the libraries, the compilers, the scripts (and also have to carry out the maintenance yourself!).
makeis a standard tool that has proven its worth (and very likely will still be used in the future).
- Makefile projects can be automated. It can also be done with a headless Eclipse CDT builder but good luck with all the proprietary add-ons on a headless server :-)
- An automated project can use a build server common to all developers (less surprises with code that "works on my machine"™).
- An automated project can use automatic testing (for example: unit tests, hardware-in-the-loop testing).
- The whole release process can be automated. The less manual steps, the less opportunities for mistakes. When was the last time you have accidentally shipped a debug version? Or version with untested features?
Real world example
I will show step-by-step how to assemble a standalone project with "bare hands". The target device will be an EFM32GG332F512. Coincidentally, Simplicity Studio (the official IDE for EFM32) had problems downloading the SDK on my Gentoo due to some SWT bugs (my system is "too new"). :-)
The toolchain is a name for compiler + assembler + linker + some other utilities that are used to make application binaries. The MCU is an ARM Cortex-M so my preferred toolchain is GNU Arm Embedded straight from ARM. I choose the Linux ZIP file and unpack it either to
/opt/toolchains or somewhere in my home directory.
The path to the bin directory of the toolchain has to be added to
$PATH shell variable so that it can be used without typing the whole path.
I will refer to the
firmware directory as "project directory" or "root directory". This is one way of organizing the project files:
1 2 3 4 5 6 7 8 9 10
Getting the EFM32 SDK
The SDK for EFM32 is called "Gecko SDK" and can be downloaded straight from Github. I will explain which files are needed for the EFM32GG and which one can be skipped.
Linker script defines addresses and sizes of all sections, ie. at what address the RAM starts and ends, where is the flash address space and what is the entry point (the very very first function to be called - usually somewhere in the startup code). If a wrong linker script is used the result will be most likely a totally unusable binary, the CPU will not run or will crash immediately.
In this case there is just a single file for GCC for the whole EFM32GG family - copy
platform/Device/SiliconLabs/EFM32GG/Source/GCC/efm32gg.ld to your
How to look for linker scripts in an unknown SDK?
find ./ -name '*.ld' and look in the file or directory name for GCC, or Atollic, or CodeSourcery (those companies delivered IDEs with GCC-based toolchains). Do not use files for Keil or IAR (different companies with their own toolchains, NOT compatible with GCC).
Header files define register names and peripheral names so that you can use
USART0_BASE in your code instead of a raw address like
0x4000C000. They are more part-specific (because not all devices with a family have the same set of peripherals, like USB). Very likely headers from "smaller" part like EFM32GG332F512 (512KB flash) work well on a bigger part like EFM32GG332F1024 (1MB flash) within the same subfamily (ie. EFM32GG332Fxxxx).
I decided to copy all files from SDK's
platform/Device/SiliconLabs/EFM32GG/Include to my
include directory. This will help if I ever choose to move to a slightly different part.
main() function is called, a significant piece of magic must happen first. This magic is commonly called the startup code.
Startup code for this particular Cortex-M performs two tasks: clearing of
.bss section (ie. all global variables without assigned values) and assigning values to variables
.data (ie. all global/static variables with assigned values).
Other MCUs or architectures may need more code, for example: setting of the initial stack pointer (done automatically by the ARM core), configuration of external RAM or various clocks.
I created a directory named
startup_code and copied all files from
platform/Device/SiliconLabs/EFM32GG/Source/GCC/ (except the .S file) plus
platform/Device/SiliconLabs/EFM32GG/Source/system_efm32gg.c . It turns out that startup of the EFM32 can be done entirely in C! :-)
CMSIS is a standard developed by ARM for their cores. It is a separate set of files, because it is provided by ARM and is common across different MCUs that have the same core (eg. Cortex-M3 or Cortex-M0+), while peripherals and headers specific to the EFM32 are provided by Silicon Labs (described two sections above).
CMSIS headers provide functions like enabling/disabling of global interrupts, configuration of the SysTick timer or NVIC (interrupt controller).
I copied files fom
CMSIS/Include in my project.
Make is the most common build automation tool. There are hundreds ways of writing makefiles. I have chosen boilermake - it automates 90% of the process. You can get the Makefile from https://github.com/dmoulding/boilermake and place in root directory of the project.
Boilermake requires custom submakefiles - at least one called
main.mk and placed in the same directory as
This is a minimal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
This makefile contains:
- compiler, size and objcopy commands (regular gcc names + ARM prefix)
COMMON_FLAGSapply to the compiler and linker (why is this important) - basically the CPU architecture, floating point support and debugging option
DEFINES- this variable depends on the exact chip. It is used by header and driver files.
INCLUDE- specifies where to look for header files so you can use
#include <core_cm3.h>instead of
WARNINGS- enables compiler warnings. In general warnings are a good way to find coding mistakes stupid enough, that even the compiler can spot them!
CFLAGS- compiler flags. I add the language standard (so that I can use for example
_Static_assertand optimization level). Data and function sections are used during linking to remove unused variables and functions from the final binary and reduce its size.
SUBMAKEFILES- this variable includes submakefiles that declare
.cfiles to compile (keep reading for full explanation).
TARGET_DIR- directories where to put temporary files and the final binary
LDFLAGS- linker flags: which linker script to use and to discard unused symbols
TARGET- name of the output binary. It will always (for GCC) be an ELF file. It is easy to make binary or Intel hex files with
- ...and finally a target that prints the size of the final binary (so that you can know how much flash is left)
Now it is time to describe which source files actually should be compiled. This is done in submakefiles. I prefer to have a submakefile for each large directory so that I don't have to modify much if I add or remove libraries (like mbedTLS). This minimum setup requires two submakefiles - one for startup code and one for main code.
Basically every C file has to be specified in those files. Files can either be in a single long line or in separate lines with backslashes at the end. I will show later how to build a list of all C files quickly. The paths of the
.c files are relative to the
I also created a dummy
main/main.c file with the following contents:
1 2 3 4 5 6
This is a simple endless loop with a breakpoint statement. If debugger stops there it mean that the build process was successful, linker script is okay and startup code is also okay. Or to read it the other way - if this does not work - something is VERY wrong.
To build: simply type
make size) in the root directory of the project. Output should be similar to this:
1 2 3 4 5 6 7
build_output/main.elf can be loaded to the chip and tested with a debugger!
Ozone (from SEGGER) is my favorite debugger. Here you can see that the CPU stopped at the breakpoint within
main() so it basically did nothing but did that in a proper way! :-)
Next step - adding more libraries
Since the previous example did exactly nothing in a spectacular way, it is time to add libraries that will make the chip do anything useful.
J-Link with SEGGER RTT allows
printf-like debugging over JTAG (or SWD). This comes very handy, because literally nothing has to be configured on the MCU side. In contrary - getting basic UART output can be challenging, especially when working with a chip for the first time (peripheral clocks, USART, GPIO multiplexing etc...). RTT works by keeping a buffer in RAM that is continuously read by the debugger. It works both ways so some data can be written back to the MCU (just like over a debug UART).
RTT code for the MCU is included in J-Link software and documentation pack in
This directory should be present in root directory of the project:
1 2 3 4 5 6
SUBMAKEFILES should also contain
Next I modified
main() to print a debugging statement at startup:
1 2 3 4 5 6 7 8
make the program can be run again under the debugger (do a
make clean first, if the build fails). Ozone now displays the message in bottom-right RTT terminal window!
EFM32 peripherals library - emlib & emdrv
Emlib and emdrv are libraries made by Silicon Labs that provide drivers for peripherals like USARTs, DMA, sleep modes etc.
Emlib is the lower-level driver library. Copy the
emdrv directories from the
platform directory to project directory. The only thing needed is a submakefile that will list all
.c files. As the libraries contain lots of
.c files it makes sense to slightly automate this step. I generate the list of files in Linux with:
This creates a file called
emlib.mk with all
.c files, including subdirectory paths.
emlib.mk can be opened with any text editor (eg. KWrite, Kate), newlines have to be replaced with a space + backslash + newline, first line must be
SOURCES := (just like in all other submakefiles).
emlib/emlib.mkhas to be added to
-I emlib/inchas to be added to
Emdrv is the higher-level driver library.
Stuff to remove:
tempdrvdirectory - this driver is not supported for EFM32GG
emdrv/configdirectory - it contains headers that will be removed in future releases of the library (your can read that in comments in those files). Drivers use their own headers in individual
As previously, I created
emdrv.mk with the
find command, redirection and manual editing in a text editor. There is a slight issue, because emdrv has header files scattered all around.
To get a list of directories with emdrv header files (to be added to
INCLUDE) I executed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
This command finds all header files within
emdrv directory, outputs only their directories, filters them to be unique and adds the variable name in front. The output can simply be pasted to
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
Final test - obligatory blinking LED
Every embedded project must feature a blinking LED :-) Here the LED is connected to PA4. This is
1 2 3 4 5 6 7 8 9 10 11 12 13
To make it blink you have to step and resume from the breakpoint with the debugger - it does not blink by itself but illustrates that the code is working and doing anything useful :-)
This project available on Github.