Using Makefiles to Automate Development

10 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 significantly. 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:

Download Makefile

Click on the link to download the file or copy and paste the text below, your system might name it “Makefile.txt”, rename it to “Makefile”. If you have errors from make, be sure to review the note below.

##########------------------------------------------------------##########
##########  System-specific Details                             ##########
##########  are contained in root-level file: env.make          ##########
##########  edit to change local/board/project parameters       ##########
##########------------------------------------------------------##########
include $(DEPTH)env.make
##########------------------------------------------------------##########
##########                  Program Locations                   ##########
##########     Be sure to set TOOLCHAIN and OS in env.make      ##########
##########------------------------------------------------------##########

ifeq ($(TOOLCHAIN),arduino)
    ifeq ($(OS),mac)
        BIN = /Applications/Arduino.app/Contents/Java/hardware/tools/avr/bin/
        AVRDUDECONF = -C /Applications/Arduino.app/Contents/Java/hardware/arduino/avr/bootloaders/gemma/avrdude.conf
    endif
    ifeq ($(OS),windows)
        BIN = 'C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\'
        AVRDUDECONF = '-CC:\Program Files (x86)\Arduino\hardware\arduino\avr\bootloaders\gemma\avrdude.conf'
    endif
    ifeq ($(OS),raspberry)
        BIN = /usr/local/arduino/hardware/tools/avr/bin/
        AVRDUDECONF = -C /usr/local/arduino/hardware/arduino/avr/bootloaders/gemma/avrdude.conf
    endif

else
    BIN =
    AVRDUDECONF = 
endif

CC = $(BIN)avr-gcc
OBJCOPY = $(BIN)avr-objcopy
OBJDUMP = $(BIN)avr-objdump
AVRSIZE = $(BIN)avr-size
AVRDUDE = $(BIN)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.

ifeq ($(LIBRARY),no_lib)
    SOURCES=$(wildcard *.c )
    CPPFLAGS = -DF_CPU=$(F_CPU) -DUSB_BAUD=$(USB_BAUD)  -DSOFT_BAUD=$(SOFT_BAUD)  \
    -DSOFT_RESET=$(SOFT_RESET) -DTC3_RESET=$(TC3_RESET)

else
    SOURCES=$(wildcard *.c $(LIBDIR)/*.c)
    CPPFLAGS = -DF_CPU=$(F_CPU) -DUSB_BAUD=$(USB_BAUD)   -DSOFT_BAUD=$(SOFT_BAUD) -I. \
    -I$(LIBDIR) -DSOFT_RESET=$(SOFT_RESET) -DTC3_RESET=$(TC3_RESET)
endif

# See Note re: CPPFLAGS if using/not using LIBDIR, pick only one LIB or NO_LIB
# LIB - Uncomment if the AVR_C Library is required (default), also 
# uncomment LIB below in CPPFLAGS (and comment NO_LIB)
# SOURCES=$(wildcard *.c $(LIBDIR)/*.c)

# NO_LIB - Uncomment if you wish the smallest code size and DON'T
# require AVR_C Library (and comment LIB)
# SOURCES=$(wildcard *.c )

OBJECTS=$(SOURCES:.c=.o)
HEADERS=$(SOURCES:.c=.h)

## Compilation options, type man avr-gcc if you're curious. 

# use below to setup gdb and debugging
CFLAGS = -Og -ggdb -std=gnu99 -Wall -Wundef -Werror -Wno-aggressive-loop-optimizations
# 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
# if attempting to use %S format specification (strings in progmem), uncomment next line
CFLAGS += -Wno-format
LDFLAGS = -Wl,-Map,$(TARGET).map 
## Optional, but often ends up with smaller code
LDFLAGS += -Wl,--gc-sections 
# Uncomment line below to add timestamp wrapper to printf() OR
# Comment line below, if  undefined reference to `__wrap_printf'
# LDFLAGS += -Wl,--wrap=printf
## 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_clean verbose

compile: $(TARGET).hex 

static: 
    cppcheck --std=c99 --platform=avr8 --enable=all --suppressions-list=$(DEPTH)suppressions.txt . 2> cppcheck.txt

env:
    @echo "MCU:"  $(MCU)
    @echo "SERIAL:"  $(SERIAL)
    @echo "F_CPU:" $(F_CPU)
    @echo "USB_BAUD:"  $(USB_BAUD)
    @echo "SOFT_RESET:"  $(SOFT_RESET)
    @echo "LIB_DIR:"  $(LIBDIR)
    @echo "LIBRARY:"  $(LIBRARY)
    @echo "PROGRAMMER_TYPE:"  $(PROGRAMMER_TYPE)
    @echo "PROGRAMMER_ARGS:"  $(PROGRAMMER_ARGS)
    @echo "TOOLCHAIN:"  $(TOOLCHAIN)
    @echo "OS:"  $(OS)
    @echo "BIN:"  $(BIN)
    @echo "TC3_RESET:"  $(TC3_RESET)
    @echo "SOFT_BAUD:"  $(SOFT_BAUD)
    @echo
    @echo "Source files:"   $(SOURCES)
    @echo   

help:
    @echo "make compile - compile only (Arduino verify)"
    @echo "make flash - show program size and flash to board (Arduino upload)"
    @echo "make clean - delete all non-source files in folder"
    @echo "make complete - delete all .o files in folder & Library then verbose flash, for complete rebuild/upload"
    @echo "make verbose - make flash with more programming information for debugging upload"
    @echo "make env - print active env.make variables"
    @echo "make size - print size information of elf file"
    @echo "make help - print this message"

# 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) -G --mcu=$(MCU) $(TARGET).elf
    $(OBJDUMP) -Pmem-usage $(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 $(LIBDIR)/*.o

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

flash: $(TARGET).hex $(TARGET).lst size
    @echo "use make verbose to see complete programming information"
    $(AVRDUDE) -q -q $(AVRDUDECONF) -c $(PROGRAMMER_TYPE) -p $(MCU) $(PROGRAMMER_ARGS) -U flash:w:$<

verbose: $(TARGET).hex $(TARGET).lst size
    $(AVRDUDE) -v -v $(AVRDUDECONF) -c $(PROGRAMMER_TYPE) -p $(MCU) $(PROGRAMMER_ARGS) -U flash:w:$<

## An alias
program: flash

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

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

##########------------------------------------------------------##########
##########       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:

Download env.make

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

# Environmental variables for specific boards
# See https://wellys.com/posts/avr_c_make_part2/ for more information
# Uncomment entire block less top line of block
# After switching boards, Library MUST BE RE-COMPILED
# Use "make LIB_clean && make all_clean && make flash" for a complete re-compile

# Baud rates to 250000 have been tested and work on Uno R3
# Baud rates above 230000 might not work on some ports or serial software
# TC3_RESET is only applicable to ATmega328PB, set to 0 for ATmega328P

# Example Serial Ports on Mac
# /dev/cu.usbserial-01D5BFFC
# /dev/cu.usbmodem5101
# /dev/cu.usbmodem3301
# /dev/cu.usbserial-AB0JQEUX
# /dev/cu.usbmodem14101

# Example Serial Ports on Linux
# /dev/ttyACM0

# Example Serial Ports on Windows
# COM3
# COM4
# COM9

# Using Arduino tools vs. GCC native
# For Arduino tool chain
# TOOLCHAIN [ arduino ]
# OS: [mac | windows | raspberry ]
# For GCC native, both TOOLCHAIN and OS need to be blank

# To reduce code size dramatically by not using AVR_C library, 
# set LIBRARY = no_lib, see examples/blink_avr
# All functions must be in avr-libc (standard library), main.c or files in folder
# otherwise, leave blank

# Arduino UNO et al using Optiboot (standard Arduino IDE approach)
MCU = atmega328p
SERIAL = /dev/ttyACM0
F_CPU = 16000000UL
USB_BAUD = 250000UL
SOFT_RESET = 0
LIBDIR = $(DEPTH)Library
LIBRARY = 
PROGRAMMER_TYPE = arduino
PROGRAMMER_ARGS = -F -V -P $(SERIAL) -b 115200
TOOLCHAIN =
OS =
TC3_RESET = 0
SOFT_BAUD = 28800UL


# Arduino UNO and compatible boards using Atmel-ICE Debugger in atmelice_isp mode
# MCU = atmega328p
# SERIAL = /dev/cu.usbserial-01D5BFFC
# F_CPU = 16000000UL
# USB_BAUD  = 250000UL
# SOFT_RESET = 0
# LIBDIR = $(DEPTH)Library
# LIBRARY =
# PROGRAMMER_TYPE = atmelice_isp
# PROGRAMMER_ARGS = -F -V -P usb -b 115200
# TOOLCHAIN = 
# OS = mac
# TC3_RESET = 0
# SOFT_BAUD = 28800UL

# Arduino UNO and compatible boards using Atmel Dragon
# MCU = atmega328p
# SERIAL = /dev/cu.usbserial-01D5BFFC
# F_CPU = 16000000UL
# USB_BAUD  = 250000UL
# SOFT_RESET = 0
# LIBDIR = $(DEPTH)Library
# LIBRARY =
# PROGRAMMER_TYPE = dragon
# PROGRAMMER_ARGS =   -c dragon_isp -P usb
# TOOLCHAIN = 
# OS = mac
# TC3_RESET = 0
# SOFT_BAUD = 28800UL

# Arduino UNO and compatible boards using Atmel SNAP in ISP mode
# MCU = atmega328p
# SERIAL = /dev/tty.usbmodem4101
# F_CPU = 16000000UL
# USB_BAUD  = 250000UL
# SOFT_RESET = 0
# LIBDIR = $(DEPTH)Library
# LIBRARY =
# PROGRAMMER_TYPE = snap_isp
# PROGRAMMER_ARGS = -P usb
# TOOLCHAIN = 
# OS = mac
# TC3_RESET = 0
# SOFT_BAUD = 28800UL

# Microchip 328PB Xplained Mini board
# MCU = atmega328pb
# SERIAL = /dev/tty.usbmodem4101
# F_CPU = 16000000UL
# USB_BAUD  = 250000UL
# SOFT_RESET = 1
# LIBDIR = $(DEPTH)Library
# LIBRARY =
# PROGRAMMER_TYPE = xplainedmini
# PROGRAMMER_ARGS =
# TOOLCHAIN = 
# OS = mac
# TC3_RESET = 0
# SOFT_BAUD = 28800UL

# Microchip Curiousity Nano AVR64DD32
# avrdude -p avr64dd32 -c pkobn_updi -P usb -t
# MCU = avr64dd32
# SERIAL = /dev/ttyACM0
# F_CPU = 16000000UL
# USB_BAUD  = 250000UL
# SOFT_RESET = 0
# LIBDIR = $(DEPTH)Library
# LIBRARY =
# PROGRAMMER_TYPE = pkobn_updi
# PROGRAMMER_ARGS =
# TOOLCHAIN = 
# OS = mac
# TC3_RESET = 0
# SOFT_BAUD = 28800UL
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.