5 Tips for Writing a Makefile for Embedded Software

Using makefiles is extremely common in embedded systems, so here are some tips for success.

Jacob Beningo

May 2, 2023

5 Min Read
gorodenkoff/iStock/Getty Images Plus via Getty Images

Makefiles are a fundamental tool that every embedded software developer needs to understand. Even if you use an Eclipse-based IDE, behind the scenes, a makefile is generated to build your objects 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 then link them into an executable image. It’s not uncommon to find a variable that lists all the source modules in the project like the following:


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 \


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')

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


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 makefile that manages to build the application, run tests, and so on, these features can all be put in their own 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 a target. 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


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


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 --rm -it --privileged -v "$(PWD):/home/app" beningo/cpp-dev:latest bash      


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

make unit_tests


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

make docker_run


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


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


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


As you can see, the *.bin file is created, and the size is printed. Keeping an eye on 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 acts as a configuration tool that will generate the makefiles for you. CMake is easier to get up to speed on than makefile writing. CMake can provide you with a cross-platform, configurable makefile system. If you don’t want to hand-code your makefiles, consider using CMake.  


Conclusions for Makefiles

Makefiles are a critical tool 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, there will be an increasing demand for writing makefiles. Developers can either write the makefiles themselves or adopt tools like CMake that will write the makefiles for you. 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.

About the Author(s)

Jacob Beningo

Jacob Beningo is an embedded software consultant who currently works with clients in more than a dozen countries to dramatically transform their businesses by improving product quality, cost and time to market. He has published more than 300 articles on embedded software development techniques, has published several books, is a sought-after speaker and technical trainer and holds three degrees which include a Masters of Engineering from the University of Michigan.

Sign up for the Design News Daily newsletter.

You May Also Like