Makefile: Build Automation
We can use the make
command line tool instead of manually typing in “compile”, “link”, and “flash” commands, automating the entire process. The make
tool uses a configuration file called Makefile
to read the instructions for executing actions. This automation is great because it also documents the process of building firmware and which compilation flags were used.
There is a very good Makefile tutorial for beginners at https://makefiletutorial.com, which I highly recommend checking out. Below, I will list some very necessary concepts to understand the Makefile we are using. For those who are already familiar with make
, you can skip this part.
Actually, the format of Makefile
is not complicated:
action1:
command ... # Comments can go after hash symbol
command .... # IMPORTANT: command must be preceded with the TAB character
action2:
command ... # Don't forget about TAB. Spaces won't work!
Now we can call make
with the action name (also known as the target) to execute the corresponding action:
$ make action1
Of course, you can also define and use variables in the command, and actions can also be the names of files to be created:
firmware.elf:
COMPILATION COMMAND .....
Any action can have a list of dependencies. For example, firmware.elf
depends on the source file main.c
, and when main.c
changes, make build
will rebuild firmware.elf
:
build: firmware.elf
firmware.elf: main.c
COMPILATION COMMAND
We are ready to write the Makefile
for the firmware and define a build
action/target:
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -Wdouble-promotion \
-Wformat-truncation -fno-common -Wconversion \
-g3 -Os -ffunction-sections -fdata-sections -I. \
-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 $(EXTRA_CFLAGS)
LDFLAGS ?= -Tlink.ld -nostartfiles -nostdlib --specs nano.specs -lc -lgcc -Wl,--gc-sections -Wl,[email protected]
SOURCES = main.c
build: firmware.elf
firmware.elf: $(SOURCES)
arm-none-eabi-gcc $(SOURCES) $(CFLAGS) $(LDFLAGS) -o $@
Here we define some compilation flags. ?=
indicates that this is a default value, which we can override in the command line like this:
$ make build CFLAGS="-O2 ...."
The Makefile
file above defines the variables CFLAGS
, LDFLAGS
, and SOURCES
, and then we tell make
to create the firmware.elf
file when building, which depends on the main.c
file, using the arm-none-eabi-gcc
compiler and the given compilation flags to generate it. The special variable $@
will be expanded to the action/target name, which in this case is firmware.elf
.
Now let’s try calling make
:
$ make build
arm-none-eabi-gcc main.c -W -Wall -Wextra -Werror -Wundef -Wshadow -Wdouble-promotion -Wformat-truncation -fno-common -Wconversion -g3 -Os -ffunction-sections -fdata-sections -I. -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Tlink.ld -nostartfiles -nostdlib --specs nano.specs -lc -lgcc -Wl,--gc-sections -Wl,-Map=firmware.elf.map -o firmware.elf
If we run it again:
$ make build
make: Nothing to be done for `build'.
make
will check the modification times of firmware.elf
and the dependency main.c
, and if they are up to date, it will do nothing. If we modify main.c
, it will rebuild:
$ touch main.c # Simulate changes in main.c
$ make build
Now, there is still the “flash” action/target:
firmware.bin: firmware.elf
arm-none-eabi-objcopy -O binary $< $@
flash: firmware.bin
st-flash --reset write $(TARGET).bin 0x8000000
OK, now executing the command make flash
in the terminal will create the firmware.bin
file and then flash it onto the board using st-link
. When main.c
changes, this command will also rebuild, because firmware.bin
depends on firmware.elf
, and firmware.elf
depends on main.c
. So our development cycle consists of these two steps:
# Develop code in main.c
$ make flash
Another good practice is to add a clean
action in the Makefile
to remove the generated build files:
clean:
rm -rf firmware.*
The complete source code can be found in the step-0-minimal[1] folder.
References
step-0-minimal: https://github.com/cpq/bare-metal-programming-guide/tree/main/step-0-minimal