M0AGX / LB9MG

Amateur radio and embedded systems

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):

  1. Will the IDE still still run on my operating system in 10 years from now?
  2. Will the IDE still support my (obsolete) chip after 10 years?
  3. Will this particular SDK and libraries still be compatible with rest of my project after 10 years?
  4. Where do I download all this stuff from after 10 years?
  5. If only the latest version of the tools is available - will it open my project at all?
  6. Will the code compile with a new toolchain?
  7. 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?

War stories

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

  1. You control the whole situation - the libraries, the compilers, the scripts (and also have to carry out the maintenance yourself!).
  2. make is a standard tool that has proven its worth (and very likely will still be used in the future).
  3. 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 :-)
  4. An automated project can use a build server common to all developers (less surprises with code that "works on my machine"™).
  5. An automated project can use automatic testing (for example: unit tests, hardware-in-the-loop testing).
  6. 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

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.

Directory structure

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
.
|-- firmware
|   |-- CMSIS
|   |-- SEGGER
|   |-- emdrv
|   |-- emlib
|   |-- include
|   |-- linker_script
|   |-- main
|   `-- startup_code

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

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 linker_script directory.

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

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.

Startup code

Before the 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 headers

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 platform/CMSIS/Include to CMSIS/Include in my project.

Makefile

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 Makefile.

This is a minimal main.mk:

 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
CC = arm-none-eabi-gcc
SIZE = arm-none-eabi-size
OBJCOPY = arm-none-eabi-objcopy

COMMON_FLAGS = -mcpu=cortex-m3 -mthumb -mfloat-abi=soft -g3

DEFINES = -DEFM32GG332F512

INCLUDE += -I include -I CMSIS/Include -I ./

WARNINGS := -Wall -Wimplicit-fallthrough -Wextra -Wfloat-equal -Wshadow

CFLAGS := $(COMMON_FLAGS) $(INCLUDE) $(DEFINES) $(WARNINGS) -O3 -std=gnu11 -pipe -ffunction-sections -fdata-sections

SUBMAKEFILES := main/main.mk startup_code/startup_code.mk

BUILD_DIR  := build
TARGET_DIR := build_output

#nosys.specs are required for printf
LDFLAGS = $(COMMON_FLAGS) -T linker_script/efm32gg.ld --specs=nosys.specs -Wl,--gc-sections

TARGET := main.elf

size: $(TARGET_DIR)/main.elf
    $(SIZE) $(TARGET_DIR)/main.elf

This makefile contains:

  • compiler, size and objcopy commands (regular gcc names + ARM prefix)
  • COMMON_FLAGS apply 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 #include "../CMSIS/Include/core_cm3.h"
  • 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_assert and 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 .c files to compile (keep reading for full explanation).
  • BUILD_DIR and 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 objcopy
  • ...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.

First submakefile startup_code/startup_code.mk :

1
SOURCES := startup_efm32gg.c system_efm32gg.c

Second submakefile startup_code/startup_code.mk :

1
2
SOURCES :=   
   main.c

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 .mk files.

I also created a dummy main/main.c file with the following contents:

1
2
3
4
5
6
int main(void){
    while(1){
        asm("bkpt #1");
    }
    return 0;
}

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.

First build

To build: simply type make (or make size) in the root directory of the project. Output should be similar to this:

1
2
3
4
5
6
7
arm-none-eabi-gcc -o build/main.elf/main/main.o -c -MD -mcpu=cortex-m3 -mthumb -mfloat-abi=soft -g3 -I include -I CMSIS/Include -DEFM32GG332F512 -Wall -Wimplicit-fallthrough -Wextra -Wfloat-equal -Wshadow -O3 -std=gnu11 -pipe -ffunction-sections -fdata-sections main/main.c
arm-none-eabi-gcc -o build/main.elf/startup_code/startup_efm32gg.o -c -MD -mcpu=cortex-m3 -mthumb -mfloat-abi=soft -g3 -I include -I CMSIS/Include -DEFM32GG332F512 -Wall -Wimplicit-fallthrough -Wextra -Wfloat-equal -Wshadow -O3 -std=gnu11 -pipe -ffunction-sections -fdata-sections startup_code/startup_efm32gg.c
arm-none-eabi-gcc -o build/main.elf/startup_code/system_efm32gg.o -c -MD -mcpu=cortex-m3 -mthumb -mfloat-abi=soft -g3 -I include -I CMSIS/Include -DEFM32GG332F512 -Wall -Wimplicit-fallthrough -Wextra -Wfloat-equal -Wshadow -O3 -std=gnu11 -pipe -ffunction-sections -fdata-sections startup_code/system_efm32gg.c
arm-none-eabi-gcc -o build_output/main.elf -mcpu=cortex-m3 -mthumb -mfloat-abi=soft -g3 -T linker_script/efm32gg.ld --specs=nosys.specs -Wl,--gc-sections build/main.elf/main/main.o build/main.elf/startup_code/startup_efm32gg.o build/main.elf/startup_code/system_efm32gg.o
arm-none-eabi-size build_output/main.elf
   text    data     bss     dec     hex filename
   1508    1080      64    2652     a5c build_output/main.elf

Great! Now 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! :-)

Ozone screenshot

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.

SEGGER RTT

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 Samples/RTT.

This directory should be present in root directory of the project:

1
2
3
4
5
6
|-- SEGGER
|   |-- SEGGER.mk
|   |-- SEGGER_RTT.c
|   |-- SEGGER_RTT.h
|   |-- SEGGER_RTT_Conf.h
|   `-- SEGGER_RTT_printf.c

SEGGER.mk contains:

1
2
SOURCES := SEGGER_RTT.c SEGGER_RTT_printf.c
CFLAGS += -I../

and in main.mk the SUBMAKEFILES should also contain SEGGER/SEGGER.mk.

Next I modified main() to print a debugging statement at startup:

1
2
3
4
5
6
7
8
#include <SEGGER/SEGGER_RTT.h>
int main(void){
    SEGGER_RTT_WriteString(0, "Hello from EFM32!");
    while(1){
        asm("bkpt #1");
    }
    return 0;
}

After typing 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!

Ozone screenshot

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

Emlib is the lower-level driver library. Copy the emlib and 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:

1
2
firmware $ cd emlib/
emlib $ find ./ -name '*.c' > emlib.mk

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).

Last steps:

  • emlib/emlib.mk has to be added to SUBMAKEFILES in main.mk
  • -I emlib/inc has to be added to INCLUDE in main.mk

emdrv

Emdrv is the higher-level driver library.

Stuff to remove:

  • tempdrv directory - this driver is not supported for EFM32GG
  • emdrv/config directory - 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 config directories.

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 main.mk's INCLUDE) I executed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
firmware $ find ./emdrv/ -name '*.h' | xargs dirname | uniq | awk '{print "INCLUDE += -I" $0}'
INCLUDE += -I./emdrv/common/inc
INCLUDE += -I./emdrv/dmadrv/config
INCLUDE += -I./emdrv/dmadrv/inc
INCLUDE += -I./emdrv/gpiointerrupt/inc
INCLUDE += -I./emdrv/nvm/config
INCLUDE += -I./emdrv/nvm/inc
INCLUDE += -I./emdrv/rtcdrv/config
INCLUDE += -I./emdrv/rtcdrv/inc
INCLUDE += -I./emdrv/sleep/inc
INCLUDE += -I./emdrv/spidrv/config
INCLUDE += -I./emdrv/spidrv/inc
INCLUDE += -I./emdrv/tempdrv/config
INCLUDE += -I./emdrv/tempdrv/inc
INCLUDE += -I./emdrv/uartdrv/config
INCLUDE += -I./emdrv/uartdrv/inc
INCLUDE += -I./emdrv/ustimer/config
INCLUDE += -I./emdrv/ustimer/inc

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 main.mk.

Final makefile

 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
CC = arm-none-eabi-gcc
SIZE = arm-none-eabi-size
OBJCOPY = arm-none-eabi-objcopy

COMMON_FLAGS = -mcpu=cortex-m3 -mthumb -mfloat-abi=soft -g3

DEFINES = -DEFM32GG332F512

INCLUDE += -I include -I CMSIS/Include -I ./ -I emlib/inc
INCLUDE += -I./emdrv/common/inc
INCLUDE += -I./emdrv/dmadrv/config
INCLUDE += -I./emdrv/dmadrv/inc
INCLUDE += -I./emdrv/gpiointerrupt/inc
INCLUDE += -I./emdrv/nvm/config
INCLUDE += -I./emdrv/nvm/inc
INCLUDE += -I./emdrv/rtcdrv/config
INCLUDE += -I./emdrv/rtcdrv/inc
INCLUDE += -I./emdrv/sleep/inc
INCLUDE += -I./emdrv/spidrv/config
INCLUDE += -I./emdrv/spidrv/inc
INCLUDE += -I./emdrv/tempdrv/config
INCLUDE += -I./emdrv/tempdrv/inc
INCLUDE += -I./emdrv/uartdrv/config
INCLUDE += -I./emdrv/uartdrv/inc
INCLUDE += -I./emdrv/ustimer/config
INCLUDE += -I./emdrv/ustimer/inc

WARNINGS := -Wall -Wimplicit-fallthrough -Wextra -Wfloat-equal -Wshadow

CFLAGS := $(COMMON_FLAGS) $(INCLUDE) $(DEFINES) $(WARNINGS) -O3 -std=gnu11 -pipe -ffunction-sections -fdata-sections

SUBMAKEFILES := main/main.mk startup_code/startup_code.mk SEGGER/SEGGER.mk emlib/emlib.mk emdrv/emdrv.mk

BUILD_DIR  := build

TARGET_DIR := build_output

#nosys.specs are required for printf
LDFLAGS = $(COMMON_FLAGS) -T linker_script/efm32gg.ld --specs=nosys.specs -Wl,--gc-sections

TARGET := main.elf

size: $(TARGET_DIR)/main.elf
    $(SIZE) $(TARGET_DIR)/main.elf

Final test - obligatory blinking LED

Every embedded project must feature a blinking LED :-) Here the LED is connected to PA4. This is main.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <SEGGER/SEGGER_RTT.h>
#include <em_cmu.h> //clock library
#include <em_gpio.h>
int main(void){
    SEGGER_RTT_WriteString(0, "Hello from EFM32!");
    CMU_ClockEnable(cmuClock_GPIO, true);
    GPIO_PinModeSet(gpioPortA, 4/*pin 4*/, gpioModePushPull /*push-pull output*/, 1/*output level*/);
    while(1){
        asm("bkpt #1");
        GPIO_PinOutToggle(gpioPortA, 4/*pin 4*/);
    }
    return 0;
}

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.