How to build a firmware using Makefile?

posted 4 min read

In embedded programming, we trust IDEs like STM32CubeIDE, Keil or so on. These IDEs give us just some buttons and UIs to build, flash, and debug the source code that we've written for a MCU. But, I think that every engineer that works on this field have to know that how these steps are made by hand.

In this tutorial, I will show building a firmware written for STM32F446RE MCU with just using a single Makefile.

Below is the project directory:

$ tree
.
├── bin/
├── drivers/
│   ├── CMSIS/
│   ├── STM32F446RETX_FLASH.ld
│   └── STM32F4xx_HAL_Driver/
├── lib/
├── Makefile
└── src/
    ├── it.c
    ├── main.c
    ├── main.h
    └── peripheral.c

Firstly, which libraries do we need to have?

  • HAL library

  • CMSIS library

  • Linker script

  • Startup/system code

  • arm-none-eabi-* tools

I've put the all required libraries/scripts into /drivers directory. Please download these using:

$ git clone --recurse-submodules https://github.com/STMicroelectronics/STM32CubeF4.git
$ sudo apt install gcc-arm-none-eabi

In here, HAL library is used to program the peripherals and CMSIS for ARM-Cortex itself. These are the most important libraries that we have to get.

Apart from that, we also need some special files. First is the linker script. It includes the memory layout of microcontroller. Sometimes, it comes from above command. But if you cannot find it there, please install it externally. Another file is the startup code. It's written in Assembly language. As its name suggests, it's the first code that runs in microcontroller. Basically, it includes the Reset_Handler and a branch that jumps into main() function in your source code. Also system code includes the some generic and general definitions.

After downloaded and explained the required ones, Let's write the Makefile step by step:

1. Toolchain definitions

CC        := arm-none-eabi-gcc
OBJCOPY:= arm-none-eabi-objcopy
SIZE:= arm-none-eabi-size
RM        := rm -f
FILE:= file

2. MCU-specific definitions

DEVICE_FAMILY     := STM32F4xx
DEVICE_MODEL     := STM32F446xx
DEVICE_VARIANT     := STM32F446RETx

3. Compiler and linker flags

CORTEX_FLAGS     := -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard
COMMON_FLAGS     := -g3 -Os -ffunction-sections -fdata-sections -Wall
AS_FLAGS             := -x assembler-with-cpp

In here, we've defined some flags:

  • -mthumb means that the firmware uses thumb (16-bit) instruction set.

  • -mcpu=cortex-m4 means the ARM-Cortex series itself.

  • -mfpu=fpv4-sp-d16 and -mfloat-abi=hard means that floating-point operations can be done in the firmware.

4. Include paths

MAIN_INC:= ./src
CMSIS_INC:= ./drivers/CMSIS/Include
CMSIS_DEV_INC:= ./drivers/CMSIS/Device/ST/STM32F4xx/Include
HAL_INC:= ./drivers/STM32F4xx_HAL_Driver/Inc
CMSIS_DSP_INC:= ./drivers/CMSIS/DSP/Include

In here, we specified the header files that are used in the source code.

5. Source and special files

MAIN_SRC:= $(wildcard ./src/*.c)
HAL_SRC:= $(wildcard ./drivers/STM32F4xx_HAL_Driver/Src/*.c)
SYSTEM_SRC:= ./drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/system_stm32f4xx.c
STARTUP_CODE:= ./drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/gcc/startup_stm32f446xx.S
LINKER_SCRIPT:= ./drivers/STM32F446RETX_FLASH.ld

6. Sorthands and build definitions

DEFINES:= -D$(DEVICE_FAMILY) -D$(DEVICE_MODEL) -D$(DEVICE_VARIANT) \
-DUSE_HAL_DRIVER

SOURCES:= $(MAIN_SRC) $(HAL_SRC) $(SYSTEM_SRC)
OBJECTS:= $(notdir $(patsubst %.c,%.o,$(SOURCES))) startup_stm32f446xx.o
INCLUDES:= -I$(MAIN_INC) -I$(CMSIS_DEV_INC) -I$(CMSIS_INC) -I$(HAL_INC) \
-I$(CMSIS_DSP_INC)

CFLAGS:= $(CORTEX_FLAGS) $(COMMON_FLAGS) $(DEFINES) $(INCLUDES)
AFLAGS:= $(CORTEX_FLAGS) $(AS_FLAGS) $(DEFINES) $(INCLUDES)
LDFLAGS:= $(CORTEX_FLAGS) -T $(LINKER_SCRIPT) \
-Wl,--gc-sections,--relax --specs=nano.specs --specs=nosys.specs \
   -Wl,--start-group -lc -lm -lnosys -Wl,--end-group

In here, I mostly did the string operations and manipulations. But some points are interesting and need some explanations:

  • Firstly, I've grouped the sources and libraries and converted the source file suffixes into object file formats.

  • Also, I've created three total flags that will be used for compiling, assembling, and linking phases.

  • In linker flags, I put the linker script. As you noticed, this step will be done after the compiling the source files. I also linked the some libraries. -lc is the standard C library. In firmware, we use often string manipulation functions (strlen(), strcat(), strcpy() or so on), snprint(), memset(), or similar ones. -lm is the math library as you know. -lnosys means that I'm just implementing the bare-metal firmware, there is no such as kernel or fully OS.

7. Output files

FIRMWARE_ELF:= firmware.elf
FIRMWARE_BIN:= firmware.bin

Generally, we use the ELF file when flashing and debugging. ELF executable includes the both machine code, metadata, debug information, symbol table or similar stuffs. In contrast, BIN executable just includes the machine code. If you look at the size of both, you will use the ELF executable is much bigger!

8. The recipe

.PHONY: all

all:
@echo "-------------------------------------"
@echo "----- Building the source files -----"
@echo "-------------------------------------"
@$(CC) $(AFLAGS) -c $(STARTUP_CODE)
@$(CC) $(CFLAGS) -c $(SOURCES)

@echo "\n------------------------------------"
@echo "----- Linking the object files -----"
@echo "------------------------------------"
@$(CC) $(LDFLAGS) $(OBJECTS) -o $(FIRMWARE_ELF)
@$(OBJCOPY) -O binary $(FIRMWARE_ELF) $(FIRMWARE_BIN)
@$(RM) $(OBJECTS)

@echo "\n-------------------------------------"
@echo "----- The firmware memory usage -----"
@echo "-------------------------------------"
@$(SIZE) $(FIRMWARE_ELF)

@echo "\n-------------------------------------"
@echo "----- The firmware binary format ----"
@echo "-------------------------------------"
@$(FILE) $(FIRMWARE_ELF)

Lastly, run the Makefile:

$ make

That's it . You've built the both firmware.elf and firmware.bin.

You are ready to flash the built firmware into microcontroller with this or similar command (over ST-Link connection):

$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program firmware.elf verify reset exit"

I've also published a repository for that. You can look at it from here: https://github.com/CanGulmez/Embedded-Firmware-with-Makefile

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

Flashing and Debugging A Firmware in Detail

CanGulmez - Apr 6

Interrupt Management in ARM Cortex-M

CanGulmez - Apr 6

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

POSIX Shared Memory

CanGulmez - Apr 8
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

2 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!