Build Systems
Makefiles
Make is the original C build tool, created in 1976. STM32CubeIDE uses it internally. Understanding Makefiles makes you a better embedded engineer — even if you primarily use CMake and Ninja day-to-day.
The Golden Rule
TABs — not spaces
The indentation before every recipe line MUST be a real TAB character (\t). If you use spaces, Make will fail with missing separator. This is the most common Make error.
Anatomy of a Makefile
A Makefile is made of rules. Each rule has three parts:
# target: dependency1 dependency2 ...
# <TAB>recipe
hello: main.c
gcc main.c -o hello
- Target — the file to produce, or a phony action name
- Dependencies — files the target depends on
- Recipe — the shell command to run (indented with TAB)
How Make Decides What to Rebuild
Make compares timestamps: if a dependency is newer than its target, the target is stale and Make re-runs the recipe.
main.c modified at 10:05
main.o created at 10:00 → main.o is STALE → recompile
main.o created at 10:10 → main.o is UP TO DATE → skip
This is why a 3-minute full build becomes a 2-second incremental build after changing one file.
Example 1 — Single File (Desktop)
# Simplest possible Makefile
CC = gcc
CFLAGS = -Wall -O2
hello: main.c
$(CC) $(CFLAGS) main.c -o hello
clean:
rm -f hello
make # Builds hello
make clean # Removes hello
make hello # Explicitly build the hello target
Example 2 — Multiple Files with Object Files (Desktop)
Compiling each .c file into a .o first, then linking, is faster because Make can skip unchanged files.
CC = gcc
CFLAGS = -Wall -O2 -Iinclude
TARGET = sensor_app
# List all object files
OBJS = main.o sensor.o uart.o
# Link: final binary depends on all object files
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
# Pattern rule: compile ANY .c file into a .o file
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Phony targets are not real files
.PHONY: clean all
clean:
rm -f $(OBJS) $(TARGET)
all: $(TARGET)
Automatic Variables
| Variable | Meaning |
|---|---|
$@ | The target name (left side of :) |
$< | The first dependency (first item right of :) |
$^ | All dependencies |
$* | The stem matched by % in a pattern rule |
Example 3 — Multi-Directory Project (Desktop Library)
Real projects separate source files, headers, and build output into different directories.
project/
├── include/
│ ├── sensor.h
│ └── uart.h
├── src/
│ ├── main.c
│ ├── sensor.c
│ └── uart.c
├── build/
└── Makefile
CC = gcc
CFLAGS = -Wall -O2 -Iinclude
TARGET = build/sensor_app
SRC_DIR = src
OBJ_DIR = build
# Auto-discover all .c files in src/
SRCS = $(wildcard $(SRC_DIR)/*.c)
# Replace src/*.c with build/*.o
OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS))
# Final binary
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
# Compile each .c to build/*.o, creating build/ if needed
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -rf $(OBJ_DIR)
Example 4 — Building a Static Library
Packaging your code as a static library .a so other projects can link against it.
CC = gcc
AR = ar
CFLAGS = -Wall -O2 -Iinclude
# Build object file
sensor.o: src/sensor.c
$(CC) $(CFLAGS) -c src/sensor.c -o sensor.o
# Archive into a static library
libsensor.a: sensor.o
$(AR) rcs libsensor.a sensor.o
# Link main app against the library
app: src/main.c libsensor.a
$(CC) $(CFLAGS) src/main.c -L. -lsensor -o app
.PHONY: clean
clean:
rm -f *.o *.a app
Example 5 — STM32 Cross-Compilation Makefile
This is the style of Makefile STM32CubeIDE generates. You can run it from the terminal without the IDE.
TARGET = my_project
CPU = -mcpu=cortex-m4
FPU = -mfpu=fpv4-sp-d16 -mfloat-abi=hard
LDSCRIPT = STM32F411RETx_FLASH.ld
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
CP = arm-none-eabi-objcopy
SZ = arm-none-eabi-size
C_DEFS = -DSTM32F411xE -DUSE_HAL_DRIVER -DDEBUG
C_INCLUDES = \
-ICore/Inc \
-IDrivers/STM32F4xx_HAL_Driver/Inc \
-IDrivers/CMSIS/Device/ST/STM32F4xx/Include \
-IDrivers/CMSIS/Include
CFLAGS = $(CPU) $(FPU) $(C_DEFS) $(C_INCLUDES) \
-Os -Wall -fdata-sections -ffunction-sections \
-g -gdwarf-2
LDFLAGS = $(CPU) $(FPU) \
-specs=nano.specs \
-T$(LDSCRIPT) \
-lc -lm -lnosys \
-Wl,-Map=$(TARGET).map,--cref \
-Wl,--gc-sections
# Source files
C_SOURCES = Core/Src/main.c \
Core/Src/stm32f4xx_it.c \
Core/Src/stm32f4xx_hal_msp.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.c
# Object files in build/
OBJECTS = $(addprefix build/, $(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
all: build/$(TARGET).elf build/$(TARGET).bin
# Compile C files
build/%.o: %.c | build
$(CC) -c $(CFLAGS) $< -o $@
# Link ELF
build/$(TARGET).elf: $(OBJECTS)
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(SZ) $@
# Convert ELF to flashable BIN
build/$(TARGET).bin: build/$(TARGET).elf
$(CP) -O binary $< $@
build:
mkdir -p build
# Flash via OpenOCD (ST-LINK)
flash: build/$(TARGET).bin
openocd -f interface/stlink.cfg \
-f target/stm32f4x.cfg \
-c "program build/$(TARGET).bin verify reset exit 0x08000000"
.PHONY: clean flash
clean:
rm -rf build
Build and flash:
make -j$(nproc) # Build using all CPU cores
make flash # Flash via ST-LINK
Example 6 — ESP32 Cross-Compilation (Manual Makefile)
ESP-IDF normally uses CMake+Ninja, but understanding a raw Makefile for ESP32 shows you what idf.py does under the hood.
IDF_PATH ?= $(HOME)/esp/esp-idf
TARGET = esp32
CC = xtensa-esp32-elf-gcc
CFLAGS = \
-mlongcalls \
-mtext-section-literals \
-falign-functions=4 \
-Os -Wall -g \
-DESP_PLATFORM \
-DIDF_VER=\"v6.0\" \
-I$(IDF_PATH)/components/freertos/FreeRTOS-Kernel/include \
-I$(IDF_PATH)/components/esp_common/include \
-Imain
main.o: main/main.c
$(CC) $(CFLAGS) -c main/main.c -o main.o
# Note: In practice, use idf.py build.
# This is shown for educational understanding only.
Useful Make Flags
make -j$(nproc) # Build in parallel (use all CPU cores) — fastest
make -j4 # Build with 4 parallel jobs
make -n # Dry run — print commands without executing
make -B # Force rebuild everything (ignore timestamps)
make V=1 # Verbose mode — show full compiler commands
make clean all # Clean then rebuild in one command
make CFLAGS="-O0 -g" # Override a variable from the command line
Makefile Quick Reference
| Feature | Syntax |
|---|---|
| Variable | CC = gcc |
| Use variable | $(CC) |
| Pattern rule | %.o: %.c |
| All .c files | $(wildcard src/*.c) |
| Replace extension | $(patsubst %.c, %.o, $(SRCS)) |
| Add prefix | $(addprefix build/, $(OBJS)) |
| Phony target | .PHONY: clean all |
| Target name | $@ |
| First dep | $< |
| All deps | $^ |
| Parallel | make -j$(nproc) |

