Developing in C for the ATmega328P: macOS Setup

11 minute read

Where I setup the Standard C toolchain (avr-gcc) for the ATmega328P on macOS using homebrew.

Test with the Arduino IDE!

After performing these steps on multiple machines, I’ve found it best to install and test using the Arduino IDE before going forward with the installation instructions below. This will reduce the errors to something more manageable and having the Arduino IDE is handy for its Serial Monitor as well it provides an easy method to which port the Uno is connected.

1. macOS Setup and Tool Chain Installation

For the macOS installation the installation will require using a package manager, in this case homebrew. If you don’t have it installed, you will need to do so before continuing. homebrew will require xcode command line tools to be installed as well.

Install the package manager, homebrew:

# this command installs command line tools only, not all of xcode 
xcode-select --install
# this command installs homebrew, a package manager for macOS
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# get the latest versions list
brew update

sudo Request

You might get a request: Checking for sudo access (which may request your password). This is required to setup the initial folder for installation. This github issue covers why:

“You’re correct that Homebrew does not need sudo when used. The installer however does so that it an create the necessary install directory /opt/homebrew and set up its permissions. The installer also uses sudo to install and setup the Xcode Command Line Tools if it is not installed. Once installed, Homebrew no longer requires sudo ever again.”

“If you can’t use sudo to install Homebrew, you still can install it without! Check here for alternative install instructions: https://docs.brew.sh/Installation#untar-anywhere. Note however that if you can’t install Homebrew to the default location (/opt/homebrew) due to permissions then you won’t be able to use some pre-built bottles and as such installing some formulae may build from source (which is a lot slower and more prone to errors).”

Install the tool chain

I followed these instructions for installing the AVR tool chain. It took a fair amount of time to install, I had no issues throughout the process. To install the tool chain, you will need to perform:

brew tap osx-cross/avr
# removal needed before upgrading
brew remove avr-gcc avr-binutils avr-libc
# avr-libc is now included in avr-gcc
brew install avr-gcc avr-binutils
brew install avrdude
brew install make
brew install git

CHECK AND CONFIRM

Once the installation completes, perform the following commands to confirm all applications were installed correctly. The application averdude provides a lot of output, only some is shown.

avr-gcc --version
make --version
git --version
avrdude
Check and Confirm the tool chain is installed

Check and Confirm the tool chain is installed

Large Version to see detail

Now, we have the same capability as the Arduino IDE, however, we are able to use it via the command line!

2. Test the tool chain

Test Code

You will want to select the code in the box below, copy it then you will paste it into a code editor window. In the past, I’ve recommended nano, however, its a bit tough to use if you are new to the command line. The best code editor, I’ve used on the Mac, is Sublime Text 4.

#include <avr/io.h>
#include <util/delay.h>
 
#define BLINK_DELAY_MS 1000
 
int main (void)
{
 /* set pin 5 of PORTB for output*/
 DDRB |= _BV(DDB5);
 
 while(1) {
  /* set pin 5 high to turn led on */
  PORTB |= _BV(PORTB5);
  _delay_ms(BLINK_DELAY_MS);
 
  /* set pin 5 low to turn led off */
  PORTB &= ~_BV(PORTB5);
  _delay_ms(BLINK_DELAY_MS);
 }
}

Testing the Code

In this step, we’ll setup a specific folder for developing C. I called it test. We’ll add a file called main.c, the we’ll compile/link the file on to the Uno.

cd
mkdir test
cd test 
# copy the file from above and we'll call it main.c
# open main.c in your code editor
# paste the file, save it and exit

Notes on the steps

  • Using the command cd without folder descriptor, will take you to your home folder. Using cd test will take you to the new folder test you just created. Once you have saved the file, go back to the Terminal program and you will be back at the command prompt. Enter the commands below to compile/link your file.
# copy and paste each line separately
avr-gcc -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o main.o main.c
avr-gcc -mmcu=atmega328p main.o -o main
avr-objcopy -O ihex -R .eeprom main main.hex

Your window will now look similar to this (both sets of operations shown:

Performing compile/link/load in the command line

Performing compile/link/load in the command line

Large Version to see detail

Our last step is to load the main.hex file on to the Uno. We use averdude to upload code to the Uno. We’ll use the following command:

avrdude -F -V -c arduino -p ATMEGA328P -P  /dev/tty.usbserial-01D5C0C7 -b 115200 -U flash:w:main.hex

Notice the "/dev/tty.usbserial-01D5C0C7"? You will want to change the “tty.usbserial-01D5C0C7” to the port name that you wrote down in the Arduino IDE port assignment in the earlier step Install the Arduino IDE. When you run the averdude command above successfully, you will see the following:

Successful averdude upload

Successful averdude upload

Large Version to see detail AND your Uno will be blinking at a much faster rate!

If you see this instead:

Typical averdude failure

Typical averdude failure

Large Version to see detail

  • Is the Arduino IDE installed, have you tested it, and it worked?
  • Have you closed the Arduino IDE?
  • Are you sure you are using the right port? Double check the number using the Arduino IDE and make sure the port name matches the port name on your averdude command. Ex: -P /dev/ttyACM0

3. 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.

Link to download Makefile

Here is the file:

##########------------------------------------------------------##########
##########  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
CFLAGS = -Os -mcall-prologues -g3 -std=gnu99 -Wall -Werror -Wundef
# 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

Copy all of the text above and save it in a file called Makefile, in the same folder as main.c.

Link to download env.make

There is one more file to add:

# Arduino UNO and compatible boards environment variables
MCU = atmega328p
SERIAL = /dev/cu.usbserial-01D5C0C7
F_CPU = 16000000UL  
BAUD  = 9600UL
PROGRAMMER_TYPE = Arduino
PROGRAMMER_ARGS = -F -V -P $(SERIAL) -b 115200

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

  • main.c
  • Makefile
  • env.make

Note: Sometimes when downloading the file name gets set to env.make.html and Makefile.html, if so, use the Finder to delete the “.html” at the end of the filename.

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 = /dev/cu.usbserial-01D5BFFC

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.

A correct run of make flash in the Terminal would look like this:

macOS Successful run of make flash

macOS Successful run of make flash

Large Version to see detail

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.