What Does My AVR Compiler Do For Me?

Jack McLeans
6 min readDec 27, 2021

--

Automating your AVR microcontroller build system using Makefiles.

Before embarking on any embedded systems development project one of the most important tasks is to set up your development toolchain. This is the set of tools (often programs) that help you compile your software down to the required platform binary. For most AVR development the Arduino platform is the most preferred for beginners. In this platform, the build system hidden behind a “Verify” button which makes it easy to hit the ground running with your first “blinky” project. But what really happens when we press the verify button? To properly understand this we’ll have to take a peek beneath the hood.

AVR Toolchain

As discussed, these are the set of tools that are required to compile our software to obtain the required binary file. The AVR toolchain consists of :

- The C/C++ cross compiler.
- An Assembler and Linker.
- C/C++ Libraries for developing C/C++ programs.
- AVR Program Downloader(AVRDUDE).

Resources and Prerequisites

- Installing avr-gcc compiler.
- Installing AVRDUDE.
- Installing Make.

Installing these resources for linux and mac platforms can be found here.

AVR C/C++ cross compiler

The AVR based cross compiler for C programs is known as “avr-gcc” while that for C++ programs is “avr-g++”. The Arduino IDE invokes the “avr-g++” for its compilation hence an indication of it being a C++ based platform. This cross compiler enables us to compile for our target architecture on the host machine on which we are writing our software. The generated binary file can then be uploaded onto our target device.

Let us create a sample program “main.cpp” that does a simple task, blink and LED connected to Port B , Pin 5 of the Atmega328P microcontroller. (We shall look into the ins and outs of this program in a future blog). This should compile on your Arduino IDE and blink the LED on Digital Pin 13 every one second.

#include <avr/io.h>
#include <util/delay.h>
int main(){
/*
* Set the pin direction to output, this is done by shifting a 1 to bit 5 of the DDRB register
*/
DDRB |= (1 << PIN5);
while(1){
/* To set the pin to HIGH we shift a 1 to bit 5 of the PORTB register */
PORTB |= (1 << PIN5);
/* Delay for 1 second */
_delay_ms(1000);
/* To set the pin back to LOW we shift a zero to bit 5 of the PORTB register (This is achieved by a not (~) operation followed by an and (&) operation with 1 on bit 5)*/
PORTB &= ~(1 << PIN5);
/* Delay for 1 second */
_delay_ms(1000);
}}

The First step will be to run this program through the compiler to obtain an object file with a “.o” extension. This is the output file format of the c++ compiler (at-least so far, the C++20 standard introduces modules and this might change for such applications)

avr-g++  -Os -D F_CPU=16000000 -mmcu=atmega328p -c main.cpp

Running this command will produce a “main.o” output file, this is the object file. If we had more than one source files e.g a “USART.cpp” file for the Microcontroller USART driver then it would also be added to the compilation command to produce a “USART.o” object file.

avr-g++  -Os -D F_CPU=16000000 -mmcu=atmega328p -c main.cpp USART.cpp

“avr-g++” invokes the c++ compiler, for C programs you’d want to invoke the “avr-gcc” compiler. “-Os” is used to enable optimisation for our software to produce efficient code. “-D F_CPU” flag is used to define the clock speed of the target microcontroller, in this case the chip is clocked at 16MHz. “-mmcu” flag declares the target microcontroller which in our case is the atmega328p, should be changed to the atmega2560 to use with the arduino-mega. “-c” flag shows that we want to compile the source files listed after this flag.

AVR C/C++ Linker

After obtaining the required object files, the next process will be to link them to produce a single executable/binary file that can be uploaded into our microcontroller. This is achieved by invoking the linker on the previously generated object files.

avr-g++ -D F_CPU=16000000 -mmcu=atmega328p -o main.elf main.o

The Linking command also uses a number of flags (some we have gone through during the compilation face). “-o” flag is followed by the output file format which in our case will be the “main.elf” aka Executable and Linkable Format. This is then followed by a list of object file(s) to be linked together e.g “main.o”. This process outputs a “main.elf” executable file.

Most microcontrollers expect a “.bin” or “.hex” file format as their executable format. To achieve this we’ll have to copy the contents of our “.elf” file into a “. hex”. “avr-objcopy” is a tool meant for just this purpose.

avr-objcopy -O ihex main.elf main.hex

We now have a main.hex file that is ready to be flashed into out microcontroller, how do we get it there?

AVRDUDE

This is a utility to download/upload/manipulate the ROM and EEPROM contents of AVR microcontrollers using the in-system programming technique (ISP). We will use it to flash our hex file onto our target microcontroller.

avrdude -c usbasp -p m328p -P usb -U flash:w:main.hex

The example above uses the USBASP avr programmer. To use the USB port provided on your arduino board the command changes to

avrdude -c arduino -p m328p -P COM5 -U flash:w:main.hex

COM5 should be replaced by the actual COM Port your arduino is connected to. This can be found by checking your device manager in a windows system or running the “ls /dev” command on a linux or mac system.

Makefiles

During embedded development, it is essential to automate your build system. Instead of typing out the commands we have looked at every time you want to compile your software a Makefile would help with this. The commands are organised in a particular order within the file and special commands can be used to execute them.

To create one, we create a new file in the same directory as our “main.cpp” and name it “Makefile”. Note that the name has to be just that and is case sensitive. Inside the Makefile we will add our commands.

all :
avr-g++ -Os -D F_CPU=16000000 -mmcu=atmega328p -c main.cpp
avr-g++ -D F_CPU=16000000 -mmcu=atmega328p -o main.elf main.o
avr-objcopy -O ihex main.elf main.hex
flash :
avrdude -c usbasp -p m328p -P usb -U flash:w:main.hex
clean :
rm *.hex *.o *.elf

Now that we have all the commands into our Makefile we can easily generate our hex file by typing the command “make all”. This will run all the commands under the “all” tag in the order in which they appear.

To load the binary onto our microcontroller, after plugging in the USB cable we type “make flash” and to delete all the build files we use “make clean”.

To Learn about Makefiles and how to use them take some time and go through this tutorial.

Makefiles also play an important role when working in teams. How do you ensure that your code isn’t broken by changes made by one of your colleagues or you in the future? Yes, unit-tests. With Makefiles you can have a test tag where typing “make tests” would build your code and run all your test cases. The current CI and CD tools such as GitHub actions also have the ability to run Makefiles. You could have your tests running automatically anytime code is pushed into a repository or before any requests are merged and schedule alerts whenever these fail.

Larger codebases require a reliable build system that can be tweaked to suit the team’s needs, Makefiles give this type of flexibility and should be encouraged as a part of embedded development tooling. Unit-tests also help prevent brittle code that fails in many undetermined conditions from reaching production.

--

--

Jack McLeans

Electronics Engineer | Embedded Systems | Firmware Engineer | Internet of Things | Open Source | KiCAD | EAGLE | C++ Programming | Qt | Python