Bare-Metal ARM Cortex-M Firmware Development with GCC and MakefilesMonday, March 11, 2019 Posted in Embedded Systems
Tags : arm cortex-m gcc make tm4c microcontrollers linux
This post is a guide to using the GCC package and the GNU Make utility along with basic code organization, to produce usable bare-metal firmware binaries for ARM Cortex-M Microcontrollers. This guide is based on the TI TM4C123GXL ARM Cortex-M4 microcontrollers, the compiler flags may vary depending on your cortex-m version and manufacturer
Why GCC and Make?
Most Embedded Systems courses world-wide are taught using IDE’s like Keil or IAR, but these tools, are propreitary with expensive upfront costs for a license, or restrictions on their usage in their trial edition form, they are also complex to setup, and lead to vendor lock-in, also they are Windows-only, with no versions for other operating systems.Which is why i decided to use GCC and Make for firmware development for ARM MCU’s, the Make + GCC combination is effective, free, open source, has decent documentation and is the only competent alternative to propreitary IDEs. GCC, Makefiles, GDB and OpenOCD or J-Link are packaged into a ready made Eclipse IDE, but that is not the topic of this post.
What is GCC?
GCC stands for GNU Compiler Collection and as its name states is a compiler package for the C and C++ programming languages, and apart from being a compiler, it also has an Assembler GNU
as and a Linker called GNU
ld, GCC is the standard compiler package for Linux and BSD variants on x86 PC’s , but we will be using the ARM microcontroller specific version in this guide.
What is Make/makefile ?
GNU Make is a build automation program that is pre-installed on most Linux variants.
We are using Make because it can build an executable binary using GCC and various source and header files in different folder automatically, using the commands specified in a
makefile, We can then build our executable simply by calling
make in the directory of our
First we install the GCC Package for ARM microcontrollers
sudo apt install arm-none-eabi-gcc
Next, we create a sane directory structure to organize our C source, C libraries, Header files, Linker Script and Assembly code files( if any ), to make development easier.
mkdir libs ld src inc
libsshould have the
*.clibrary files for peripheral initialization and utilization by other programs
*.hHeader files, containing Register definitions and startup code
srchas the main program logic
.ldlinker script specific to this TM4C chip
I already have a github repo containing a ready make directory setup like this with a basic RGB Blinky C Program and the necessary startup code and linker script files, you can get it from github like so.
git clone https://github.com/shawn-dsilva/tm4c-linux-template.git
Since the focus of this guide is writing a Makefile, delete the makefile from this cloned repo.
Writing the Makefile
This makefile will be primarily three parts, the variables and macros needed for this makefile, the compiler and linker flags needed for compilation, and the rules needed to build the final binary using these variables.
Basic Variables and Macros
First off, we start with basic variables
PROJECT = main MCU = TM4C123GH6PM
PROJECT is just what you want your final binary to be named, in my case it is just
MCU here specifies the part number of your MCU, this is fed to the GCC compiler later on
Next, we move on to setting up the macros pointing to the directories and files which are used to build the executable.
SRCS = $(wildcard src/*.c) \ $(wildcard libs/*.c) OBJ = obj/ OBJS = $(addprefix $(OBJ),$(notdir $(SRCS:.c=.o))) LD_SCRIPT = ld/$(MCU).ld
SRCShere points to a wildcard function, which selects every
.cfile in the
OBJhere points to the
objdirectory where our
.oobject files and
.ddependency files will go
OBJSIs a list of object files, but this leads to a complex function which is divided into two parts, the
$(SRCS:.c=.o)marco says that
OBJShave the same name as the
.cfiles in the
SRCSmacro,except with the
.osuffix. Next, the
addprefixfunction here is used to put the new
.ofiles into the
objfolder instead of leaving them in the
INCis a compiler flag
-Iwhich points to the
incfolder containing our
LD_SCRIPTpoints to the linker script for this MCU
The following are just some variables for the compiler and related utilities we will be using in the build-rules later on, these variables reduce unnecessary repetition.
CC = arm-none-eabi-gcc # C Compiler LD = arm-none-eabi-ld # Linker OBJCOPY = arm-none-eabi-objcopy # Final Binary Builder FLASHER = lm4flash # Flashing utility RM = rm -rf # Remove recursively command MKDIR = @mkdir -p $(@D) # Creates folders if not present
Next we move on to our compiler flags.
Variables of Compiler and Linker flags
Since there are various ARM Cortex-M MCU models,and even more manufacturer variants, there will contain some specific GCC flags related to the TM4C123G this makefile is written for, if you use a different microcontroller, it may be different for you
CFLAGS = -ggdb -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16
The flags here tell the compiler to
- add GDB debug symbols(
- compile using the ARM Thumb instruction set(
- using the Cortex-M4 microcontroller(
- specify the floating point unit used in this M4 microcontroller(
CFLAGS += -mfloat-abi=softfp -Os -MD -std=c99 -c
We use the
+= operator here to concatenate these additional commands to the CFLAGS variable, not overwrite them with an
- The first flag
Ostells the compiler to enable all optimizations that do not increase code size.
.dtext files in the
.objdirectory, these are simply text files about the dependencies of each file.
-std=c99tells the compiler to use the C99 standard, and finally,
-ctells it to compile without linking the files, leading to object files instead of an executable.
Now we move on to using
ld to link all our object files into an excutable
LDFLAGS = -T $(LD_SCRIPT) -e Reset_Handler
-Ttells the compiler to use the
.ldlinker script specified after it, in this case it is the variable $(LD_SCRIPT),
-e Reset_Handlerstands for program entry point, which is pointed to the Reset_Handler function in the interrupt vector table, this means that the program runs upon reseting of the microcontroller.
Now we are on the final stage, where we use our defined macros and variables to build our executable
The rules work in a simple fashion, a
target on the left side of the colon
: is built using the
dependencies on the right side of the colon
all is the default target, which runs when you type
make in your shell.
all, it needs the
bin/$(PROJECT).bin file, which will be made using the below
$(OBJ)%.o: src/%.c $(MKDIR) $(CC) -o $@ $^ $(INC) $(CFLAGS) $(OBJ)%.o: libs/%.c $(MKDIR) $(CC) -o $@ $^ $(INC) $(CFLAGS)
%.c here means that for every
.c source file in the
libs directories, an object file is being created in the
obj directory with the same name,
$@ refers to the left side of the colon, that is the target name, and
$^ refers to the right side of the colon that is the dependencies
for example this
$(CC) -o $@ $^ $(INC) $(CFLAGS) line expands to
` arm-none-eabi-gcc -o main.o main.c -Iinc -ggdb -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=softfp -Os -MD -std=c99 -c `
in the shell, assuming
main.c is the file being compiled here
$(MKDIR) here creates an
obj directory if it isn’t already present
bin/$(PROJECT).elf: $(OBJS) $(MKDIR) $(LD) -o $@ $^ $(LDFLAGS)
This is the stage where we build the executable with GDB debug symbols attached(
*.elf) by Linking our Object files.
$(OBJS) variable as desribed earlier is a list of objects in the
obj folder, which will all be linked to make our
$(PROJECT.elf) binary in the
$(MKDIR) here creates a
bin directory if it isn’t already present
bin/$(PROJECT).bin: bin/$(PROJECT).elf $(OBJCOPY) -O binary $@ $<
This is the last stage, where another executable is made, stripped of all the GDB debug symbols, this is the binary that will be flashed to our ARM microcontroller
$< means to select the first element of the dependencies on the right side of the colon, and since we have only one dependency, this is the expression we use here.
clean: -$(RM) obj -$(RM) bin
This command removes the
bin folders, leaving only our source code folders and header files intact, for fresh compilation
.PHONY: all clean
.PHONY macro here specifies that the elements to the right of the colon are not actual targets to be built, but pseudo targets, this enables to use
clean as commands with
rather than having any executable build for
flash: $(FLASHER) -S $(DEV) bin/$(PROJECT).bin
This is an optional
make flash command that uses the unofficial
lm4tools flashing utility for the TM4C123G microcontrollers, other brands like STM have the
st-link utility, altough i recommend OpenOCD for flashing.You can read my guide to flashing using OpenOCD here.
Makefile should look like this
PROJECT = main # SRCS: all source files from src directory SRCS = $(wildcard src/*.c) \ $(wildcard libs/*.c) OBJ = obj/ # OBJS: list of object files OBJS = $(addprefix $(OBJ),$(notdir $(SRCS:.c=.o))) #Flag points to the INC folder containing header files INC = -Iinc # LD_SCRIPT: linker script LD_SCRIPT = ld/$(MCU).ld #UTILITY VARIABLES CC = arm-none-eabi-gcc #compiler LD = arm-none-eabi-ld #linker OBJCOPY = arm-none-eabi-objcopy #final executable builder FLASHER = lm4flash #flashing utility RM = rm -rf MKDIR = @mkdir -p $(@D) #creates folders if not present #GCC FLAGS CFLAGS = -ggdb -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 CFLAGS += -mfloat-abi=softfp -Os -MD -std=c99 -c #LINKER FLAGS LDFLAGS = -T $(LD_SCRIPT) -e Reset_Handler # Rules to build bin all: bin/$(PROJECT).bin $(OBJ)%.o: src/%.c $(MKDIR) $(CC) -o $@ $^ $(INC) $(CFLAGS) $(OBJ)%.o: libs/%.c $(MKDIR) $(CC) -o $@ $^ $(INC) $(CFLAGS) bin/$(PROJECT).elf: $(OBJS) $(MKDIR) $(LD) -o $@ $^ $(LDFLAGS) bin/$(PROJECT).bin: bin/$(PROJECT).elf $(OBJCOPY) -O binary $< $@ #Flashes bin to TM4C flash: $(FLASHER) -S $(DEV) bin/$(PROJECT).bin #remove object and bin files clean: -$(RM) obj -$(RM) bin .PHONY: all clean