Remembering or copying the build commands isn't going to be a usable way to build the example project in this series. So we need a makefile to automate this task. While make isn't the nicest tool it's basic working is well understood and it's still the defacto standard.

This will not be a tutorial for make. But i try to explain at least some basic points. First lets start with a makefile that just replicates what we previously did manually. Adding support for C and assembler files for completeness.

C_SRC = 

CXX_SRC = main.cpp



INCLUDEDIRS = -iquote . -I common/stm_include -I common/cmsis_include

PROJECT = minblink

LD_SCRIPT = linkerscript.ld

The first part is the section that defines the variables which contain the source files to process and similar project specific configuration. It starts with the variables for all the source files: C_SRC for plain c files, CXX_SRC for c++ files and ASM_SRC for assembler source files(using .S as file extension).

Alternativly you can move up the OBJ_FILES line and add the resulting .o files there directly without the code to replace the source extensions by .o. In that case you can remove the *_SRC lines. But as this is setup to output everything in out/ you either need to add that explicitly or keep the transformation with addprefix.

After that are project global settings. PROJECT defines the name of the output files. INCLUDEDIRS specifies the gcc/g++ parameters that relate to the include file directories. DEFINES allows specifing macros that should be defined using the usual -DVAR=VALUE syntax. LD_SCRIPT specifies the name of the linker script to use.

TARGET = arm-none-eabi-


CC = $(TARGET)gcc
CXX = $(TARGET)g++
AS = $(TARGET)gcc -x assembler-with-cpp
OBJCOPY = $(TARGET)objcopy

This section defines the output path and shortcut variables for all used toolchain components.

CPUFLAGS = -mcpu=cortex-m3 -mthumb

         -std=gnu99 $(INCLUDEDIRS)

         -fno-rtti -fno-exceptions \
         -std=gnu++14 $(INCLUDEDIRS)


LDFLAGS = $(CPUFLAGS) -T$(LD_SCRIPT) -nostartfiles

-mcpu=cortex-m3 -mthumb -O2 -fno-rtti -fno-exceptions --std=c++14 are the g++ commandline options from the first part. The linker options are the same too.

New is -MD -MP -MF $(@:.o=.d). It instructs gcc to output a file with all dependencies of the currently compiled source in a format that is directly includable in a makefile. This is needed for automatically rebuilding the affected object files when a included header file is changed.

OBJ_FILES = $(addprefix $(OUT)/, $(notdir $(ASM_SRC:.S=.o)))  \
            $(addprefix $(OUT)/, $(notdir $(C_SRC:.c=.o)))    \
            $(addprefix $(OUT)/, $(notdir $(CXX_SRC:.cpp=.o)))
DEPS = $(OBJ_FILES:.o=.d)

Here all source files from the variables defined at the beginning are gathered and transformed into the object file names that will generated for them. These are used as dependencies for linking. Also for make to automate rebuilding objects when included files change make needs files with dependency information. So DEPS is a list of all these dependency files.

# unconditionally ensure output directory
$(shell test -d $(OUT) || mkdir $(OUT))

It’s needed later. So make sure it exists.

all: $(OUT)/$(PROJECT).bin

    rm -f $(OUT)/*

Define the default target all that just depends on the output of the objdump below. And also define a clean target that just removes all files from out/.

$(OUT)/%.o: %.c Makefile
    @echo CC $<
    @$(CC) -c $(CFLAGS) $< -o $@

$(OUT)/%.o: %.cpp Makefile
    @echo CXX $<
    @$(CXX) -c $(CXXFLAGS) $< -o $@

$(OUT)/%.o: %.S Makefile
    $(AS) -c $(ASFLAGS) $< -o $@

Rules for to transform all supported kinds of source to object files.

$(OUT)/$(PROJECT).elf: $(OBJ_FILES) Makefile $(LD_SCRIPT)
    $(CXX) $(LDFLAGS) $(OBJ_FILES) -o $@

$(OUT)/$(PROJECT).bin: $(OUT)/$(PROJECT).elf Makefile
    $(OBJCOPY) -O binary $< $@

These are the linking and objcopy step for the very first post in this series.

-include $(DEPS)

It’s traditional to add the include for the dependency files at the end.

Now i like to add more features to help understanding my code and to avoid possible problems.

@@ -23,6 +24,6 @@
 OBJCOPY = $(TARGET)objcopy

 CPUFLAGS = -mcpu=cortex-m3 -mthumb
+COMMONFLAGS = $(CPUFLAGS) -g -ggdb3 -Wa,-amhlsd=$(@:.o=.lst)  -MD -MP -MF $(@:.o=.d) $(DEFINES)


Generation of debug information (-g -ggdb3) is always a good idea. Even without an debug cable for the used hardware it allows running tools like pa-hole that use debug information for further analysis.

The other part (-Wa,-amhlsd=$(@:.o=.lst)) generates a assembler listing files with annotations for every source file. I tend to look into these quite a bit when working on low-level code. While the compiler is free to produce different code, it helps me quite a bit with understanding where i might have parts in my code that are not supposed to be that way. For example it’s quite easy to constrain the compiler from doing useful and safe optimisations, seeing the assembler often leads to a easy fix back in the C++ code.

@@ -26,11 +26,13 @@ CPUFLAGS = -mcpu=cortex-m3 -mthumb

-         -std=gnu99 $(INCLUDEDIRS)
+         -Wall -Werror=strict-prototypes -Wextra -Werror=return-type \
+         -std=gnu99 -fstack-usage -fverbose-asm $(INCLUDEDIRS)

+         -Wall -Wextra  -Werror=return-type \
          -fno-rtti -fno-exceptions \
-         -std=gnu++14 $(INCLUDEDIRS)
+         -std=gnu++14 -fstack-usage -fverbose-asm $(INCLUDEDIRS)


Also a good idea is to enable warnings and even promote some warnings to errors.

  • -Werror=strict-prototypes (C only): Yes, prototypes without argument specifications should never happen.
  • -Werror=return-type: Not returning a value from a function declared to have one is bug, not just a warning.
  • -Wall -Wextra: enable useful warnings and errors.

  • -fstack-usage: Let the compiler calculate stack usage for every function.

  • -fverbose-asm: Output more information in the assembler listings.
-LDFLAGS = $(CPUFLAGS) -T$(LD_SCRIPT) -nostartfiles
+LDFLAGS = $(CPUFLAGS) -T$(LD_SCRIPT) -nostartfiles -g \
+          -Wl,-Map=$(OUT)/$(PROJECT).map,--cref -Wl,--print-memory-usage -Wl,--warn-common
  • -Wl,-Map=$(OUT)/$(PROJECT).map,--cref: instruct the linker to generate a Mapfile with cross reference table.
  • -Wl,--print-memory-usage: Often there is a need to keep an eye on the size of the program to still fit into the hardware’s limits. This prints a summary of the used space as assigned by the linker. (At the time of writing this needed a fairly recent version of binutils, if you have trouble you might need to remove it)
  • -Wl,--warn-common: Warn about sloppy usage of "extern".
OBJDUMP = $(TARGET)objdump
-all: $(OUT)/$(PROJECT).bin
+all: $(OUT)/$(PROJECT).bin $(OUT)/$(PROJECT).lss $(OUT)/$(PROJECT).dmp
$(OUT)/$(PROJECT).lss: $(OUT)/$(PROJECT).elf Makefile
    $(OBJDUMP) -Slw $< > $@

$(OUT)/$(PROJECT).dmp: $(OUT)/$(PROJECT).elf
    $(OBJDUMP) -x --syms $< > $@

Generates some additional information that can help when trying to find out how much space is used by different parts of the program (.lss and .map) and what is actually included in the final linked program.

For full code see here

more in this series:
5. Enabling C/C++ features
6. Heap, malloc and new
7. STM32 PLL and Crystal Oscillator
8. Interrupts
STM32 from scratch, serial »
« Enabling C/C++ features