In embedded programming, we trust IDEs like STM32CubeIDE, Keil or so on. These IDEs give us just some buttons and UIs to build, flash, and debug the source code that we've written for a MCU. But, I think that every engineer that works on this field have to know that how these steps are made by hand.
In this article, I will show building a firmware written for STM32F446RE MCU with just using a single Makefile.
Below is the project directory:
$ tree
.
├── bin/
├── drivers/
│ ├── CMSIS/
│ ├── STM32F446RETX_FLASH.ld
│ └── STM32F4xx_HAL_Driver/
├── lib/
├── Makefile
└── src/
├── it.c
├── main.c
├── main.h
└── peripheral.c
Firstly, which libraries do we need to have?
HAL library
CMSIS library
Linker script
Startup/system code
arm-none-eabi-* tools
I've put the all required libraries/scripts into /drivers directory. Please download these using:
$ git clone --recurse-submodules https://github.com/STMicroelectronics/STM32CubeF4.git
$ sudo apt install gcc-arm-none-eabi
In here, HAL library is used to program the peripherals and CMSIS for ARM-Cortex itself. These are the most important libraries that we have to get.
Apart from that, we also need some special files. First is the linker script. It includes the memory layout of microcontroller. Sometimes, it comes from above command. But if you cannot find it there, please install it externally. Another file is the startup code. It's written in Assembly language. As its name suggests, it's the first code that runs in microcontroller. Basically, it includes the Reset_Handler and a branch that jumps into main() function in your source code. Also system code includes the some generic and general definitions.
After downloaded and explained the required ones, Let's write the Makefile step by step:
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
In here, we've defined some flags:
-mthumb means that the firmware uses thumb (16-bit) instruction set.
-mcpu=cortex-m4 means the ARM-Cortex series itself.
-mfpu=fpv4-sp-d16 and -mfloat-abi=hard means that floating-point operations can be done in the firmware.
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
In here, we specified the header files that are used in 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. Sorthands 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
In here, I mostly did the string operations and manipulations. But some points are interesting and need some explanations:
Firstly, I've grouped the sources and libraries and converted the source file suffixes into object file formats.
Also, I've created three total flags that will be used for compiling, assembling, and linking phases.
In linker flags, I put the linker script. As you noticed, this step will be done after the compiling the source files. I also linked the some libraries. -lc is the standard C library. In firmware, we use often string manipulation functions (strlen(), strcat(), strcpy() or so on), snprint(), memset(), or similar ones. -lm is the math library as you know. -lnosys means that I'm just implementing the bare-metal firmware, there is no such as kernel or fully OS.
7. Output files
FIRMWARE_ELF := firmware.elf
FIRMWARE_BIN := firmware.bin
Generally, we use the ELF file when flashing and debugging. ELF executable includes the both machine code, metadata, debug information, symbol table or similar stuffs. In contrast, BIN executable just includes the machine code. If you look at the size of both, you will use the ELF executable is much bigger!
8. The 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)
Lastly, run the Makefile:
$ make
That's it 🥳🥳🥳. You've built the both firmware.elf and firmware.bin.
You are ready to flash the built firmware into microcontroller with this or similar command (over ST-Link connection):
$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program firmware.elf verify reset exit"
Top comments (0)