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.
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
## Your project TARGET = blinkLED OBJECTS = blinkLED.o utility-functions.o
And then, because
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 CPPFLAGS = -DF_CPU=$(F_CPU) -DBAUD=$(BAUD) -I.
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 $@ clean: 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
$< 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
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!