px-lib  0.9.3
Cross-platform embedded library and documentation for 8/16/32-bit microcontrollers
7.2 How to understand and modify Makefiles

1. Introduction

Make is a command-line utility packaged with IDEs such as Atmel Studio and build systems such as MSYS2 or UnixShellUtils. All of the projects in this library are built using a Makefile. Normally it is used as as an intelligent command-line script to compile a project's source file(s) and link the intermediate object file(s) to create a final executable, but it can be used for other purposes too.

I recommend that you download UnixShellUtils so that you are not restricted to the build tools packaged with one specific IDE. If you extract UnixShellUtils to "C:\UnixShellUtils", you will find Make here:

C:\UnixShellUtils\usr\bin\make.exe

I prefer to create my own Makefile over an automated IDE build where I can, because then I can use the same editor (for example SlickEdit) to edit and build my projects and I have finer control over the build process. Another advantage is that the project can be built on different operating systems.

The Makefile build process is used extensively in the Un*x world and the time invested to master this fundamental tool will pay big dividends later in life.

1.1 References

2. Manual build

To perform a (simplified) hand build of an Microchip ATmega328P project on the command line, you would do something like this:

1 avr-gcc -mmcu=atmega328p -c board.c -o board.o
2 avr-gcc -mmcu=atmega328p -c main.c -o main.o
3 avr-gcc -mmcu=atmega328p board.o main.o -o test.elf
4 avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock test.elf test.hex
5 avr-objdump -h -S -z test.elf > test.lss
  • Line 1 compiles a C file "board.c" to create an intermediate object file "board.o"
  • Line 2 compiles a C file "main.c" to create an intermediate object file "main.o"
  • Line 3 links the object files "board.o" and "main.o" to create an executable "test.elf"
  • Line 4 extracts an Intel HEX file "test.hex" from "test.elf" that can be used to program the microcontroller
  • Line 5 creates an assembly listing text file ("test.lss") from the ELF file so that you can see EXACTLY what code will be executed on the compiler.

If you inspect the "test.lss" file you will notice that the compiler links in extra assembly start up code, before your main() function is called. This code ensures that the C run time environment is correctly initialised, interrupt vectors jump to the correct locations, etc.

You can put all of these steps in a Windows Batch file or Bash Shell script, but just imagine if you have 20 source files... each time you want to build, the WHOLE project is built from scratch, even though maybe only one source file was modified. There must be a better way, and the answer is... Make

3. A "simple" Makefile

Here is a simple Makefile to build the project:

1 all: test.elf test.hex test.lss
2 
3 test.elf: board.o main.o
4  avr-gcc -mmcu=atmega328p board.o main.o -o test.elf
5 
6 test.hex: test.elf
7  avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock test.elf test.hex
8 
9 test.lss: test.elf
10  avr-objdump -h -S -z test.elf > test.lss
11 
12 board.o: board.c
13  avr-gcc -mmcu=atmega328p -c board.c -o board.o
14 
15 main.o: main.c
16  avr-gcc -mmcu=atmega328p -c main.c -o main.o
17 
18 clean:
19  rm -f *.o
20  rm -f *.elf
21  rm -f *.hex
22  rm -f *.lss

To invoke Make on the command-line, you would do something like this:

>make -f Makefile all

"Makefile" is the name of the script file to use and "all" is the target to create. Make searches for a file called "Makefile" first, so "-f Makefile" is redundant and can be omitted. By default, Make will execute the first target found in the Makefile, so "all" can also be omitted and thus the command simplifies to:

>make

To create a specific target, e.g. "board.o", you will invoke Make as follows:

>make board.o

And now for a brief explanation...

4. Make Rules (pun intended)

A Makefile consists mostly of rules to create targets. The syntax / form is as follows:

target: prerequisites
--->recipe
--->recipe

Take note that each line of the recipe starts with a TAB character (indicated above with an arrow —>). Make needs the TAB character to know that the line is a recipe and still part of the target rule. A common mistake is to use SPACES instead of a TAB character.

Now look at this rule to create "board.o" (the target):

board.o: board.c
    avr-gcc -mmcu=atmega328p -c board.c -o board.o

It tells Make that "board.c" is required (the prerequisite) to create "board.o" (the target). Then the recipe starts: it takes the "board.c" file, compiles it and creates a "board.o" file as the output.

And now for the intelligent bit. If "board.o" already exists, then Make compares the timestamp of the "board.o" file and the "board.c" file. If "board.c" is newer (i.e. you have just modified and saved it), then Make executes the recipe to create an updated version of "board.o". If "board.o" is already up to date, then Make does not execute the recipe.

"all" and "clean" are so-called phony targets. It basically means that no real target file is created / updated, but you can still perform the recipe as follows:

>make clean

or

>make all

By understanding make rules, you are now equiped to figure out the recursive build process that ensues when you execute Make from a clean slate. Let's see what happens when "make" is executed with this "simple" Makefile example:

1 all: test.elf test.hex test.lss
2 
3 test.elf: board.o main.o
4  avr-gcc -mmcu=atmega328p board.o main.o -o test.elf
5 
6 test.hex: test.elf
7  avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock test.elf test.hex
8 
9 test.lss: test.elf
10  avr-objdump -h -S -z test.elf > test.lss
11 
12 board.o: board.c
13  avr-gcc -mmcu=atmega328p -c board.c -o board.o
14 
15 main.o: main.c
16  avr-gcc -mmcu=atmega328p -c main.c -o main.o
17 
18 clean:
19  rm -f *.o
20  rm -f *.elf
21  rm -f *.hex
22  rm -f *.lss
  1. The first target that Make finds is "all" on line 1. "all" requires "test.elf", "test.hex" and "test.lss" (the prequisites) which does not exist.
  2. Make needs to create "test.elf" and finds a rule to do so on line 3. It needs "board.o" and "main.o" which also does not exist.
  3. Make now needs to create "board.o" and finds a rule to do so on line 12. It needs "board.c" which does exist. So it proceeds to execute the recipe (on line 13) on the command-line:
    >avr-gcc -mmcu=atmega328p -c board.c -o board.o
    
  4. In a similiar fashion it finds and executes the rule on line 15 to create "main.o".
  5. Make now has the two required files "board.o" and "main.o" and executes the recipe on line 4 to create "test.elf".
  6. In a similiar fashion it executes the recipes on line 7 and 10 to create "test.hex" and "test.lss".

This is the command-line output:

>make clean
rm -f *.o
rm -f *.elf
rm -f *.hex
rm -f *.lss

>make
avr-gcc -mmcu=atmega328p -c board.c -o board.o
avr-gcc -mmcu=atmega328p -c main.c -o main.o
avr-gcc -mmcu=atmega328p board.o main.o -o test.elf
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock test.elf test.hex
avr-objdump -h -S -z test.elf > test.lss

If you modify and save "main.c", this is the resulting output:

>make
avr-gcc -mmcu=atmega328p -c main.c -o main.o
avr-gcc -mmcu=atmega328p board.o main.o -o test.elf
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock test.elf test.hex
avr-objdump -h -S -z test.elf > test.lss

You can see that Make did not recompile "board.c", but only did what it needed to update "test.elf".

5. Make Variables

One of the golden coding rules is: never repeat yourself!

Make allows the declaration and use of variables. The syntax is as follows:

VARIABLE_NAME = value

To use a variable, the variable name must be surrounded by brackets () and prepended with a dollar sign $:

$(VARIABLE)

So the simple Makefile can be updated to:

1 PROJECT = test
2 OBJECTS = board.o main.o
3 MCU = atmega328p
4 
5 all: $(PROJECT).elf $(PROJECT).hex $(PROJECT).lss
6 
7 $(PROJECT).elf: $(OBJECTS)
8  avr-gcc -mmcu=$(MCU) $(OBJECTS) -o $(PROJECT).elf
9 
10 $(PROJECT).hex: $(PROJECT).elf
11  avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock $(PROJECT).elf $(PROJECT).hex
12 
13 $(PROJECT).lss: $(PROJECT).elf
14  avr-objdump -h -S -z $(PROJECT).elf > $(PROJECT).lss
15 
16 board.o: board.c
17  avr-gcc -mmcu=$(MCU) -c board.c -o board.o
18 
19 main.o: main.c
20  avr-gcc -mmcu=$(MCU) -c main.c -o main.o
21 
22 clean:
23  rm -f *.o
24  rm -f *.elf
25  rm -f *.hex
26  rm -f *.lss

Now if you want to change the name of your project, you only have to edit one location: line 1

To append more text to a variable, you can use the += operator, for example:

OBJECTS  = board.o
OBJECTS += main.o

As a naming convention, variables are written with ALL CAPITALS with underscores _ separating words.

There is also the conditional asignment operator for variables: '?='. It is used to assign a (default) value to variable if it has not been defined yet. For example:

MCU ?= atmega328p

The variable value can then be provided when Make is executed, for example:

make MCU=atmega128

6. Make Automatic Variables

Automatic variables are calculated for each rule that is executed. They only have a value within the recipe. An automatic variable starts with a dollar sign $, e.g. "$@" or "$<"

Thus the following rule:

main.o: main.c
    avr-gcc -mmcu=atmega328p -c main.c -o main.o

Can be updated to:

main.o: main.c
    avr-gcc -mmcu=atmega328p -c $< -o $@
  • "$<" refers to the first prerequisite, so "$<" will be replaced with "main.c"
  • "$@" refers to the target file name, so "$@" will be replaced with "main.o"

For more info on automatic variables and a detailed list, click here.

7. Make Pattern Rules

Instead of writing a separate rule for each object file, you can use a pattern rule. A pattern rule starts with the percentage sign % and it is used as the wildcard character for the match. Thus the separate rules for each object file can be replaced with a single pattern rule:

%.o: %.c
    avr-gcc -mmcu=$(MCU) -c $< -o $@

Thus if you want to create "board.o", the pattern match % is "board" and the prerequisite is "board.c".

Note the use of automatic variables ($< and $@) in the recipe.

8. An "advanced" Makefile

After applying all of these features, the "simple" Makefile is updated to:

1 PROJECT = test
2 SRC = board.c main.c
3 MCU = atmega328p
4 OBJECTS = $(patsubst %.c,%.o,$(SRC))
5 
6 all: $(PROJECT).elf $(PROJECT).hex $(PROJECT).lss
7 
8 $(PROJECT).elf: $(OBJECTS)
9  avr-gcc -mmcu=$(MCU) $^ -o $@
10 
11 $(PROJECT).hex: $(PROJECT).elf
12  avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock $< $@
13 
14 $(PROJECT).lss: $(PROJECT).elf
15  avr-objdump -h -S -z $< > $@
16 
17 %.o: %.c
18  avr-gcc -mmcu=$(MCU) -c $< -o $@
19 
20 clean:
21  rm -f *.o
22  rm -f *.elf
23  rm -f *.hex
24  rm -f *.lss

Note the use of the a string substitution function on line 4.

9. Dependencies

If your C file includes other H files, then you can specify these files as prerequisites too. That way, you can ensure that your C file will be rebuilt if any of the H files are changed. For example:

main.o: main.c board.h

This means that if you modify and save "main.c" or "board.h", then Make must rebuild "main.o".

Here is the final Makefile:

1 # Specify project options
2 PROJECT = test
3 SRC = board.c main.c
4 MCU = atmega328p
5 
6 # Calculate object files by replacing ".c" suffix with ".o"
7 OBJECTS = $(patsubst %.c,%.o,$(SRC))
8 
9 # "all" is the default (first) target to create ELF, HEX and LSS file
10 all: $(PROJECT).elf $(PROJECT).hex $(PROJECT).lss
11 
12 # Rule to create ELF file
13 $(PROJECT).elf: $(OBJECTS)
14  avr-gcc -mmcu=$(MCU) $^ -o $@
15 
16 # Rule to create HEX file using ELF file
17 $(PROJECT).hex: $(PROJECT).elf
18  avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock $< $@
19 
20 # Rule to create LSS file using ELF file
21 $(PROJECT).lss: $(PROJECT).elf
22  avr-objdump -h -S -z $< > $@
23 
24 # Pattern rule to create *.o file from a *.c file
25 %.o: %.c
26  avr-gcc -mmcu=$(MCU) -c $< -o $@
27 
28 # "clean" target to delete all created files
29 clean:
30  rm -f *.o
31  rm -f *.elf
32  rm -f *.hex
33  rm -f *.lss
34 
35 # Specify dependencies
36 board.o: board.c board.h
37 main.o: main.c board.h

Comments starting with hash/pound character #

The GCC compiler can parse your C files and generate these dependencies automatically. This has been implemented in the Make scripts (*.mk) included in the library.

10. Conclusion

Well, that's the end of the "gentle" introduction. I hope that you have enough insight now to copy, paste and modify the Makefiles used in this library and have enough confidence to delve into the magical world of Makefiles.