How to build a firmware using Makefile?
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
-mthumbUse the Thumb instruction set.-mcpu=cortex-m4Target the Cortex‑M4 core.-mfpu=fpv4-sp-d16and-mfloat-abi=hardEnable 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
SOURCESgathers all.cfiles;OBJECTSconverts them to.onames.CFLAGS,AFLAGS, andLDFLAGSare used for compilation, assembly, and linking respectively.LDFLAGSlinks 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
- Compilation – each
.cfile is compiled with$(CFLAGS)into an object file. - Assembly – the startup assembly file is assembled with
$(AFLAGS). - Linking – all objects are linked using
$(LDFLAGS)and the provided linker script, producingfirmware.elf. - Binary conversion –
objcopyconverts the ELF into a raw binary (firmware.bin). - Flashing – the optional
flashtarget demonstrates how to program the MCU (here usingst-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).