How to build a firmware using Makefile?

Published: (December 21, 2025 at 07:46 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Project Layout

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

Required Libraries & Tools

  • HAL library – peripheral drivers.
  • CMSIS library – Cortex‑M core support.
  • Linker script – memory layout of the MCU.
  • Startup / system code – assembly reset handler and system initialization.
  • arm‑none‑eabi‑* toolchain (gcc, objcopy, size, …).

All required libraries and scripts are placed under the drivers directory. You can obtain them with:

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

The HAL library is used to program peripherals, while CMSIS provides the core definitions.

Makefile Walk‑through

Below is the complete Makefile, explained section by section.

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

Explanation

  • -mthumb      Use the Thumb instruction set.
  • -mcpu=cortex-m4  Target the Cortex‑M4 core.
  • -mfpu=fpv4-sp-d16 and -mfloat-abi=hard Enable hardware floating‑point.

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

These directories contain the header files referenced by 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. Shorthands 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

Key points

  • SOURCES gathers all .c files; OBJECTS converts them to .o names.
  • CFLAGS, AFLAGS, and LDFLAGS are used for compilation, assembly, and linking respectively.
  • LDFLAGS links the newlib‑nano C library (-lc), the math library (-lm), and the nosys stub library (-lnosys) because the firmware runs on bare metal (no OS).

7. Output files

FIRMWARE_ELF := firmware.elf
FIRMWARE_BIN := firmware.bin
  • ELF – contains machine code, debug information, symbol tables, etc.; used for flashing and debugging.
  • BIN – raw binary image, suitable for programming the flash directly.

8. Build rules

.PHONY: all clean flash

all: $(FIRMWARE_ELF) $(FIRMWARE_BIN)

$(FIRMWARE_ELF): $(OBJECTS)
	$(CC) $(LDFLAGS) -o $@ $^

$(FIRMWARE_BIN): $(FIRMWARE_ELF)
	$(OBJCOPY) -O binary $< $@

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

startup_stm32f446xx.o: $(STARTUP_CODE)
	$(CC) $(AFLAGS) -c -o $@ $<

clean:
	$(RM) *.o $(FIRMWARE_ELF) $(FIRMWARE_BIN)

# Example flash target (adjust the programmer/command as needed)
flash: $(FIRMWARE_BIN)
	st-flash write $(FIRMWARE_BIN) 0x08000000

How It Works

  1. Compilation – each .c file is compiled with $(CFLAGS) into an object file.
  2. Assembly – the startup assembly file is assembled with $(AFLAGS).
  3. Linking – all objects are linked using $(LDFLAGS) and the provided linker script, producing firmware.elf.
  4. Binary conversionobjcopy converts the ELF into a raw binary (firmware.bin).
  5. Flashing – the optional flash target demonstrates how to program the MCU (here using st-flash; replace with your programmer as needed).

Conclusion

With this single Makefile you can:

  • Compile, link, and generate both ELF and raw binary images.
  • Keep the build process completely transparent—no hidden IDE magic.
  • Easily adapt the file list, compiler flags, or target MCU.

Understanding each step gives you full control over the build process, which is invaluable when debugging low‑level issues or when working in environments where a full IDE is unavailable. Happy hacking!

The machine code
If you look at the size of both, you will see the ELF executable is much bigger!

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

Build the firmware

$ make

That’s it 🥳🥳🥳. You’ve built both firmware.elf and firmware.bin.

Flash the firmware

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

You are now ready to flash the built firmware into the microcontroller (using an ST‑Link connection or a similar tool).

Back to Blog

Related posts

Read more »