Day 5 of 5
⏱ ~60 minutes
Firmware Development in 5 Days — Day 5

Build and Flash

Professional firmware development requires reproducible builds, a proper toolchain, and a reliable flash-and-verify workflow. Today you'll set it all up.

Makefile Builds

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 for Larger Projects

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.

Flash and Verify with OpenOCD

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
# 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
💡
Always run 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.
📝 Day 5 Exercise
Set Up a Professional Firmware Build
  1. Create a Makefile-based project structure: Src/, Inc/, Drivers/CMSIS/, build/.
  2. Compile your Day 3 interrupt example with the Makefile. Fix any warnings (treat as errors with -Werror).
  3. Run make size. Record .text (flash), .data (initialized globals), .bss (zero-init globals), decimal total.
  4. Add a make flash target and flash with OpenOCD + verify. Confirm the LED blinks.
  5. Add a compile-time assertion that fails if firmware exceeds 64KB: _Static_assert(FLASH_USED < 65536, "Too large"). Use the size output to extract FLASH_USED.

Day 5 Summary

  • Makefile automates compilation: only recompile changed files; one 'make flash' does everything
  • Use -ffunction-sections -fdata-sections with --gc-sections to remove unused code and data
  • arm-none-eabi-size reports flash (.text+.data) and RAM (.data+.bss) usage after every build
  • OpenOCD 'verify' step reads back flash and confirms it matches — always use it
Challenge

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.

Finished this lesson?