![]() |
px-fwlib 0.10.0
Cross-platform embedded library and documentation for 8/16/32-bit microcontrollers generated with Doxygen 1.9.2
|
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.
To perform a (simplified) hand build of an Microchip ATmega328P project on the command line, you would do something like this:
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
Here is a simple Makefile to build the project:
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...
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:
>avr-gcc -mmcu=atmega328p -c board.c -o board.o
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".
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:
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
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 $@
For more info on automatic variables and a detailed list, click here.
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.
After applying all of these features, the "simple" Makefile is updated to:
Note the use of the a string substitution function on line 4.
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:
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.
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.