Makefile Review

By: Gil Pinheiro

Every project needs a good build process.

You spend most of your day cycling through the basic edit/build/deploy/test process, and it makes sense to spend some time optimizing that.

Our project is embedded, which means that rebuilding the world doesn’t take minutes or hours. We need touch only a few dozen files, so clean rebuilds are not a painful event.

So naturally, we stuck with Make. If you are not familiar with it is a venerable build system that powers a great number of projects, but has a reputation for crustiness. Well earned in some cases, granted, but overall, if you spend the time to learn the tool it is quite powerful and intuitive.

The problem is that it does take quite a bit of time to really get to know Make. The basics are quite easy, but you can quickly get into trouble. Once you’ve done something wrong, debugging the problem is quite painful After it is set up, you only occasionally need that knowledge. It goes to the back of your mind and is quickly forgotten.

At first I tried to modify the example makefile provided with the SDK examples, but it ultimately left me scratching my head. Makefiles are like perl, if you don’t write them to be read, you are left with unmaintainable code.

I found a good example on github via esp8266.org, and was struck by its simplicity and readability. It expanded my Make repertoire a little, and ultimately taught me a lot. So for the rest of this post I’m going to do a literate programming style review of the Makefile, so when I need to go back and edit it in six months, I’ll be able to do so.

001
# updated at https://github.com/pushrate/esp8266_template
002
# Based on Makefile for ESP8266 projects
003
# https://github.com/esp8266/source-code-examples
Makefiles come with a bunch of rules built in, usually helpful, except when you are trying to debug at which point your rules become very insignificant. n.b. On debugging: Use --only-print to extract what exactly is being executed, --debug to see how the rules are nested and how dependancies are being resolved, and --trace to understand why things are being rebuilt.
023
MAKEFLAGS += --no-builtin-rules
024
025
# Output directory to store intermediate compiled files.
026
# They are relative to the project directory
027
BUILD_BASE = build
028
FW_BASE = firmware
029
030
# base directory of the ESP8266 SDK package, absolute
031
# default assumes ESP_HOME is your esp-open-sdk root directory
032
SDK_BASE ?= $(ESP_HOME)/sdk
033
034
# esptool.py details
035
ESPTOOL     ?= esptool.py
036
ESPPORT     ?= /dev/ttyUSB0
037
FLASH_FLAGS ?= --flash_mode qio --flash_size 4MB -ff 40m
038
039
# name for the target project, will effect naming of .out file
040
TARGET = example
This makefile just assumes all .c and .s files in the MODULES directories are code that should be compiled into the final binary
044
# which modules (subdirectories) of the project to include in compiling
045
MODULES = app
046
047
# allow some configuration headers to live at the repo root, as it is much easier to find them there
048
EXTRA_INCDIR = .
this is all of the libs present in the espressif SDK except lwip_536
051
# libraries used in this project, mainly provided by the SDK
052
LIBS = c hal airkiss at crypto driver espnow gcc json lwip main mesh net80211 phy pp pwm smartconfig ssl upgrade wpa2 wpa wps
053
054
# compiler flags using during compilation of source files
055
#http://www.esp8266.com/viewtopic.php?t=231&p=1139
056
CFLAGS = -Wpointer-arith -Wundef -Werror -Wl,-EL -fno-inline-functions -nostdlib \
057
-mlongcalls -mtext-section-literals  -D__ets__ -DICACHE_FLASH
from linker docs: -u symbol: force symbol to be entered in the output file as an undefined symbol.
060
LDFLAGS = -nostdlib -Wl,--no-check-sections -Wl,--gc-sections -u call_user_start -Wl,-static
There is some interesting voodoo that I don't yet completely understand with the ld file, you can add a symbol pattern there to force the linker to put all matching objects in irom (flash). Instead, I use the GDBFN define to set all the gdbstub functions to default into flash which will allow us to link gdbstub in without overfilling ram
065
# Linker script defines where everything will go in memory
066
LD_SCRIPT = eagle.app.v6.ld
067
068
# If asked to build 'debug', add gdbstub directory to the build, and enable necessary debug cflags,
069
# if building in debug configuration, set define, and add gdbstub
070
ifeq ($(filter debug,$(MAKECMDGOALS)),debug)
071
MODULES += esp-gdbstub
072
CFLAGS  += -Og -ggdb -DDEBUG -DGDBSTUB_FREERTOS=0 -DATTR_GDBFN=ICACHE_FLASH_ATTR
073
else
074
#optimize for space
075
CFLAGS += -Os
076
endif
077
078
# various paths inside the espressif SDK that we'll be using in this project
079
SDK_LIBDIR = lib
080
SDK_LDDIR  = ld
081
SDK_INCDIR = include include/json driver_lib/include
So there is a little mystery here, looking at many past examples file_2_addr used to be 0x40000 but esptool now generates 0x10000. I found this reference to explain the change: https://github.com/pfalcon/esp-open-sdk/issues/226 which indicates that the ld script has changed. I think the implication is that there is much more irom space, but nothing is reserved for updates.
Update: this might help clarify it all, file1 is a whole bunch of stuff, fronted by a header that describes its own layout (segments, see esptool image_info) that need to be loaded into ram at boot time by the builtin firmware, basically initializing iram, etc. 0x10000 is the irom, exactly as it will exist at 0x40210000.
092
# We create two different files for uploading into the flash.
093
FW_FILE_1_ADDR = 0x00000
094
FW_FILE_2_ADDR = 0x10000
095
096
# select which tools to use as compiler, librarian and linker
097
CC := xtensa-lx106-elf-gcc
098
AR := xtensa-lx106-elf-ar
099
LD := xtensa-lx106-elf-gcc
From here down, deal with generic compile logic
104
SRC_DIR   := $(MODULES)
105
BUILD_DIR := $(addprefix $(BUILD_BASE)/,$(MODULES))
106
107
SDK_LIBDIR := $(addprefix $(SDK_BASE)/,$(SDK_LIBDIR))
108
SDK_INCDIR := $(addprefix -I$(SDK_BASE)/,$(SDK_INCDIR))
Find and add each .s file in each module to SRC ( end up with "app/app.c esp-gdbstub/gdbstub-entry.S ...")
111
SRC := $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.c)) $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.S))
Take each source file, prefix with build_base, and make them .o files, (end up with "build/app/app.o build/esp-gdbstub/gdbstub-entry.o")
113
OBJ := $(patsubst %.S,$(BUILD_BASE)/%.o,$(patsubst %.c,$(BUILD_BASE)/%.o,$(SRC)))
For each lib listed, turn them into compiler -l flags
115
LIBS := $(addprefix -l,$(LIBS))
Generate the name for the combined file/lib
118
APP_AR := $(addprefix $(BUILD_BASE)/,$(TARGET)_app.a)
Generate the elf executable name
120
TARGET_OUT := $(addprefix $(BUILD_BASE)/,$(TARGET).out)
121
122
# Again, turn the ld script into a linker flag
123
LD_SCRIPT := $(addprefix -T$(SDK_BASE)/$(SDK_LDDIR)/,$(LD_SCRIPT))
124
125
# Generate include flags (module, module/include, and *EXTRA_INCDIR*)
126
INCDIR := $(addprefix -I,$(SRC_DIR))
127
EXTRA_INCDIR := $(addprefix -I,$(EXTRA_INCDIR))
128
MODULE_INCDIR := $(addsuffix /include,$(INCDIR))
129
130
FW_FILE_1 := $(addprefix $(FW_BASE)/,$(FW_FILE_1_ADDR).bin)
131
FW_FILE_2 := $(addprefix $(FW_BASE)/,$(FW_FILE_2_ADDR).bin)
Simple helper function that if in verbose mode provides extra information
134
V ?= $(VERBOSE)
135
ifeq ("$(V)","1")
136
Q :=
137
vecho := @true
138
else
139
Q := @
140
vecho := @echo
141
endif
There are a bunch of ways to handle nested directories, but I found this pretty elegant, and I've never actually define functions in a makefile before, so seeing it done this way was really helpful
Make rules are tested, but if the requirements aren't satisfied, then it just moves on to the next one that matches. Here I use that property to try and build the .o file from a .S file (if it exists)
148
define compile-objects
149
build/$1/%.o: $1/%.S
150
	$(vecho) "SCC $$<"
151
	$(Q) $(CC) $(INCDIR) $(MODULE_INCDIR) $(EXTRA_INCDIR) $(SDK_INCDIR) $(CFLAGS) -c $$< -o $$@
152
153
build/$1/%.o: $1/%.c
154
	$(vecho) "CC $$<"
155
	$(Q) $(CC) $(INCDIR) $(MODULE_INCDIR) $(EXTRA_INCDIR) $(SDK_INCDIR) $(CFLAGS) -c $$< -o $$@
156
endef
.PHONY target is something I need to look up every time: basically it serves to tell Make that these targets aren't associated with an actual filesystem file
160
.PHONY: all checkdirs flash clean
161
162
all: checkdirs $(TARGET_OUT) $(FW_FILE_1) $(FW_FILE_2)
This was a bit of an oddity: if observing with just-print it will appeart to be run twice, but in practice it is only run once (I think). The rule would be matched for each of the firmware files, but the first invocation will generate both.
166
# esptool splits the elf file (example.out) into the two firmware files.
167
$(FW_BASE)/%.bin: $(TARGET_OUT) | $(FW_BASE)
168
	$(vecho) "FW $(FW_BASE)/"
169
	$(Q) $(ESPTOOL) elf2image -o $(FW_BASE)/ $(TARGET_OUT)
Call the linker: start-group end-group tells the linker to iterate through this set of libraries multiple time to resolve references. Otherwise it would only go through once, and items at the start of the list couldn't rely on those at the end
175
$(TARGET_OUT): $(APP_AR)
176
	$(vecho) "LD $@"
177
	$(Q) $(LD) -L$(SDK_LIBDIR) $(LD_SCRIPT) $(LDFLAGS) -Wl,--start-group $(LIBS) $(APP_AR) -Wl,--end-group -o $@
178
179
$(APP_AR): $(OBJ)
180
	$(vecho) "AR $@"
181
	$(Q) $(AR) cru $@ $^
182
183
checkdirs: $(BUILD_DIR) $(FW_BASE)
184
185
$(BUILD_DIR):
186
	$(Q) mkdir -p $@
187
188
$(FW_BASE):
189
	$(Q) mkdir -p $@
190
191
debug: checkdirs $(TARGET_OUT) $(FW_FILE_1) $(FW_FILE_2)
192
TODO: There is probably some utility in wiping/initializing memory areas that aren't program related (stored settings, etc)
194
flash: $(FW_FILE_1) $(FW_FILE_2)
195
	$(ESPTOOL) --port $(ESPPORT) write_flash $(FLASH_FLAGS) $(FW_FILE_1_ADDR) $(FW_FILE_1) $(FW_FILE_2_ADDR) $(FW_FILE_2)
196
197
clean:
198
	$(Q) rm -rf $(FW_BASE) $(BUILD_BASE)
Iterate through all the modules and create the compile rules for their contents
201
$(foreach bdir,$(SRC_DIR),$(eval $(call compile-objects,$(bdir))))
Back to Post