To wrap up my quick tour through the wonderland of
make and makefiles, we’re going to look at a pair of possible makefiles for building ARM projects. Although I’m specifically targeting the STM32F407, the chip on a dev board that I have on my desk, it’s reasonably straightforward to extend these to any of the ST ARM chips, and only a bit more work to extend it to any ARM processor.
If you followed along in the first two installments of this series, I demonstrated some basic usages of
make that heavily leveraged the built-in rules. Then, we extended these rules to cross-compile for the AVR series of microcontrollers. Now we’re going to tackle a more complicated chip, and that’s going to mean compiling with support libraries. While not required, it’s a lot easier to get an LED blinking on the ARM platforms with some additional help.
One of the main contributions of an IDE like Arduino or mbed or similar is the ease of including external libraries through pull-down menus. If you’ve never built a makefile-based project before, you might be surprised how it’s not particularly more difficult to add libraries to your project.
ARM Makefile Take One: Explicit Version
To start off, our ARM makefile is a lot like our AVR version. We need to specify the cross-compilation tools so that the computer doesn’t build files in its native format, and pass some compilation and linker flags using the
LFLAGS implicit variables, respectively.
## Cross-compilation commands CC = arm-none-eabi-gcc AR = arm-none-eabi-ar OBJCOPY = arm-none-eabi-objcopy SIZE = arm-none-eabi-size ... ## Platform and optimization options CFLAGS = -c -fno-common -Os -g -mcpu=cortex-m4 -mthumb CFLAGS += -Wall -ffunction-sections -fdata-sections -fno-builtin CFLAGS += -Wno-unused-function -ffreestanding LFLAGS = -Tstm32f4.ld -nostartfiles -Wl,--gc-sections
A number of these options are shared with the AVR makefile from last time — splitting the functions out into their own sections and garbage-collecting the unused ones, for instance. ARM-specific entries include the processor and the “thumb” instruction set options. Finally, in the linker flags,
LFLAGS, we pass a memory map (
stm32f4.ld) to the linker that tells it where everything needs to go in memory. We didn’t need this file in the AVR case because the chip has a simple and consistent memory layout that GCC can just fill in for us. Not so in the ARM world, but you can find the right memory map for your project in the development libraries that you’re using.
The rules to build the project aren’t particularly complicated. Because
make rules are specified in terms of targets and their dependencies, it’s often easiest to think backwards through a rule chain. In this case, my ARM flash programmer needs a raw binary image file to send to the chip, so we can start there. The
object-copy command makes a binary out of an
.elf file, the linker makes an
.elf file from compiled objects, and a default rule compiles our source code into objects.
## Rules all: main.bin size flash %.bin: %.elf $(OBJCOPY) --strip-unneeded -O binary $< $@ main.elf: $(OBJS) stm32f4.ld $(LD) $(LFLAGS) -o main.elf $(OBJS)
That wasn’t hard, was it? The first rule defined is always the default that gets run when you just type
make, so I tend to make it a good one. In this case, it compiles the needed binary file, prints out its size, and flashes it into the chip all in one fell swoop. It’s like the “do-everything” button in any IDE, only I don’t have to move my hand over to the mouse and click. (Remember that
$< is makefile-speak for the dependency — the
.elf file — and
$@ is the variable that contains the target
Which brings us to libraries. The rule for making
main.elf above relies on a variable
OBJS (objects) that we haven’t defined yet, and that’s where we get to include other people’s code (OPC). (Yeah, you know me.) In C, including other modules is simply a matter of compiling both your code and the OPC into object files, including a header file to tell the compiler which functions come from which files, and linking the objects together.
In our case, we’ll be linking against both the CMSIS standard ARM library and ST’s HAL driver library for the F4 processor. Since I’ve done a bit of STM32F4 programming, I like to keep these libraries someplace central rather than re-duplicating them for each sub-project, and that means that I have to tell
make where to find both the header files to include, and the raw C source files that provide the functions.
The header files are easy, so let’s tackle them first. You simply pass the
-I[your/directory/here] option to the compiler and it knows to go looking there for the
.h files. (Make sure you also include the current directory!)
## Library headers CFLAGS += -I./ CFLAGS += -I/usr/local/lib/CMSIS/Device/ST/STM32F4xx/Include/ CFLAGS += -I/usr/local/lib/CMSIS/Include/ CFLAGS += -I/usr/local/lib/STM32F4xx_HAL_Driver/Inc/
In this case, we’re including both the overall CMSIS headers that are shared across all ARM platforms, as well as the specific ones for my chip.
Once the compiler knows where to find the header files, we need to compile the actual C code in the add-on libraries. We do this, as before, by telling
make that we want the object files that correspond to the C code in question. We specify the target, and
make tries to satisfy the dependencies.
# our code OBJS = main.o # startup files and anything else OBJS += handlers.o startup.o ## Library objects OBJS += /usr/local/lib/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.o OBJS += /usr/local/lib/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.o OBJS += /usr/local/lib/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.o OBJS += /usr/local/lib/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.o OBJS += /usr/local/lib/CMSIS/Device/ST/STM32F4xx/Source/Templates/system_stm32f4xx.o
Remember from the first makefile example that
make knows how to compile
.c code into
.o object files. So by setting the variable
OBJ as a dependency of some other target, each of the source files that correspond to the listed objects will get automagically compiled. And since we already have a rule that links all of the object files into a single
main.elf file, we’re done!
## Rules main.elf: $(OBJS) stm32f4.ld $(LD) $(LFLAGS) -o main.elf $(OBJS)
The rest of the makefile is convenience rules for flashing the chip and etcetera. Look through if you’d like and here’s a helpful tip on how to do that: If you want to see what
make is thinking just type
make -p which lists all of the rules and some variables, taking the current makefile into account.
make --warn-undefined-variables can also help catch typos — if you type
OBJ instead of
Fancier ARM Makefile: Building up the Core Library
make system gives you so many cool tools to automate things, it’s really hard to know when to stop. Quite honestly, I probably should stick to a simple makefile with few moving parts like the one above. It’s very nice to have an explicit list of all of the bits of included library code in one place in the makefile. If you want to know what extra stuff is needed to compile and run the program, all you need to look at is the
OBJS definitions. But there are a few
make tricks that maybe will come in handy later on in your life, and I can’t resist, so here goes.
The above procedure also has one real flaw. It compiles the object files into the same directory as the library source. When you end up (re-)using the code in the CMSIS directory across projects with different processors, for instance, you’ll have object files compiled for one chip being linked in with another unless you’re careful to remove them all first.
It would be better to compile the object files locally, leaving you many object files floating around in your code directory. OCD programmers of yore hated that kind of clutter, and thus the archive file was born. An archive is just a bunch of object files jammed into one. To build one you pass the object files to an archiver, and out comes a
.a file that you can link against later, and the end result is just the same as if you’d linked against all of the component object files, with much less typing.
So let’s take the CMSIS and HAL libraries, all of them, compile them and wrap them up in one big archive called
core.a. That way, we’ll only ever have to compile those objects once, until we download new versions of the libraries, and we’ll be free to use any additional parts of the library almost without thinking. (Note that this goes against my advice to be specific about which bits of code we’re including. But it’s so convenient.)
To build the
core.a archive, we’ll need an object file for every source file in a few different directories. We could list them out, but there’s a better way. The solution is to use a wildcard to match every
.c filename and then edit the names of each file to change the
.c to a
.o, giving us the complete list of objects.
Here’s a simple snippet that compiles every
.c file in the current directory by defining the corresponding object files as dependencies for an executable
main program. This makes for a quick and dirty makefile to have on hand because it usually does what you want if you just plunk it down in a directory full of code.
SRCS = $(wildcard *.c) OBJS = $(SRCS:.c=.o) CFLAGS = -Wall -I./ main: $(OBJS) $(LD) $(LFLAGS) -o main $(OBJS)
So we can get our list of object files by wildcarding the various CMSIS and ST library directories:
## Locate the main libraries CMSIS = /usr/local/lib/CMSIS HAL = /usr/local/lib/STM32F4xx_HAL_Driver HAL_SRC = $(HAL)/Src CMSIS_SRC = $(CMSIS)/Device/ST/STM32F4xx/Source/Templates CORE_OBJ_SRC = $(wildcard $(HAL_SRC)/*.c) CORE_OBJ_SRC += $(wildcard $(CMSIS_SRC)/*.c) CORE_LIB_OBJS = $(CORE_OBJ_SRC:.c=.o) CORE_LOCAL_LIB_OBJS = $(notdir $(CORE_LIB_OBJS))
This creates a list of object files for every source file, and it does one other cute trick. The
notdir command removes the leading directory information from every file in the list. So if we had a long filename like
/usr/local/lib/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.o we now just have
stm32f4xx_hal_rcc.o. We’re going to need this because we want to assemble all of the object files together in the current directory, so we have to specify the object files without their full path.
We’ve got an object file for each source file in the various libraries, but we still need a rule to make them. If all of the C code were in the current directory, we’d be set, because when
stm32f4xx_hal_rcc.o, it tries to build it out of
stm32f4xx_hal_rcc.c lives in
/usr/local/lib/STM32F4xx_HAL_Driver/Src/. If only there were a way to tell
make to go looking for C code in different directories, just like we told the compiler to go looking for header files in different include directories. Enter the
VPATH = $(HAL_SRC):$(CMSIS_SRC)
Long story short, a colon-separated list of directories passed to the special
VPATH variable treats OPC, located anywhere on your disk, as if it were our own code in the current directory. So with our list of the localized version of all of the core object files, and the
VPATH correctly set to point to the corresponding source code, we can compile all the object files and throw them all into a single archive file.
core.a: $(CORE_OBJ_SRC) $(MAKE) $(CORE_LOCAL_LIB_OBJS) $(AR) rcs core.a $(CORE_LOCAL_LIB_OBJS) rm -f $(CORE_LOCAL_LIB_OBJS)
$(MAKE) automatic variable just calls
make. Here, we’re effectively saying
make stm32f4xx_hal_i2c_ex.o stm32f4xx_hal_dcmi.o stm32f4xx_hal_pcd.o ... for all of the object files. The
AR line creates our archive (
core.a). Then, we clean up all the extraneous object files that are now included in
core.a. Neat and tidy.
Wizardry: Putting it all Together
A recap: We use a wildcard to identify all of the source and get the corresponding object filenames.
VPATH points at the source files in the foreign library, so
make can find the remote source. We then make each of the object files and throw them all together into an archive, and then clean up afterwards. Now we’re ready to compile our personal code against this gigantic library of functions. But don’t worry, because we passed the
CFLAGS to remove unused code, we only end up with the bare minimum. And we never have to re-compile the library code again. In this version of the makefile,
OBJS is just the three local object files. Everything else is in the
main.elf: $(OBJS) core.a stm32f4.ld $(LD) $(LFLAGS) -o main.elf $(OBJS) core.a
The core version of this makefile is also available in one piece for your perusal. I’ve also pushed up the entire project to GitHub so you can make it for an STM32F4 chip, or modify it to work with other platforms. If people are interested, I’ll do some more “getting started with ARM” type topics in the near future.
We’ve barely scratched the surface of
make, yet we’ve done a complex compilation that automatically pulls in external code libraries and pre-compiles them into a local archive. There’s no limit to the trouble you can get yourself into with
make and once you start, you’ll never stop. Just try to remember to keep things simple if you can, and remember that debugging is twice as hard as writing code in the first place — you’ll want to make it easy on yourself.