Using Makefiles to Automate Development

7 minute read

Where I demonstrate how to use make and makefiles to automate your build process.

Update

While this entry accurately describes how to use a Makefile, the approach has changed slightly. See Developing in C for the ATmega328P: Make, Makefile and env.make for the latest information.

Automate using a Makefile

We’ll use the Makefile from Elliot William’s book, he has in the folder setupProject. This Makefile is comprehensive and delivers an Arduino IDE type of simplicity with significantly increased speed. I’ve made some changes to it to make it easier to switch between different types of systems. Here is the file:

Link to download Makefile

Click on the link to download the file, your system might name it “Makefile.txt”, rename it to “Makefile”.

##########------------------------------------------------------##########
##########  System-specific Details                             ##########
##########  are contained in root-level file: env.make          ##########
##########  edit to change local/board/project parameters       ##########
##########------------------------------------------------------##########
include ./env.make
##########------------------------------------------------------##########
##########                  Program Locations                   ##########
##########     Won't need to change if they're in your PATH     ##########
##########------------------------------------------------------##########

CC = avr-gcc
OBJCOPY = avr-objcopy
OBJDUMP = avr-objdump
AVRSIZE = avr-size
AVRDUDE = avrdude

##########------------------------------------------------------##########
##########                   Makefile Magic!                    ##########
##########         Summary:                                     ##########
##########             We want a .hex file                      ##########
##########        Compile source files into .elf                ##########
##########        Convert .elf file into .hex                   ##########
##########        You shouldn't need to edit below.             ##########
##########------------------------------------------------------##########

## The name of your project (without the .c)
TARGET = main
## Or name it automatically after the enclosing directory
# TARGET = $(lastword $(subst /, ,$(CURDIR)))

# Object files: will find all .c/.h files in current directory
#  and in LIBDIR.  If you have any other (sub-)directories with code,
#  you can add them in to SOURCES below in the wildcard statement.
# See Note re: CPPFLAGS if using/not using LIBDIR
SOURCES=$(wildcard *.c $(LIBDIR)/*.c)
OBJECTS=$(SOURCES:.c=.o)
HEADERS=$(SOURCES:.c=.h)

## Compilation options, type man avr-gcc if you're curious. 
## Use this CPPFLAGS with LIBDIR if a library directory is known 
##CPPFLAGS = -DF_CPU=$(F_CPU) -DBAUD=$(BAUD) -DSOFT_RESET=$(SOFT_RESET) -I.  -I$(LIBDIR)
## Else, use this one which simply uses the local directory
CPPFLAGS = -DF_CPU=$(F_CPU) -DBAUD=$(BAUD) -I.
# use below to setup gdb and debugging
# If GCC is < 12.x
CCFLAGS = -Og -ggdb -std=gnu11 -Wall -Wundef
# If GCC 12+ Add --param=min-pagesize=0 to solve subscript error on AVR uC
# "array subscript 0 is outside array bounds"
# https://gcc.gnu.org/bugzilla//show_bug.cgi?id=105523
# CFLAGS = -Og -ggdb -std=gnu11 -Wall -Wundef --param=min-pagesize=0 
# Use below to optimize size
#CFLAGS = -Os -g -std=gnu99 -Wall
## Use short (8-bit) data types 
CFLAGS += -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums 
## Splits up object files per function
CFLAGS += -ffunction-sections -fdata-sections 
LDFLAGS = -Wl,-Map,$(TARGET).map 
## Optional, but often ends up with smaller code
LDFLAGS += -Wl,--gc-sections 
## Relax shrinks code even more, but makes disassembly messy
## LDFLAGS += -Wl,--relax
LDFLAGS += -Wl,-u,vfprintf -lprintf_flt -lm  ## for floating-point printf
## LDFLAGS += -Wl,-u,vfprintf -lprintf_min      ## for smaller printf
TARGET_ARCH = -mmcu=$(MCU)

## Explicit pattern rules:
##  To make .o files from .c files 
%.o: %.c $(HEADERS) Makefile
    $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o $@ $<;

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

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

%.eeprom: %.elf
    $(OBJCOPY) -j .eeprom --change-section-lma .eeprom=0 -O ihex $< $@ 

%.lst: %.elf
    $(OBJDUMP) -S $< > $@

## These targets don't have files named after them
.PHONY: all disassemble disasm eeprom size clean squeaky_clean flash fuses

complete: all flash all_clean

all: $(TARGET).hex 

static: 
    cppcheck --std=c99 --platform=avr8 --enable=all ../../. 2> cppcheck.txt

debug:
    @echo
    @echo "Source files:"   $(SOURCES)
    @echo "MCU, F_CPU, BAUD:"  $(MCU), $(F_CPU), $(BAUD)
    @echo   

# Optionally create listing file from .elf
# This creates approximate assembly-language equivalent of your code.
# Useful for debugging time-sensitive bits, 
# or making sure the compiler does what you want.
disassemble: $(TARGET).lst

disasm: disassemble

# Optionally show how big the resulting program is 
size:  $(TARGET).elf
    $(AVRSIZE) -C --mcu=$(MCU) $(TARGET).elf

clean:
    rm -f $(TARGET).elf $(TARGET).hex $(TARGET).obj \
    $(TARGET).o $(TARGET).d $(TARGET).eep $(TARGET).lst \
    $(TARGET).lss $(TARGET).sym $(TARGET).map $(TARGET)~ \
$(TARGET).eeprom cppcheck.txt

all_clean:
    rm -f *.elf *.hex *.obj *.o *.d *.eep *.lst *.lss *.sym *.map *~ *.eeprom core

LIB_clean:
    rm -f $(LIBDIR)/*.o

##########------------------------------------------------------##########
##########              Programmer-specific details             ##########
##########           Flashing code to AVR using avrdude         ##########
##########------------------------------------------------------##########

flash: $(TARGET).hex 
    $(AVRDUDE) -c $(PROGRAMMER_TYPE) -p $(MCU) $(PROGRAMMER_ARGS) -U flash:w:$<

## An alias
program: flash

flash_eeprom: $(TARGET).eeprom
    $(AVRDUDE) -c $(PROGRAMMER_TYPE) -p $(MCU) $(PROGRAMMER_ARGS) -U eeprom:w:$<

avrdude_terminal:
    $(AVRDUDE) -c $(PROGRAMMER_TYPE) -p $(MCU) $(PROGRAMMER_ARGS) -nt

## If you've got multiple programmers that you use, 
## you can define them here so that it's easy to switch.
## To invoke, use something like `make flash_arduinoISP`
flash_usbtiny: PROGRAMMER_TYPE = usbtiny
flash_usbtiny: PROGRAMMER_ARGS =  # USBTiny works with no further arguments
flash_usbtiny: flash

flash_dragon: PROGRAMMER_TYPE = dragon
flash_dragon: PROGRAMMER_ARGS =  -c dragon_isp -P usb 
flash_dragon: flash

flash_usbasp: PROGRAMMER_TYPE = usbasp
flash_usbasp: PROGRAMMER_ARGS =  # USBasp works with no further arguments
flash_usbasp: flash

flash_atmelice: PROGRAMMER_TYPE = atmelice
flash_atmelice: PROGRAMMER_ARGS = -P $(SERIAL) 
## (for windows) flash_arduinoISP: PROGRAMMER_ARGS = -b 19200 -P com5
flash_atmelice: flash

flash_snap: PROGRAMMER_TYPE = snap_isp
flash_snap: PROGRAMMER_ARGS = -P usb 
flash_snap: flash

flash_xplain: PROGRAMMER_TYPE = xplainedmini
flash_xplain: PROGRAMMER_ARGS =  # USBTiny works with no further arguments
flash_xplain: flash

##########------------------------------------------------------##########
##########       Fuse settings and suitable defaults            ##########
##########------------------------------------------------------##########

## Mega 48, 88, 168, 328 default values
LFUSE = 0x62
HFUSE = 0xdf
EFUSE = 0x00

## Generic 
FUSE_STRING = -U lfuse:w:$(LFUSE):m -U hfuse:w:$(HFUSE):m -U efuse:w:$(EFUSE):m 

fuses: 
    $(AVRDUDE) -c $(PROGRAMMER_TYPE) -p $(MCU) \
               $(PROGRAMMER_ARGS) $(FUSE_STRING)
show_fuses:
    $(AVRDUDE) -c $(PROGRAMMER_TYPE) -p $(MCU) $(PROGRAMMER_ARGS) -nv   

## Called with no extra definitions, sets to defaults
set_default_fuses:  FUSE_STRING = -U lfuse:w:$(LFUSE):m -U hfuse:w:$(HFUSE):m -U efuse:w:$(EFUSE):m 
set_default_fuses:  fuses

## Set the fuse byte for full-speed mode
## Note: can also be set in firmware for modern chips
set_fast_fuse: LFUSE = 0xE2
set_fast_fuse: FUSE_STRING = -U lfuse:w:$(LFUSE):m 
set_fast_fuse: fuses

## Set the EESAVE fuse byte to preserve EEPROM across flashes
set_eeprom_save_fuse: HFUSE = 0xD7
set_eeprom_save_fuse: FUSE_STRING = -U hfuse:w:$(HFUSE):m
set_eeprom_save_fuse: fuses

## Clear the EESAVE fuse byte
clear_eeprom_save_fuse: FUSE_STRING = -U hfuse:w:$(HFUSE):m
clear_eeprom_save_fuse: fuses

There is one more file to add:

Link to download env.make

Click on the link to download the file, your system might rename it. Make sure it is exactly env.make .

# Arduino UNO and compatible boards
MCU = atmega328p
SERIAL = /dev/cu.usbmodem141201
F_CPU = 16000000UL  
BAUD  = 9600UL
SOFT_RESET = 0
LIBDIR = ../../../Library
PROGRAMMER_TYPE = Arduino
PROGRAMMER_ARGS = -F -V -P $(SERIAL) -b 115200

Copy and paste this text into a file called env.make or move the file from your downloads folder into the test folder. There are now three original files in your test folder:

  • main.c
  • Makefile
  • env.make

In env.make, there is a change that need to be made. Make sure that SERIAL points to the right serial port for your system and Arduino. Use the serial port from the avrdude command which worked above into the right-hand side of the SERIAL line.

SERIAL = COM3

Once you have made the above changes, please try the following to test your setup:

  1. Manually delete all of the files EXCEPT the main.c, env.make and Makefile in the folder test.
  2. Run make flash, this will perform all of the tasks required to compile/link/load the program onto the Arduino Uno.

Hopefully, it all works the first time. If not, look at the errors to discern what needs to be fixed. For me, I had not changed my serial port (line 25 -P /dev/cu.usbmodem3101) to be the correct one.

Make and tabs

The make program requires tabs for indentation and not spaces. When you copy and paste text from this site, sometimes the tabs are replaced by a series of spaces by your text editor. If this happens, you will get errors like this:

Makefile:65: *** missing separator.  Stop.
# or
Makefile:125: *** target pattern contains no `%`.  Stop.

If you go to line 65, it will probably “look ok”, however, what you can not see is that the tabs have been replaced by spaces. The easiest way to fix (in a GUI editor) is to double-click on the spaces, which will select from the beginning of the line to the first word on the line, then hit tab. There won’t be an obvious change.

Now run the command again, and the error will probably occur line down where a tab has again, been replaced by spaces. I recommend you go through the file quickly, replacing the spaces in the indented lines with tabs. Run make again, and it will quite nicely point out the lines you missed. :)

If you continue to have problems, here is a grep command which will find all lines in a file which start with 3 spaces. And a perl command to replace all lines beginning with 4 spaces to beginning with a tab.

# to find all lines which start with 3 spaces as compared to a tab
grep -n "^    " Makefile
# to replace all lines which start with spaces with a tab
perl -pi -e 's/^    /\t/g' Makefile 

Now that it is working, I noticed the complete time from pressing enter to “avrdude done.” was 2-3 times faster than what the Arduino framework would take on a similar file.

Finish up

Once you have tested the Makefile and the program main.c, the folder test is no longer required.

Comments powered by Talkyard.