I must confess that I like Makefiles. Now, I know that Makefiles can be very polarizing and have been losing favour bit by bit for over two decades to other options such as CMAKE. But for some reason, I’ve always been satisfied with the “simplicity” of Makefiles over other options. And since I was in the middle of writing the series of guides on using the IAR tools with Eclipse, I thought that a separate article on how to build an embedded C application using IAR and Makefiles would be an interesting addition.
In this article I’ll go over a simple set of Makefiles that can be used to build a basic project using the IAR Embedded Workbench tools under Windows. Note that IAR also has available a set of build tools for Linux but this article focuses on the Windows use case.
Documentation
Before diving into the Makefiles, there’s a few important documents that developers should be aware of when it comes to using the IAR tools from the command line or within a Makefile.
First, there are three important user manuals and reference manuals that can be found from the IAR Information Center. The Information Center can be reached through the Help menu of both the IAR IDE or the Eclipse Help menu if the IAR plugin for Eclipse is already installed. There is a distinct set of manuals for each architecture with different command-line options for each.
The three manuals are as follows:
- IDE Project Management and Building Guide
- IAR C/C++ Development Guide
- IAR Assembler Reference Guide
Those three manuals contain all the information required to use all the build tools either from the IDE or from the command line. Additionally, there is also Technical Note 47884 on the subject of building an IAR Embedded Workbench project from the command line.
Note on the Shell and Make Provider on Windows
To build a makefile project using the IAR tools, the Make utility must be installed as well as a suitable shell to run the build. Multiple options are available but I recommend MSYS2 for performance and compatibility. Instructions on how to install MSYS2 can be found in another article on installing the IAR tools on Windows. This article assumes that MSYS2 is installed and that the make utility is reachable through the system’s PATH variable.
Writing a Makefile for IAR Tools
This guide isn’t meant to be an introduction to Make but let’s start with a working Makefile to build a simple application using the IAR tools and then we’ll look at each part of it in more details. This example uses the NXP i.MX 7 SoC as an example but any supported target can be used.
# Intermediary objects working directory. OBJDIR := obj # Name of the output binary executable file. LINK_TARGET := a.out # Application source file list. APP_SOURCE := main.c test_file.c # Application assembly source file list. APP_ASM_SOURCE := test.asm # Name of toolchain executables to use. CC := iccarm.exe LD := ilinkarm.exe AS := iasmarm.exe # C build flags. C_FLAGS := -e --cpu Cortex-A7 --fpu VFPv4 --debug \ --endian little --cpu_mode arm -On # Assembler build flags. ASM_FLAGS := --cpu Cortex-A7 --fpu VFPv4 -s+ # Linker flags. LINK_FLAGS := --config C:/iar_arm/arm/config/linker/NXP/MCIMX7D_A7.icf # Configure include directories. INCLUDEDIR = -I$(subst \,/,$(PWD)) # Extract all the C source file names. SOURCE += $(notdir $(APP_SOURCE)) # Extract all the assembly source file names. ASM_SOURCE += $(notdir $(APP_ASM_SOURCE)) # Compute intermediary object names. OBJ += $(SOURCE:%.c=$(OBJDIR)/%.o) OBJ += $(ASM_SOURCE:%.asm=$(OBJDIR)/%.o) # Build everything rule. all : $(OBJDIR) $(LINK_TARGET) @echo All done # Clean rule. .PHONY: clean clean: rm -rf ${OBJDIR} rm -rf $(LINK_TARGET) # Create the object directory. $(OBJDIR) : mkdir $(OBJDIR) # Firmware application binary linking rule. $(LINK_TARGET) : $(OBJ) $(LD) -o $@ $^ $(LINK_FLAGS) # C file build rule. $(OBJDIR)/%.o : %.c | $(OBJDIR) $(CC) $(C_FLAGS) $(INCLUDEDIR) -o $@ -c $<; # Assembly file build rule. $(OBJDIR)/%.o : %.asm | $(OBJDIR) $(AS) $(ASM_FLAGS) $(INCLUDEDIR) -o $@ -c $<;
The example Makefile is sufficient to build the application binary with the specified source files. Let’s now look at it in more details. Starting with the first few lines of the Makefile where important variables are set.
# Intermediary objects working directory. OBJDIR := obj # Name of the output binary executable file. LINK_TARGET := a.out # Application source file list. APP_SOURCE := main.c test_file.c # Application assembly source file list. APP_ASM_SOURCE := test.asm # Name of toolchain executables to use. CC := iccarm.exe LD := ilinkarm.exe AS := iasmarm.exe
The example Makefile will build the intermediate object files in a subdirectory named obj
assigned to variable OBJDIR
. While it’s not strictly necessary to have compiler objects stored in a subdirectory, it is rather a nice-to-have instead of cluttering the top build directory.
Following the configuration of the object directory, the name of the linked executable is set in LINK_TARGET
. Although the name can be set to anything, it is useful for debuggers, IDE and other utilities to use a name with the proper extension. In this case this is an ARM ELF file so .out
or .axf
would be appropriate.
Next APP_SOURCE
and APP_ASM_SOURCE
contain the list of C source files and assembly source files to compile. In this example the files are assumed to be in the root of the build directory. If that’s not the case the VPATH should be set to point to the directories where the source files are located. Now there are a few different ways to handle files in multiple directories with everyone privy to the subject having a strong opinion about how it should be done. But for the purpose of this example this method works quite well and is simple enough. Many prefer to use the shell or built-in Make features to programmatically generate a list of files to be built. I must say, however, that I prefer to explicitly write the list of files. That way if a file is added for some reason into the directory tree, it won’t be compiled without my knowledge. In the same vein, if a file goes missing or is renamed by mistake, you get a build error from make instead of cryptic errors from the linker.
Finally, three other convenience variables, CC
, LD
and AS
are set to the names of the tool executables. With only the executable name set, the tools must be reachable through the system’s PATH
variable. Alternatively, the full path to the tool executable could be added to the Makefile.
Now we move on to the compilation and linking flags.
# C build flags. C_FLAGS := -e --cpu Cortex-A7 --fpu VFPv4 --debug \ --endian little --cpu_mode arm -On # Assembler build flags. ASM_FLAGS := --cpu Cortex-A7 --fpu VFPv4 -s+ # Linker flags. LINK_FLAGS := --config C:/iar_arm/arm/config/linker/NXP/MCIMX7D_A7.icf # Configure include directories. INCLUDEDIR = -I$(subst \,/,$(PWD))
The first three variables C_FLAGS
, ASM_FLAGS
and LINK_FLAGS
contain the build flags to be passed to the build tools later in the Makefile. You can consult the reference manuals mentioned earlier in this guide to check all the possible build flags. Another option that you can use when feeling lazy, is to create a dummy IAR IDE project for the intended target and settings and extract the command line to use from there.
Finally, INCLUDEDIR does what it says and provides the list of include directories. For the purpose of this example, only the current build directory is added to the list.
Following those, the next few lines generate the list of files to build and the list of intermediary target object files.
# Extract all the C source file names. SOURCE += $(notdir $(APP_SOURCE)) # Extract all the assembly source file names. ASM_SOURCE += $(notdir $(APP_ASM_SOURCE)) # Compute intermediary object names. OBJ += $(SOURCE:%.c=$(OBJDIR)/%.o) OBJ += $(ASM_SOURCE:%.asm=$(OBJDIR)/%.o)
The way the source file and object file names are generated is relatively simple. Any path is first stripped from the source file names and the result is stored in SOURCE and ASM_SOURCE. Those last two variables will be used to pass the list of files to the compiler and assembler later in the Makefile. Then, a list of intermediary object files is computed from the C and ASM sources by appending the object directory and replacing the file extension with ".o".
The result is then aggregated in the OBJ
variable.
Now we finally get to the first build rules.
# Build everything rule. all : $(OBJDIR) $(LINK_TARGET) @echo All done # Clean rule. .PHONY: clean clean: rm -rf ${OBJDIR} rm -rf $(LINK_TARGET) # Create the object directory. $(OBJDIR) : mkdir $(OBJDIR)
The first three rules are part of the usual boilerplate. The very first rule is designed to build the all
target with a dependency on the application binary and the built object directory. Remember that Make considers the first rule found when parsing a Makefile to be the default rule, i.e., the one that gets built when invoking make without specifying a rule.
The second rule is the customary clean
target which simply deletes the application binary and the built object directory. The last rule in this snippet is just there to create the build directory.
From here we reach the actual rules responsible for compilation and linking.
# Firmware application binary linking rule. $(LINK_TARGET) : $(OBJ) $(LD) -o $@ $^ $(LINK_FLAGS) # C file build rule. $(OBJDIR)/%.o : %.c | $(OBJDIR) $(CC) $(C_FLAGS) $(INCLUDEDIR) -o $@ -c $<; # Assembly file build rule. $(OBJDIR)/%.o : %.asm | $(OBJDIR) $(AS) $(ASM_FLAGS) $(INCLUDEDIR) -o $@ -c $<;
The three rules above are respectively for linking, assembling and compiling and are very similar to what they would look like for another compiler such as GCC or CLANG. Other than using the appropriate flags the automatic variables do all the job of building the command line. Remember that $@
in a rule expands to the target name while $^
expands to all the dependencies. That way the rule for linking links all the object files at once and outputs the target binary a.out
in this example. On the other hand, $<
as used for compiling and assembling expands only to the first file of the dependency list, however, for the pattern rules as written this is equivalent to $^
.
Creating or Linking a Static Library
It’s easy to create a static library by using iarchive.exe
instead of the linker. The following Makefile example is modified to build a static library instead of an application binary.
# Intermediary objects working directory. OBJDIR := obj # Name of the output library. LIB_TARGET := lib.a # Application source files list. LIB_SOURCE := test_lib_src.c # Name of toolchain executables to use. CC := iccarm.exe AR := iarchive.exe # C Build flags. C_FLAGS := -e --cpu Cortex-A7 --fpu VFPv4 --debug\ --endian little --cpu_mode arm -On # Configure include directories. INCLUDEDIR = -I$(subst \,/,$(PWD)) # Extract all the C source file names. SOURCE += $(notdir $(LIB_SOURCE)) # Compute intermediary object names. OBJ += $(SOURCE:%.c=$(OBJDIR)/%.o) # Build everything. all : $(OBJDIR) $(LINK_TARGET) @echo All done # Create object directory. $(OBJDIR) : mkdir $(OBJDIR) # Clean target. .PHONY: clean clean: rm -rf ${OBJDIR} rm -rf $(LINK_TARGET) # Create the static library archive. $(LIB_TARGET) : $(OBJ) $(AR) --create -o $@ $^ # C files build. $(OBJDIR)/%.o : %.c | $(OBJDIR) $(CC) $(C_FLAGS) $(INCLUDEDIR) -o $@ -c $<;
Linking a library is even easier, one just needs to add it to the list of object files to link. For example:
# Firmware application binary linking rule. $(LINK_TARGET) : $(OBJ) $(LD) -o $@ $^ lib.a $(LINK_FLAGS)
Library search directories can also be added using the -L flag.
This concludes this introduction article on how to write a Makefile for the IAR build tools. Stay tuned for a future article describing how to create a Makefile project using the IAR Embedded Workbench for Eclipse plugin.
Click here to read how to create a Makefile project within Eclipse using the IAR tooclhain.
Questions or comments?
Do not hesitate to contact us at blog@jblopen.com. Your questions, comments and, suggestions are appreciated.