5 Tips for Writing Makefiles to Compile Embedded Software

Makefiles are a fundamental tool that every embedded software developer needs to understand. Even if you use an Eclipse-based IDE, a makefile is generated behind the scenes to build your object files and invoke your linker. As more teams adopt DevOps and modern development practices, the need to handwrite and understand makefiles is increasing. In this post, we will explore five tips for writing makefiles.

Tip #1 – Simplify file list generation using the shell ‘find’ command

Every makefile comprises variables, recipes, and patterns that convert your application modules into object files and link them to an executable image. It’s not uncommon to find a variable that lists all the source modules in the project like the following:

CPP_SOURCES =  \
firmware/app/main_cpp.cpp \
firmware/app/led.cpp \
firmware/app/led_io.cpp \
firmware/app/led_pwm.cpp \
firmware/app/dio_stm32.cpp \
firmware/app/relay_io.cpp \
firmware/app/pwm_stm32.cpp \
Makefile

In this example, the variable CPP_SOURCES contains a list of all the *.cpp files that must be compiled into object files. Listing each file individually gives developers much control over which files they will build. However, it can also be annoying if every module is included in a project. Every time a new module is added, you must remember to add it to your makefile.

Instead, developers can use a simple trick; use the shell ‘find’ command to search your project directories for all *.c or *.cpp files. Then, instead of CPP_SOURCES growing into a giant confusing list that makes the makefile challenging to read, it becomes the following:

CPP_SOURCES := $(shell find firmware/app -type f -name '*.cpp')
Makefile

Any new files added are automatically added to the build, and the makefile doesn’t become a hot pippin mess.

Note: You can also use wild cards which is a more efficient, and portable mechanism

APP_C_SOURCES := $(wildcard $(APP_DIR)/*.c)
Makefile

Tip #2 – Use multiple makefiles

You might often see highly complex makefiles with a lot built into them. Makefiles can be broken up into separate makefiles that contain individual functionality. For example, instead of creating one giant make file that manages to build the application, run tests, and so on, these features can all be put in their makefiles.

Separating functionality into separate makefiles comes with several benefits. First, it provides several smaller files that are easier to develop and maintain. Next, it increases the portability of the build system so that different products can use all or parts of the makefile system. Finally, it can decrease complexity and make them easier to read and understand.

Tip #3 – Leverage phony targets

Makefiles are all about building targets. Sometimes though, we want our makefiles to assist us with activities that aren’t directly building targets. For example, I might want a makefile to contain the configuration and command line parameters for running a code formatting tool, performing static analysis, or other activities associated with my code. To do that, you can create phony targets.

According to the GNU Make website, a phony target is not really the name of a file; instead, it is just a name for a recipe to be executed when you make an explicit request. So, if I want to use Make to execute my test harness, cpputest, I might make a phony target that looks like the following:

.PHONY: unit_tests

unit_tests:

    $(MAKE) -j CC=gcc -f cpputest.mk
Makefile

If you wanted to initialize a docker container from your makefile, you could use a phony target that looks something like the following:

.PHONY: docker_run

docker_run:

    docker run --rm -it --privileged -v "$(PWD):/home/app" beningo/cpp-dev:latest bash
Makefile

The advantage is that complex commands can be executed simply using the makefile system. To run unit tests, you simply use:

make unit_tests
Makefile

If you want to start your docker container, you use:

make docker_run
Makefile

Phony targets can dramatically improve your build environment and make things far easier for developers.

Tip #4 – Don’t forget to use size

Tracking how much flash and RAM are used in embedded software development is critical. If you add a library that suddenly dramatically increases code size or memory usage, that’s a good thing to know. Unfortunately, I often find that developers forget to print out the application’s size information after the build when they write their own makefiles. Printing out the application’s size information is easy.

First, you need to define a variable that will define the application to give the size information. The application will be part of your compiler toolchain. For example, if you use GCC for Arm, you would use arm-none-eabi-size. Defining a variable to specify the application can be done something like the following:

SZ = arm-none-eabi-size
Makefile

Finally, in the last step of your target build process, you use the SZ variable to print the size information with your final target. For example, your last step might look something like the following:

$(EXE_DIR)/%.bin: $(EXE_DIR)/%.elf | $(BUILD_DIR)

            $(BIN) $< $@  

            $(SZ) $(EXE_DIR)/$(TARGET).elf
Makefile

The step above is creating a *.bin file from the resultant &.elf file and then invoking arm-none-eabi-size on the final *.elf image. The result of the build is the following displayed in the terminal:

arm-none-eabi-objcopy -O binary -S bin/controller.elf bin/controller.bin

arm-none-eabi-size bin/controller.elf

   text    data     bss     dec     hex filename

  27480     144   10640   38264    9578 bin/controller.elf
Makefile

As you can see, the *.bin file is created, and the size is printed. Monitoring these statistics is essential when developing embedded applications.

Tip #5 – Consider using CMake

Using makefiles is extremely common in embedded systems. However, as you start to look at other areas of the software industry, the majority (2/3) of developers use CMake. For example, the CMake website describes CMake as follows:

“CMake is used to control the software compilation process using simple platform—and compiler-independent configuration files and generate native makefiles and workspaces that can be used in the compiler environment of your choice.”

CMake is a configuration tool that generates makefiles for you. It is easier to learn than makefile writing and can provide you with a cross-platform, configurable makefile system. If you don’t want to hand-code your makefiles, consider using CMake.  

Conclusions

Makefiles are critical tools for embedded software developers to build their applications successfully. Unfortunately, many vendor-provided toolchains hide the makefiles in the background. As teams adopt DevOps and use vendor-independent environments like Visual Studio Code, the demand for writing makefiles will increase. Developers can either write the makefiles themselves or adopt tools like CMake that will write the makefiles for them. In either case, developers need to understand makefiles, and the tips we’ve explored should help you make your makefiles more portable and easier to understand.

Share >

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.