Professional firmware development requires reproducible builds, a proper toolchain, and a reliable flash-and-verify workflow. Today you'll set it all up.
A Makefile automates compilation: define CC, CFLAGS, LDFLAGS, sources, and targets. Dependencies are automatically recalculated — only changed files recompile. Key flags: -mcpu=cortex-m3 -mthumb (CPU architecture), -nostdlib (no standard library), -T linker.ld (linker script), -Wl,-Map=output.map (generate memory map). Use arm-none-eabi-size to check flash and RAM usage after each build.
CMake generates Makefiles (or Ninja files) from a higher-level description. Advantages: out-of-tree builds, easy integration of third-party libraries, IDE integration. For embedded: use CMake toolchain files that set CMAKE_SYSTEM_PROCESSOR, CMAKE_C_COMPILER to arm-none-eabi-gcc. add_executable creates the ELF target. target_link_options adds linker flags. CMake is overkill for single-file projects but essential for anything over 5 source files.
OpenOCD can flash, verify, and run firmware in one command: openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c 'program firmware.bin verify reset exit'. The verify step reads back flash and compares to the ELF — catches bad flash connections. Add a make flash target to your Makefile so it's one command. Generate a binary with arm-none-eabi-objcopy -O binary firmware.elf firmware.bin.
# Makefile for STM32F103 bare-metal project
# Usage: make — compile
# make flash — compile + flash
# make clean — remove build artifacts
# make size — show flash/RAM usage
# Toolchain
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
# MCU flags
CPU = -mcpu=cortex-m3 -mthumb
FPU = # no FPU on F103
# Compile flags
CFLAGS = $(CPU) $(FPU)
CFLAGS += -O2 -g3
CFLAGS += -Wall -Wextra -Werror
CFLAGS += -ffunction-sections -fdata-sections # dead code elimination
CFLAGS += -nostdlib
CFLAGS += -DSTM32F103xB
CFLAGS += -IDrivers/CMSIS/Include
CFLAGS += -IDrivers/CMSIS/Device/ST/STM32F1xx/Include
# Linker flags
LDFLAGS = $(CPU) $(FPU)
LDFLAGS += -TSTM32F103C8Tx_FLASH.ld
LDFLAGS += -Wl,--gc-sections # remove unused sections
LDFLAGS += -Wl,-Map=build/out.map # memory map
LDFLAGS += -nostdlib
LDFLAGS += -lc -lm -lnosys # minimal C runtime
# Sources
SRCS = Src/main.c
SRCS += Src/startup_stm32f103.s # assembly startup
OBJS = $(SRCS:%.c=build/%.o)
OBJS := $(OBJS:%.s=build/%.o)
TARGET = build/firmware
# Rules
all: $(TARGET).elf $(TARGET).bin size
$(TARGET).elf: $(OBJS)
@mkdir -p build
$(CC) $(OBJS) $(LDFLAGS) -o $@
$(TARGET).bin: $(TARGET).elf
$(OBJCOPY) -O binary $< $@
build/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
build/%.o: %.s
@mkdir -p $(dir $@)
$(CC) $(CPU) -c $< -o $@
flash: $(TARGET).bin
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
-c "program $(TARGET).bin 0x08000000 verify reset exit"
size: $(TARGET).elf
$(SIZE) $@
clean:
rm -rf build/
.PHONY: all flash size clean
make size after compilation and track the flash/RAM usage trend. Firmware that silently grows will eventually run out of flash or overflow the stack. Add a CI step that fails the build if flash usage exceeds 90% — don't find out it's full when you're trying to add a critical bug fix.Src/, Inc/, Drivers/CMSIS/, build/.make size. Record .text (flash), .data (initialized globals), .bss (zero-init globals), decimal total.make flash target and flash with OpenOCD + verify. Confirm the LED blinks._Static_assert(FLASH_USED < 65536, "Too large"). Use the size output to extract FLASH_USED.Set up a GitHub Actions CI pipeline for your firmware. On every push, the workflow should: check out the code, install arm-none-eabi-gcc, run make, check the size output and fail if flash usage exceeds 80%, and upload the firmware.bin as a workflow artifact. This is professional embedded CI. Research how to install arm-gcc in a GitHub Actions runner.