Developing in C for the ATmega328P: Linux Setup

12 minute read

Where I setup the Standard C toolchain for the ATmega328P for Linux (and WSL).

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.

Linux Serial Permissions

Linux also has permissions issues with its serial ports. My recommendation is this (also shown at the bottom of this page). This step is not required for WSL.

Linux Setup and Tool Chain Installation

For the Linux installation (and with WSL), it can’t get much easier.

1. Install the tool chain

To install the tool chain, you will need to perform one simple operation:

# Bring up the command line and enter
sudo apt install gcc-avr binutils-avr gdb-avr avr-libc avrdude make git

2. 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 nano editor window.

#include <avr/io.h>
#include <util/delay.h>
 
#define BLINK_DELAY_MS 100
 
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
nano main.c
# paste the file, save it and exit

Notes on the steps

  • I’m using nano as my editor to keep this simple, this will be a copy and paste from code above into a file on your system.
  • Using the command cd without folder descriptor, will take you to your Linux home folder. Using cd test will take you to the new folder test you just created.
  • nano main.c will open a file called main.c in the nano editor. The window will be empty as main.c is a new file. Once you paste the file into nano, it will look like this:
    main.c in the nano editor

    main.c in the nano editor

Large Version to see detail

To save and exit nano, press Ctrl-X, y (to confirm saving) and Return (to confirm file name). Once you have saved the file, you will be back at the WSL prompt. Enter the commands below to compile/link your file.

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:

main.c in the nano editor

main.c in the nano editor

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/ttyACM0 -b 115200 -U flash:w:main.hex

Notice the "/dev/ttyACM0"? You will want to change the “ttyACM0” to the port name that you wrote down in the Arduino IDE port assignment in the earlier step Install the Arduino IDE. Note: The two versions I have seen are "/dev/ttyACM0" and "/dev/ttyUSB0". 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

WSL Notes

WSL will use a different name than Linux. Notice the ttyACM0. It needs to look like "/dev/ttyS3". You will want to change the “3” to the number that follows the “COM” in the Arduino IDE port assignment in the earlier step Install the Arduino IDE.

Note: Two things:

  1. You need to perform the Arduino IDE step to get the port number.
  2. Opening the port via the Arduino IDE will allow WSL to access the port (See Fail note below.) When you run the averdude command above successfully, you will see the following:
    main.c in the nano editor

    main.c in the nano editor

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

If you see this instead:

main.c in the nano editor

main.c in the nano editor

Large Version to see detail

  • Is the Arduino IDE installed, have you tested it, and it worked?
  • Have you opened the Arduino IDE and tested it since you last rebooted? (Required by WSL to recognize the port.)
  • Have you closed the Arduino IDE?
  • Are you sure you are using the right port? Double check the number on the successful COM port matches the number on your averdude port. Ex: COM6 => /dev/ttyS6

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. 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
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 or move the file from your downloads folder into the test folder.

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 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 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 = /dev/ttyACM0

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.

Linux Serial Permissions

These steps are required for Linux, however, not for WSL.

  1. Run dmesg to determine idVendor and idProduct of the USB interfaces (in this case there are two.)
  2. Use sudo and your favorite editor to create a file: “/etc/udev/rules.d/50-myusb.rules”
  3. Using the idVendor and idProduct numbers, create two lines in the file using this format (replacing the numbers shown with your numbers):

SUBSYSTEMS==“usb”, ATTRS{idVendor}==“2e8a”, ATTRS{idProduct}==“0004”, GROUP=“plugdev”, MODE=“0660”, TAG+=“uaccess”

  1. Save and close the file then restart your system.
# use dmesg to determine idVendor and idProduct
# be sure to use the right USB devices (such as usb 1-1 and usb 1-2 below)
dmesg
# ...
[62603.487834] usb 1-1: New USB device found, idVendor=0403, idProduct=6001, bcdDevice= 6.00
...
[62603.492032] usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0
...
[62607.143187] usb 1-2: New USB device found, idVendor=2e8a, idProduct=0004, bcdDevice= 1.00
...
[62607.145394] cdc_acm 1-2:1.0: ttyACM0: USB ACM device
# ...
sudo nano /etc/udev/rules.d/50-myusb.rules
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="plugdev", MODE="0660", TAG+="uaccess"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", GROUP="plugdev", MODE="0660", TAG+="uaccess"
# save the file then reboot your system

Comments powered by Talkyard.