Makefile is an indispensable build tool in embedded development that automates the compilation and linking process, supporting incremental builds (only recompiling modified files), avoiding the need to repeatedly input lengthy commands, and significantly improving development efficiency.
1. Core Logic of Makefile
Each Makefile rule follows the core structure of “target – dependencies – commands”, which is the essence of Makefile:
target: dependencies
command # Important: The command must be preceded by a 【Tab key】, spaces are not allowed!
-
Target: The file to be generated (such as
<span>app.exe</span>,<span>main.o</span>, or embedded firmware<span>firmware.elf</span>) or the action to be executed (such as<span>clean</span>); -
Dependencies: The files necessary to generate the target (such as
<span>main.c</span>,<span>add.o</span>) or other targets; -
Commands: The specific steps to generate the target from the dependencies (such as the compiler command
<span>gcc</span>).
2. Getting Started: From Single File to Multiple Files
1. Step One: Compiling a Single File (Simplest Scenario)
Project Structure
project/
├── main.c # Core code (contains main() function)
└── Makefile # Build script (case insensitive, no extension)
Basic Version (Direct Compilation)
# Target: Generate app.exe; Dependencies: main.c
app.exe: main.c
# Compile command: Generate app.exe from main.c
gcc -o app.exe main.c
Optimized Version (Using Variables for Maintenance)
After defining variables, when changing toolchains (such as embedded cross-compilers), only the variable needs to be modified, without changing the commands:
CC = gcc # Compiler variable
# Target: Dependencies
app.exe: main.c
# Variable reference syntax: $(variable_name)
$(CC) -o app.exe main.c
Type <span>make</span> in the command line to generate the app.exe executable file.
2. Step Two: Compiling Multiple Files
When the project is split into multiple source files (such as main program, drivers, utility functions), use <span>.o</span> intermediate files as dependencies to support incremental compilation (only recompiling modified files).
Project Structure
project/
├── main.c # Main program (calls addition function)
├── add.c # Implementation of addition function
├── add.h # Declaration of addition function (header file)
└── Makefile
Basic Version
CC = gcc
# Final target: Link all .o files to generate executable program
app.exe: main.o add.o
$(CC) -o app.exe main.o add.o # Link command
# Intermediate target: Compile .c files to .o files (-c: compile only, do not link)
main.o: main.c add.h # main.c depends on add.h (must include function declaration)
$(CC) -c main.c
add.o: add.c add.h
$(CC) -c add.c
# Clean target: Remove compiled products
.PHONY: clean # Declare as a phony target to avoid command failure if a clean file exists in the directory
clean:
rm -f main.o add.o app.exe # Linux/macOS
# del main.o add.o app.exe # Windows needs to replace this line
Type <span>make</span> in the command line to generate the app.exe executable file, and type <span>make clean</span> to clean up the intermediate files generated during compilation.
Advantages of Incremental Compilation
-
When only
<span>add.c</span>is modified,<span>make</span>automatically detects dependency changes, recompiles only<span>add.o</span>, and then links to generate<span>app.exe</span>, without recompiling<span>main.o</span>, saving time; -
If a specific
<span>.o</span>file (such as<span>main.o</span>) is manually deleted,<span>make</span>will automatically recompile that file.
3. Step Three: Pattern Rules + Automatic Variables (Simplifying Code)
In the above multi-file Makefile, the compilation commands for each <span>.o</span> file are repeated (<span>$(CC) -c XXX.c</span>), which can be simplified using “pattern rules” and “automatic variables”. When adding new files, there is no need to modify the rules.
Optimized Version (Recommended for Embedded)
CC = gcc # Compiler
OBJS = main.o add.o # All intermediate .o files (new files only need to be added here)
TARGET = app.exe # Final target file name (managed uniformly for easy modification)
# Final target: Link all dependent .o files
$(TARGET): $(OBJS)
$(CC) -o $@$^ # Automatic variables: $@=target name, $^=all dependencies
# Pattern rule: All .c files automatically generate corresponding .o files (replace repeated rules)
%.o: %.c
$(CC) -c $< -o $@ # Automatic variables: $< = first dependency (i.e., .c file), $@ = target (i.e., .o file)
# Clean target
.PHONY: clean # Declare as a phony target to avoid command failure if a clean file exists in the directory
clean:
rm -f $(OBJS) $(TARGET) # Linux/macOS
# del $(OBJS) $(TARGET) # Windows replacement
Core Automatic Variables (Must Remember)
| Automatic Variable | Meaning | Example |
|---|---|---|
<span>$@</span> |
Current target file name | When compiling <span>add.o</span>, <span>$@=add.o</span>; when linking <span>app.exe</span>, <span>$@=app.exe</span> |
<span>$<</span> |
First dependency file name | When compiling <span>add.o</span>, <span>$<=add.c</span> |
<span>$^</span> |
All dependency file names | When linking <span>app.exe</span>, <span>$^=main.o add.o</span> |
Simplification Advantages
-
When adding a source file (such as
<span>uart.c</span>), only need to add<span>uart.o</span>in<span>OBJS</span>, no need to add new compilation rules; - No redundant code, reducing the probability of manual errors
Conclusion
-
Core Rule:
<span>target: dependencies + Tab command</span>(Tab is key); -
Variable Usage:
<span>CC</span>(compiler),<span>CFLAGS</span>(compiler options),<span>OBJS</span>(intermediate files), for easier maintenance; -
Simplification Techniques: Pattern rules
<span>%.o: %.c</span>reduce repetitive code, automatic variables<span>$@</span>/<span>$<</span>/<span>$^</span>simplify commands; -
Embedded Adaptation: Replace
<span>CC</span>with cross-compiler, add<span>MCU architecture</span>and<span>Thumb instruction set</span>options; -
Incremental Compilation: Modifying a single file only recompiles the corresponding
<span>.o</span>, greatly saving time.