Contents
Makefile (make) For Linux/STM32
What Is Makefile?
When I started learning embedded programming, I would manually build every project on my little Raspberry Pi Pico.
This means compiling + assembling (arm-none-eabi-gcc), linking (arm-none-eabi-ld), converting ELF to binary (arm-none-eabi-objcopy), converting binary to UF2 (uf2conv), flashing (picotool), and finally removing all leftover files from the build process.
In total, there were about 7 commands to run every time I wanted to make any tiny change.
This is what Makefile solves, and it does it by ‘bundling' all commands when you call make.
This article is an example-driven explanation of how to use Makefile on Linux (and MacOS - as they're both Unix-based).
The General Idea
This is what Makefile is at its core, in a general form:
target : prerequisite(s)
command(s)
Here's what's happening:
targetis the target/file name, which can also be an input for other targets (think of it like a function name from something like C/Python)prerequisite(s)are file names that must exist in order for the commands to be executed - they're dependenciescommand(s)are the actual shell commands that will run should the target get executed
Command lines must start with a tab character. Spaces do not work, it has to be the ‘\t’ ASCII character.
Targets
Here's a basic target:
hello:
gcc main.c -o hello
When you run make from the same directory as your Makefile, this will compile main.c and output a file called hello. Note that main.c must exist, as it would if you compiled it by hand.
There is one caveat - if the hello file exists already, it won't compile (that's because our target name is hello).
Prerequisites
This is where prerequisites come in:
hello: main.c
gcc main.c -o hello
By specifying main.c as a prerequisite, make will now check if main.c has been modified more recently than hello, if that's true it'll execute the commands. If hello doesn't exist, it'll run in any case.
Targets As Prerequisites
hello: hello.o
gcc hello.o -o hello
hello.o: main.c
gcc -c main.c -o hello.o
Here, hello requires hello.o to run, so it runs it after finding it in the Makefile. hello.o requires main.c, which it can find in the working directory.
In turn, hello.o runs first, when hello runs after.
If you delete main.c, both targets will need to be run again after the file is created again.
If you change the last-modified time of hello.o to make it newer, only hello will need to be run again.
Calling Targets
Up until now, I've only mentioned calling make without parameters. That will, by default, just execute the very first target in the file, which may or may not result in an error (e.g. if the prerequisites are older than the target file).
This can be changed by calling make <target>, such as by calling make hello.o from the section above - it'll only run hello.o and skip hello, so it'll only create a hello.o file as an output.
Clean
Specifying a target is useful for many reasons. One of the main ones is cleanup:
hello: hello.o
gcc hello.o -o hello
hello.o: main.c
gcc -c main.c -o hello.o
clean:
rm -f hello hello.o
Running make will only call hello and hello.o, which get created as files in the working directory. So you can avoid cleaning up manually after, you can run make clean, which will remove the 2 that got made.
Variables
These work exactly like you'd expect, exactly like your favourite programming language (hopefully C).
The only important distinction is that these variables can only be strings.
# create a variable called inputs that is the string "input1 input2 input3"
# note there are NO quotation marks
inputs := input1 input2 input3
# then use it
my_program : $(inputs)
gcc $(inputs) -o my_program
In the above code, anywhere I write $(inputs), it'll get replaced with the string "input1 input2 input3"
Automatic Variables
Sometimes you want to use the name of the target or prerequisite(s) inside a command. This is how:
prerequisites := one two three four
my_program : $(prerequisites)
echo $@
echo $<
echo $^
echo $?
If you run make here, it'll see that my_program is the first target and run it. That will result in 4 echo commands being executed:
$@is the current target's name, so that'll echomy_program$<is the first prerequisite, so that'll echoone$^is all prerequisites, so that'll echoone two three four$?is all prerequisites that are newer than the target, so the echo will depend on timestamps here
Wildcards
In the above example, we only had to clean up 2 files, which was easy enough to do by writing their names in the clean target as an rm command.
But what if you had a very elaborate build process which created tens of intermediate files on its way to creating the final executable binary?
For that there are wildcards.
* Wildcard
When you put * around a target, it will create a match for any amount of any characters. The placement also matters:
*_drivers.cwill matchusb_drivers.c,uart_drivers.c,essential_startup_drivers.c, and any file that ends in_drivers.cusb_*will matchusb_drivers.c,usb_drivers.h,usb_init.c,usb_init.h, and any file that begins withusb_usb_*.cwill matchusb_drivers.c,usb_init.c, and any file that begins withusb_and ends with.c- so all USB-related .c files
% Wildcard
This is the confusing one.
What makes it easy to understand is knowing that:
- it can only be used in the target and prerequisite areas (
target : prerequisite) - it doesn't care if a matching file exists yet
Here's an example use case to talk about it:
OBJECTS := main.o uart.o gpio.o
my_program : $(OBJECTS)
gcc $(OBJECTS) -o my_program
%.o : %.c
gcc -c $< -o $@
If you run make here, it'll see that my_program is the first target and run it:
my_programhas prerequisites that don't exist (the.ofiles haven't been created yet)- As there are multiple prerequisites, it goes one at a time - starting with
main.oas that's first inOBJECTS - It sees
%.oas a target name - bingo! The%wildcard allows make to replace all%symbols with the 'stem' - in this casemain
As a result, the following target gets called:
main.o : main.c
gcc -c $< -o $@
This is done with uart.o and gpio.o as well, as they're also in the variable OBJECTS.
The $< and $@ automatic variables are explained in the above section.
I think what really makes this click for me is understanding that %.o : %.c can't exist on its own - it needs to be explicitly called with a stem like main so it can turn it into something useful, main.o : main.c in this case. It doesn't just automatically search the working directory for all possible target names that would match; it only searches the working directory for prerequisites.