Developing in C for the ATmega328P: Using Windows 10

13 minute read

Where I setup the Standard C tool chain (avr-gcc) for the ATmega328P on Windows 10.

If you are looking to install the AVR toolchain for C using Windows Subsystem for Linux, go here.

Sources

Background

Windows has two different operating environments, Windows (the Graphical User Interface or GUI) and the command line interface (CLI). The former is the typical method of using Windows, particularly when using applications such as Word, Excel and Powerpoint. The latter is used for program automation and Windows program development and it is typically entered by typing cmd in the Windows search bar. We’ll introduce a new and improved method, shortly.

We want to use the command line environment to develop C software for the Uno. Unfortunately, the GCC compiler for C while intended for use in a command line environment, the environment is expected to be Linux and not Windows CLI.

To resolve this issue we’ll do two things, we’ll install the Windows Terminal to interface with the CLI and a special set of applications that were compiled to run under Windows.

Start by testing 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 identify the serial port the Uno is connected.

If you haven’t installed the Arduino IDE, I would recommend installing from the Windows Store, it is the easiest process. Once installed, connect your Uno to your computer and compile and upload the blink sketch. Don’t continue until you’ve been successful, otherwise the remaining instructions will be far too difficult to complete.

Introduction

Microsoft has introduced a new application for interacting with the command line. The new application called Windows Terminal or Terminal will work with the Windows CLI environment, Azure Cloud environments and with Windows Subsystem for Linux.

Install Windows Terminal

Install the Windows Terminal application via the Windows Store. Be sure the version you install was developed by Microsoft. Once installed, you may open using the Open button in the Store or in subsequent uses, search for Terminal in the Windows Search bar and open it there. It might be helpful to pin to the Taskbar for future access.

When it is first opened, it will be in the Command prompt or CLI environment. This is the environment we’ll need to use, to develop code using the C tool chain for the Uno.

Terminal Opens in Windows Environment

Terminal Opens in Windows Environment

Large Version to see detail

Install the Tool Chain

Download the avr-gcc directory

We can’t use the Linux-based version of the tool chain, as it won’t run in the Windows CLI. We’ll need to download and install a special set of applications, which have been created specifically to run in Windows CLI. Go to AVR-GCC 12.1.0 for Windows 32 and 64 bit and download the latest, which will probably be version 12.1.0. It will be a zip file, so we’ll need to expand it and place the entire contents in a program folder.

Install the avr-gcc directory

To install:

  1. Extract the contents of the zip file by right clicking on the file and select “Extract All…”.
  2. Have it to extract to the Downloads directory (current directory) by hitting return.
  3. Open another Explorer window and in that window click on C:, your main drive.
  4. Select the folder “Program Files(x86)” then drag and drop the “avr-gcc 12.10…” directory into the “Program Files(x86)” directory.

Add Folder to Execution Path

Once we have it where we want to run it, we need to add that location to our execution path.

To do this, go to the Search bar and begin to enter environment, this will bring up the “Edit the System Environment Variables Control Panel” in the results window, click on it.

Search bar for environment

Search bar for environment

Large Version to see detail

This will open a window called System Properties, click on Environment Variables… at the bottom

Click on Environment Variables...

Click on Environment Variables...

Large Version to see detail

In the next window, select Path, in the top window and click on Edit… under the top window.

select Path...click on Edit...

select Path...click on Edit...

Large Version to see detail

In the Edit environment variable window, click first on New then second on Browse.

Click first on New then second on Browse

Click first on New then second on Browse

Large Version to see detail

Then broswe to the location where you stored avr-gcc… above, …Program Files(x86…) and select the bin directory in avr-gcc… then press OK. (You might need to click on This PC to see the C: drive.)

Select bin then click OK

Select bin then click OK

Large Version to see detail

Then click on OK on each window which has opened to back out of the exercise.

Confirm Installation

Open a Terminal window by typing Terminal in the Windows search bar and hitting return. In the Terminal window, enter the following commands. Type or copy/paste each command separately, pressing return after each one:

avr-gcc --version
make --version
git --version
avrdude

Confirm each component is installed

Confirm each component is installed

Large Version to see detail

The application avrdude will show more detail, however, on the last line of the printout, the version number needs to be 7.0.

Congratulations, you are now ready to code in C for the Arduino Uno! Let’s start!

Use the tool chain

Before you start, you might need one more application, a code editor. A code editor is similar to a word processor such as Microsoft Word, however, it provides important capabilities like text only (no special characters) and it will highlight the syntax of the code being written. A very popular and free version for Windows is Notepad++. I will use it for the following examples. I recommend you install Notepad++ or use one that is to your liking.

Test Code

You will want to select the code in the box below, copy it then you will paste it into Notepad++ 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
start notepad++ main.c
# paste the contents above into the file and save it

Notes on the steps

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

    main.c in the Notepad++ editor

Large Version to see detail

Enter the commands below to compile/link your file. I recommend using copy and paste, attempting to type them in by hand might be difficult. As we did with testing the versions of the software, copy each command and paste it into the Terminal window, then hit return. T

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, note that the compile, link and load operations won’t have output (unless you have errors.):

main.c in the Notepad++ editor

main.c in the Notepad++ 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 COM3 -b 115200 -U flash:w:main.hex

Notice the “COM3”? You will want to change the “COM3” to the port name that you wrote down in the Arduino IDE port assignment in the earlier step. 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.

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
# 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 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 = COM3

There is a bug in gcc 12.0 which causes the compiler to fail when its compiling for the ATmega328P (Uno chip). To fix this:

  1. Open the Makefile in Notepad++
  2. Comment line 47 (line 2 below) by placing a “# " at the beginning of the line
  3. Uncomment the line 51 (line 6 below) by deleting the “# " at the beginning of the line (to comment, place a “# " at the beginning of the line):
    1
    2
    3
    4
    5
    6
    
    # 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 
    

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.

Finish up

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

Comments powered by Talkyard.