logo

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:

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:

  1. $@ is the current target's name, so that'll echo my_program
  2. $< is the first prerequisite, so that'll echo one
  3. $^ is all prerequisites, so that'll echo one two three four
  4. $? 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:

% Wildcard

This is the confusing one.

What makes it easy to understand is knowing that:

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:

  1. my_program has prerequisites that don't exist (the .o files haven't been created yet)
  2. As there are multiple prerequisites, it goes one at a time - starting with main.o as that's first in OBJECTS
  3. It sees %.o as a target name - bingo! The % wildcard allows make to replace all % symbols with the 'stem' - in this case main

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.

Sources

Chase Lambert Makefile Tutorial GNU Pattern Rules