Embed With Elliot: Microcontroller Makefiles

Last time on Embed with Elliot, I began my celebration of the make command’s 40th birthday next month. We discussed using the default rules and how to augment them with your own variables defined in a makefile. Next, I’ll walk you through some makefiles that can be used for real-world microcontroller code development. This week, we’ll focus on one for the AVR platform, and later on, I’ll run through a slightly more complicated version for the ST32M series of ARM Cortex micros.

Along the way, we’ll pick up a couple of tricks, but the aim is to keep the makefiles minimal, readable, and easily extensible. Once you get a little taste of the power of writing your own makefiles, you probably won’t be able to stop adding bells and whistles — custom routines for flashing, checking the size of binaries, generating assembly listings, etc. I’ll leave the extras up to you, but you’ll eventually find that anything you do can be automated with a makefile.

AVR Makefile

Compiling for a microcontroller isn’t as straightforward as compiling for your desktop computer. That’s because the compiler on your desktop is designed to output the same machine language that’s spoken by your desktop’s CPU. If you want to create code to run on an AVR (or any other platform), you’re cross compiling, which isn’t nearly as daunting as it sounds at first.

Cross compiling just means using a non-default compiler, linker, and other assorted tools. Last time, we specified which compiler we wanted to use with a line like CC=gcc in our makefile. Here, we just need to list out the minimal set of tools that let us compile for the AVR platform:

## Cross-compilation 
CC = avr-gcc 
OBJCOPY = avr-objcopy 

Next, it’s on to the project-specific details. We’ll start with our project’s name and the object files we’ll need to create. If you had your code spread across multiple .c files, you would specify one object file per .c file in the project — that’s how make is going to know to compile them. Here, we have some utility functions that we use across various projects broken out into their own utility-functions.c and utility-functions.h files.

## Your project 
TARGET = blinkLED 
OBJECTS = blinkLED.o utility-functions.o 

And then, because avr-gcc‘s delay and usart libraries are implemented as macros that are run through the preprocessor instead of compiled with the regular C code, we’ll need to define some parameters up front and pass those values off to the preprocessor so that it can do its work. We’re simply telling our code how fast the CPU is running, and what baud rate we’d like the serial peripheral to use.

## Chip and project-specific global definitions 
MCU = atmega328p 
F_CPU = 8000000UL
BAUD = 9600UL 

The variable CPPFLAGS is used in one of make‘s implicit rules — in particular the one that compiles source files into object files. Now, wherever the macro F_CPU is used in the AVR code, for instance, it will be replaced by the specified (literal) value.

Speaking of implicit rules and their variables, let’s override a few of the defaults for the compiler and linker stages to optimize our code for the AVR:

## Compiler/linker options 
CFLAGS = -Os -g -std=gnu99 -Wall 
CFLAGS += -ffunction-sections -fdata-sections

TARGET_ARCH = -mmcu=$(MCU)

LDFLAGS = -Wl,-Map,$(TARGET).map 
LDFLAGS += -Wl,--gc-sections 

The compiler flags, CFLAGS are used when blinkLED.c is compiled into blinkLED.o, along with CPPFLAGS. The first line sets the optimization level, enables debugging hooks, sets dialect of C that we’re using, and asks the compiler to warn us about any possible errors. The last two CFLAGS instruct the compiler to keep track of which functions and data sections are used. Coupled with the linker flag --gc-sections (“garbage-collect sections”), just below, they let the linker remove all unused functions from your code, which can make a huge difference if you’re including a library of functions just to use one of them.

TARGET_ARCH is used to pass the cross-compilation target architecture option to the compiler and linker. It’s used in the implicit rule that makes object files, and we’ll need to use it explicitly as well. The -Map linker option tells the linker to use a memory map of the same name as your project. The good news is that make has a default rule for making the memory map for you, so you can just sleep through this part.

Finally, we’ve set up all the variables, and we can get to targets and rules.

## Targets and rules
all: flash

flash: $(TARGET).hex
	avrdude -c usbtiny -p $(MCU) -U flash:w:$(TARGET).hex

%.hex: %.elf
	 $(OBJCOPY) -j .text -j .data -O ihex $< $@

%.elf: $(OBJECTS)
	$(CC) $(LDFLAGS) $(TARGET_ARCH) $^ -o $@

	rm -f $(TARGET).elf $(TARGET).hex $(TARGET).map

As our default target, all, we’ll specify a so-called “phony” target called flash. Targets that don’t actually correspond to physical files are called phony, but they’re tremendously useful. Our flash target depends on having the hex file that we need to upload to the chip and includes a rule, in the form of an avrdude command, to do the uploading.

Now all we need is the hex file. The next rule is a generic rule to make .hex files from .elf files, which are an “executable and linkable format” that includes all of our compiled functions, data, and some extra AVR-platform-specific information. For instance, if we were using data pre-loaded into the chip’s EEPROM, that would also be included in the .elf file. $< is a make special variable that refers to a single dependency, in our case the .elf file in question. $@ is an automatic variable for the target of the rule — here it resolves to the name of the file being generated.

Finally, we instruct make how to create an .elf file by linking together the various compiled object files that we specified. The .elf file is essentially a place to combine all the object files together, removing the unused functions from them. (Notice that this is where the LDFLAGS that does the garbage collection shows up.) $^ is a special make variable that refers to all of the listed dependencies, so the command links them all together.

It might seem like something’s missing. We never explicitly tell make to compile the .c code files into the .o object files! But make already knows how to do that, and we’ve told it to use the AVR-specific compiler and relevant options, so there’s no sense in re-writing that rule. And make knows which object files it needs to build since we specified them as dependencies for the .elf file via the OBJECTS variable.

Extensions and Other Fancy Stuff

As presented here, this bare-bones AVR makefile will take your code, compile it, link it, and turn it into a format that an uploader needs to write it directly into the AVR’s flash memory. (And it even does that!) There are, of course, a number of other things that you might like to do.

You might want to disassemble the code and check the timings of certain subroutines, for instance. Or maybe you need to break the EEPROM contents out of the .elf file so that you can pre-load them into the chip. AVRs have all sorts of fuses that need to be set, and you may have a few different chip programmers in your stable. All of this and more can be easily accommodated by just writing a couple more rules. If you’re interested in a more full-blown AVR example, you can peruse this makefile that I actually use for most of my projects.

But the basic ideas shown here are the core of how to use makefiles to cross compile for microcontroller platforms. You’ll need to redefine all of the compilation and linking programs to fit your target, define target-specific compiler and linker options using implicit variables, and then probably write a couple rules of your own as we did here to tie it all together.

And if you’re interested in a makefile that will work with an STM32 ARM chip, stay tuned for the next installment of Embed with Elliot, where we’ll walk through a more involved compile process. Along the way, I’ll demo a couple more useful makefile tricks that I’m sure you’ll enjoy even if you’re not using an ARM chip. March is makefile madness!

10 thoughts on “Embed With Elliot: Microcontroller Makefiles

  1. I always set:

    CROSS_COMPILE ?= avr-

    up the top of my Makefile, then I can set CC, etc like this:


    This is useful because if your C compiler is not called avr-gcc, or isn’t in $PATH, you can run `make CROSS_COMPILE=/path/to/compiler/bin/prefix-`. This is how things are done in the Linux kernel.

    If the extra typing bothers you, add another line:

    -include local.mk

    and put in local.mk:

  2. Eliot, thanks for writing about this kind of stuff. I’ve wondered for a long time about makefiles, and now I’m willing to try the STM32 project in your next article. I’m really looking forward to it. I basically come to Hackaday to read these embedded topics. The ones here are high quality posts, I just wish there were even more of them!

    1. Newer isn’t always better, though. I’m using Keil compiler for ARM, storing all the information about the build in vendor-specific project files. It’s no fun trying to merge these after two teams edited their project properties.

  3. A very nice flag for make is the -j It sets the level of concurrency of the operations done by the make utility. For a ARM cross compile job with some final header-patching and resource building steps I went from almost 3.5 minutes build time down to less than 20 seconds by simply tacking on a -j32 on the make.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

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