AVR Programming Tutorial

Introduction

This tutorial introduces programming for AVR microcontrollers. It uses the ATmega328p found on Arduino boards, but works with straight AVR C and a bare microcontroller. It covers the basic information you need to get started, whether or not you've used Arduino or other microcontrollers previously.

Why would you want to learn AVR programming (instead of, say, using Arduino)? First, it should demystify what happens inside of the Arduino platform (which is really fairly simple). Second, it will give you the flexibility to use other microcontrollers, like the ATtiny series, which are smaller and cheaper than the ATmega328. Third, it will help you take full advantage of the microcontroller's capabilities, which can be important when writing performance-critical applications.

AVR Toolchain

Download and install:

  • Mac OS X: CrossPack
  • Windows: WinAVR
  • Linux: gcc-avr, avr-libc, avrdude packages

Examples

Download the examples: avr-examples.zip

Materials

You'll need the following:

For more information, see our list of materials and parts and our list of prototyping supplies.

Microcontroller (ATmega328p)

We're using the ATmega328p microcontroller. You'll want to download the datasheet. Here's the pinout of the DIP (through-hole) package. The pins are numbered on the inside and labelled on the outside. In parentheses are abbreviations for some of the unique functions of each pin.

Connect your Programmer

To load programmers onto the ATmega328p, you need an in-system programmer (ISP). There are a few of these, each with slight differences.

Using an Arduino as an ISP

Connecting the Arduino and ATmega328

Here, we're using the Arduino board to program the ATmega328 as well as supply power to it. Notice we only need a single component besides the microcontroller: the 10K resistor (brown, black, orange) pulling the reset line high.

Turning your Arduino into an AVR In-System Programmer

Arduino 0018 (download) comes with a firmware (sketch) that turns your Arduino board into an AVR in-system programmer, allowing you to upload programs onto other microcontrollers. To use it, upload the ArduinoISP example to your Arduino.

AVRISP mkII

If you want to use an AVRISP mkII, you'll need to provide power separately. Here we use a 9V battery and a voltage regulator to bring this down to five volts.

Insert the 7805 voltage regulator into your breadboard, and add an 0.1 uF capacitor across the output and ground pins.

Δ

Attach a battery and use a multimeter to confirm that the regulator is producing ~5V output.

Δ

Add the microcontroller to the setup, and make sure that all of the GND and VCC pins are connected properly.

Attach:Microcontroller_added.jpg Δ Δ

Here's the pinout for the AVR ISP connector. Remember that the red wire in the ribbon cable indicated pin 1, and that this diagram shows the connections as viewed from the back of the connector, i.e. if you're looking at the holes, this diagram is reversed..

Attach:AVRISP_pinout.png Δ Δ

If you are using the 7805 regulator to power your microcontroller and the AVRISPmkII to program it, your completed setup (including 10k pullup resistor on the reset pin) should look something like this. Notice that the status light on the programmer turns green when everything is connected properly. If is flashes yellow, one or more of your connections may be wrong. If the light turns red, check your power supply.

Attach:Completed_setup.jpg Δ Δ

FabISP

Verifying the Circuit and Software

To check that your software installation is working and your circuit is correct, you'll need to run a command in a command prompt (or "terminal"). To open a command prompt, run:

  • Mac OS X: Terminal (in Applications > Utilities)
  • Windows: cmd from the run option in the Start menu
  • Linux: look for something called "terminal" or "xterm"

Once at a command prompt, run the following command:

  • Arduino as ISP: avrdude -p m328p -c stk500v1 -b 19200 -P <PORT>, where <PORT> is:
    • Mac OS X: /dev/tty.usbserial-*
    • Windows: COM3, for example; use the COM port that your Arduino board is on
    • Linux: /dev/ttyUSB0
  • AVRISP mkII: avrdude -p m328p -c avrisp2 -P usb
  • FabISP: avrdude -p m328p -c usbtiny

You should get:

Reading | ################################################## | 100% 0.01s

avrdude: Device signature = 0x1e950f

avrdude: safemode: Fuses OK

avrdude done.  Thank you.

which means that the avrdude has talked to your ISP, the ISP has talked to the chip, the chip has reported its device signature (here 0x1e950f ) back to the ISP, which passed it on to avrdude, which confirmed that it matched the device signature of the microcontroller you specified in the -c parameter (the ATmega328p).

Possible errors:

avrdude: ser_open(): can't open device "/dev/tty.usbserial-*": No such file or directory
  • Is your Arduino board connected? Is the power LED on?
avrdude: stk500_recv(): programmer is not responding
  • Does the ATmega328p have power? Is it wired correctly? Did you upload the ArduinoISP sketch to your Arduino board?
avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.13s

avrdude: Device signature !!0x000000
avrdude: Yikes!  Invalid device signature.
         Double check connections and try again, or use -F to override
         this check.
  • Is the ATmega328 connected to the Arduino correctly?
avrdude: ser_open(): can't open device "\\.\COM3": The system cannot find the file specified.
  • Did you use the right COM port? (Windows)

avrdude

The program avrdude lets you upload compiled programs (hex files) to your microcontroller. (It's what the Arduino development environment uses to upload sketches to the Arduino board.) You run it from the command line, with various parameters:

-p part
Specifies the microcontroller (part) you're programming, e.g. atmega328p

-P port
The port you're uploading to. If you are using an Arduino board as a programmer, this will be something like /dev/tty.usbserial-A6008b1a on the Mac (or you can just use /dev/tty.usbserial-* to select it automatically). On Windows, it's COM or similar (you can check in the Device Manager). On Linux, it's probably /dev/ttyUSB0. If you're using an AVRISP mkII, it will be usb instead on all three operating systems. For a USBtinyISP or a FabISP, you omit this parameter entirely.

-c protocol
The protocol to use for uploading. If using an Arduino board as a programmer, this should be stk500v1. For an AVRISP mkII, avrisp2. For the FabISP or a USBtinyISP, it's usbtiny.

-b baud rate
The baud rate to use for uploading. For an Arduino as programmer, use 19200. If using an AVRISP mkII, USBtinyISP, or FabISP, this parameter can be omitted altogether.

Connecting an LED

Now that you've got the basic circuit working, let's hook up an LED so we can see the microcontroller do something. Connect the longer leg of the LED to pin PB0 (pin 14 of the ATmega328p), and the shorter leg goes through a 330Ω (orange orange brown) resistor to ground. If you look carefully at the LED, you'll notice that one side of it is flat - the leg on that side goes to ground.

A First Program

Below is one of the simplest programs you can write. It sets pin PB0 as an output and then sets it high.

#include <avr/io.h>

int main(void)
{
    DDRB = 1;
    PORTB = 1;
}

Save it as main.c in a convenient directory.

Compilation and Uploading

To get your program onto the microcontroller, you'll need to compile it (translate it into a machine-readable format) and upload it (transfer it to the microcontroller). You do this from the command line; if you're not familiar with it, you'll want to read this page on the command line?]].

Once you cd to the directory containing your program (main.c), you can compile the program with the following commands:

avr-gcc -mmcu=atmega328p -o main.out main.c
avr-objcopy -O ihex main.out main.hex

This compilation translates the code you typed into machine code that the microcontroller can understand, stored in the main.hex file.

You can upload this hex file with avrdude, adding one extra parameter to the command we used above (to check that our microcontroller was connected properly). We'll use -U flash:w:main.hex to tell avrdude to upload the main.hex file and write it to the flash memory of the microcontroller.

So, the full command looks like:

  • Arduino as ISP: avrdude -p m328p -c stk500v1 -b 19200 -P <PORT> -U flash:w:main.hex, where <PORT> is:
    • Mac OS X: /dev/tty.usbserial-*
    • Windows: COM3, for example; use the COM port that your Arduino board is on
    • Linux: /dev/ttyUSB0
  • AVRISP mkII: avrdude -p m328p -c avrisp2 -P usb -U flash:w:main.hex
  • FabISP: avrdude -p m328p -c usbtiny -U flash:w:main.hex

The LED should light up.

C Programs

The program we loaded on the ATmega328 is written in C, a popular programming language for microcontrollers and low-level software on computers. It's designed to be portable across various platforms, which means that it needs to be translated into lower-level code that can run on various processors. This translation is done by a program called a compiler. It turns the C code into, in our case, a .hex file that contains the raw bytes to be loaded onto the ATmega328.

Libraries and Header Files

When you write a C program, you typically make use of existing code libraries that provide common functionality. The C compiler that targets the microcontrollers we're using includes a library called AVR Libc. To use the functions contained in this library, we need to include various header files in our program. These header files contain specifications for the functions in the library, which allows the compiler to ensure that our program uses them correctly. To include a header file, you use a statement like:

#include <avr/io.h>

This particular header file contains definitions for the input and output (i/o) pins available for the various AVR microcontrollers. We'll use it in all our programs.

The main() function

C programs include a function (block of code) called main(). This function is called when the program starts. The body of the function (the lines of code it contains) are delimited by curly braces {}. Don't worry about the words int and void for now.

Binary, Bits, Bytes, and Registers

To understand what the our main function actually does, we'll first need to discuss binary numbers and some other concepts.

Binary

The ATmega328 microcontroller (and basically all computers) use binary math. This is base 2, as opposed to the base 10 used for normal numbers.

Let's look at a regular, base-10 number, 123:

100 = 102 10 = 101 1 = 100
123

There are three columns, one for each digit, the 1's column (or place), the 10's place, and the 100's place. To calculate the value of the number you multiply each place by the digit in that place and add them up. That is, 123 = 1 * 100 + 2 * 10 + 3 * 1.

In binary, 123 is written 1111011:

64 = 2632 = 2516 = 248 = 234 = 222 = 221 = 20
1111011

Here, there's a 1's place, a 2's place, a 4's place, an 8's place, etc. To calculate the value of the number you add up the places with 1's in them. That is, 123 = 1 * 64 + 1 * 32 + 1 * 16 + 1 * 8 + 0 * 4 + 1 * 2 + 1 * 1.

Bits and Bytes

Inside the microcontroller, binary numbers are stored in values called bytes. Each byte contains eight bits (binary digits), each of which is 0 or 1. When a bit has the value 1, we say that the bit is set. When the bit is 0, we say it's clear. Each byte can store 256 different values (because 28 is 256).

Registers

Most of the bytes in the microcontroller's memory are used to store numbers and other data, but a few have special meanings. Some of these special bytes are called registers and they control the behavior of the various components that are contained in the microcontroller. These registers are bytes, but we don't use them as numbers. Instead, each of their eight bits has an particular meaning. The program above used two registers: DDRB and PORTB. These registers control the behavior of the eight pins of port B (PB0 to PB7):

  • DDRB is the data direction register for port B. It determines whether each pin is an input or an output.
  • PORTB is the data register for port B. For output pins, this determines whether the pin is set high or low.

Each of the eight bits of the register corresponds to one of the eight pins of the port. The 0'th (right-most) bit of the register corresponds to PB0. The 7'th (left-most) bit to PB7. If a bit of the DDRB register is set to 1, the corresponding pin is set as an output. If the bit is 0, the pin is an input. The pins default to being inputs (i.e. the bits of DDRB default to 0). If a bit of the PORTB register is set to 1, and the pin is set as an output, the microcontroller will hold the pin high - that is, set it to 5V (or whatever voltage is used to power the microcontroller). If a bit of PORTB is 0, and the pin is an output, the microcontroller will hold the pin low (0V).

Here's an example:

PinPB7PB6PB5PB4PB3PB2PB1PB0
DDRB00100001
PORTB00000001

Here, pins PB0 and PB5 are set as outputs. PB5 is set low, PB0 high. That means that if you measured the voltage on PB5 leg of the microcontroller with a multimeter (with the other probe touching ground), you'd get 0V; if you measured PB0, you'd get 5V.

There are also registers for the other ports of the microcontroller: DDRC and PORTC control pins PC0 to PC6 (although PC6 is the reset pin, so we don't normally use it for input and output); DDRD and PORTD control pins PD0 to PD7.

Our Program

Now we know enough about binary and registers to understand the core of our program:

DDRB = 1;
PORTB = 1;

The number 1 is 00000001 in binary; so assigning it to the register DDRB sets PB0 as an output and the rest of the pins as inputs (remember that the 0'th bit is in the right, the 7'th bit is on the left). The next line uses PORTB to take the pin high (set it at 5V). Therefore, the LED on pin PB0 turns on.

If you wanted to connect your LED to a different pin, you'd need to change the value you assign to DDRB and PORTB. For example, for pin PB3, you'd use the number 8. In the next program, we'll see an better way of writing these values using the _BV() macro.

A Second Program

Here's a slightly more complicated program. It should blink the LED on pin 14 (PB0).

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB = _BV(PB0);
    for(;;){
    	PORTB = _BV(PB0);
        _delay_ms(1000);
    	PORTB = 0;
        _delay_ms(1000);     
    }
    return 0;
}

Makefiles

We could compile and upload this program by manually typing the commands needed to properly run the compiler (avr-gcc) and uploaded (avrdude), but this gets tedious. Instead, we can place the rules for running these programs into another file, called a makefile, and use a program called make to run the programs for us.

Here's a basic Makefile that can compile and upload an AVR C program:

PROJECT=blink
SOURCES=$(PROJECT).c
MMCU=atmega328p
F_CPU=1000000

# The serial port of your Arduino board (if using Arduino as ISP):
# - on Mac OS X, use: /dev/tty.usbserial-*
# - on Windows, use the appropriate COM port, e.g.: COM3
# - on Linux, use: /dev/ttyUSB0
SERIAL=

CFLAGS=-mmcu=$(MMCU) -DF_CPU=$(F_CPU) -Os

$(PROJECT).hex: $(PROJECT).out
	avr-objcopy -j .text -O ihex $(PROJECT).out $(PROJECT).hex

$(PROJECT).out: $(SOURCES)
	avr-gcc $(CFLAGS) -o $(PROJECT).out $(SOURCES)

program-arduinoisp: $(PROJECT).hex
	avrdude -p $(MMCU) -P $(SERIAL) -c stk500v1 -U flash:w:$(PROJECT).hex

program-avrisp2: $(PROJECT).hex
	avrdude -p $(MMCU) -P usb -c avrisp2 -U flash:w:$(PROJECT).hex

program-usbtiny: $(PROJECT).hex
	avrdude -p $(MMCU) -c usbtiny -U flash:w:$(PROJECT).hex

clean:
	rm $(PROJECT).out $(PROJECT).hex

Save it in a file called makefile in the same directory as your program. To compile the program, you simply type make at the command line. To upload, you need to tell make to run the rule in the makefile that corresponds to the programmer you're using, for example:

  • make program-avrisp2 to upload using an AVRISP mkII
  • make program-usbtiny to upload with a USBtiny or FabISP
  • make program-arduinoisp to upload using an Arduino as an ISP

For the latter, you'll need to edit the makefile to specify the serial port of your Arduino board. Set the SERIAL variable to the appropriate value for your operating system (this is the same as the value you passed to the -P argument of avrdude previously).

Understanding the Program

_BV()

In this program, we use a new syntax to set bits in the DDRB and PORTB registers: the _BV() macro. This macro accepts a number and turns it into the byte with that numbered bit set. For example _BV(0) gives a byte with the 0'th bit set: 00000001, _BV(1) is 00000010, _BV(2) is 00000100, etc. In the program, we use a notation that specifically refers to the pin we're using: PB0. This actually has the value 0, but using PA0 instead helps make it clear that we are doing something that affects that pin. So: DDRB = _BV(PB0) sets DDRB to 00000001, making PB0 an output and the other port B pins inputs.

Infinite Loops

Delays

Buttons and Digital Input

In this circuit, pressing the button will complete the circuit from pin PC0 to ground.

A Third Program

#include <avr/io.h>

int main(void)
{
    DDRB |= (1 << PB0);
    PORTC |= (1 << PC0);
    for(;;) {
    	if (PINC & (1 << PC0)) {
            PORTB &= ~(1 << PB0);
        } else {
            PORTB |= (1 << PB0);
        }
    }
    return 0;
}

Bit Math

To understand this program, we need to know more about bit math, various operations that manipulate the value of a byte. There are five basic operations: left-shifting (written <<), right-shifting (>>), bitwise-or (|), bitwise-and (&), and bitwise-negation (~).

Bit Shifting

Bit shifting is the process of shifting all the bits of a number over by a certain number of places, either left or right:

  • number << places shifts the bits of number left by places bits
  • number >> right shifts right

For example, 9 << 1 equals 18 (we move the bits in the number 9 to the left by one place):

 128= 2764 = 2632 = 2516 = 248 = 234 = 222 = 221 = 20
9 =00001001
18 =00010010

The bits in number 9 have each been shifted one place to the left. The rightmost bit gets filled in with a 0 (if we shifted left by two places, the two rightmost bits would both become 0). The leftmost bits disappear if they're shifted off the left end (e.g. 130 is 10000010 in binary; shifting it left by one place would give 00000100, which equals 4).

For a right-shift, the bits shift to the right. For example, 20 >> 2 is 5 (we move the bits in the number 20 to the right two places):

 128= 2764 = 2632 = 2516 = 248 = 234 = 222 = 221 = 20
20 =00010100
5 =00000101

Here, the left-most bits are filled in with 0's, and any bits that are shifted off the right end disappear (for example, 5 >> 1 is 2).

Bitwise-And (&)

Bitwise-And, written with an ampersand (&), operates on the individual bits of a pair of numbers. The resulting number has a 1 in a particular bit location if both the first number and the second number have a 1 in the corresponding location, hence the name of the operator.

That is:

  • 1 & 1 = 1
  • 1 & 0 = 0
  • 0 & 1 = 0
  • 0 & 0 = 0

For example, 20 & 5 = 4:

 128= 2764 = 2632 = 2516 = 248 = 234 = 222 = 221 = 20
20 =00010100
5 =00000101
4 =00000100

The only bit that is set (equal to 1) in the result, 4, occurs where both numbers 20 and 5, have a bit of 1: the 4's column.

Bitwise-Or (|)

Bitwise-Or, written with an vertical pipe (|), is similar to bitwise-and, except that its result has a 1 in any bit where the first or the second number has a 1.

That is:

  • 1 | 1 = 1
  • 1 | 0 = 1
  • 0 | 1 = 1
  • 0 | 0 = 0

For example, 20 | 5 = 21:

 128= 2764 = 2632 = 2516 = 248 = 234 = 222 = 221 = 20
20 =00010100
5 =00000101
21 =00010101

The set bits in the result (21) occur wherever either 20 or 5 has a bit set: the 16's column, the 4's column, and the 1's column.

Variables and Data Types

Mathematical Operations

Boolean Expressions and If-Statements

Loops

Functions

PWM and Fading LEDs

This program will fade an LED on PD6 (pin 12). It uses the hardware PWM (pulse-width modulation) functionality of the ATmega328p to generate a square wave of varying duty cycle.

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    int i;
    DDRD |= (1 << PD6);
    TCCR0A |= (1 << WGM00); // phase correct pwm
    TCCR0A |= (1 << COM0A1); // non-inverting mode
    TCCR0B |= (1 << CS01); // prescale factor of 8
    for(;;) {
    	for (i = 0; i < 256; i++) {
            OCR0A = i; // set duty cycle
            _delay_ms(10);
        }
    }
    return 0;
}

Light Sensors and Analog Input

This example uses the analog to digital convertor (ADC) to measure the voltage on PC0 (pin 23) and change the rate of blinking of an LED on pin PB0. To use, replace the push-button from the example above with a light sensor.

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    volatile unsigned int val, val2;
    DDRB |= 0x01;
    PORTC |= 0x01;
    //ADMUX |= (1 << ADLAR); // left-adjust result
    ADMUX |= (1 << REFS0);
    ADCSRA |= (1 << ADPS1) | (1 << ADPS0); // prescale of 8 (1 MHz -> 125 KHz)
    ADCSRA |= (1 << ADEN); // enable adc
    for(;;) {
    	ADCSRA |= (1 << ADSC);
        while (ADCSRA & (1 << ADSC))
            ;
        val = ADCL;
        val = (ADCH << 8) | val;
        //val = ADCH;
        PORTB |= 0x01;
        _delay_loop_2(val << 6);
        _delay_loop_2(val << 6);
        _delay_loop_2(val << 6);
        _delay_loop_2(val << 6);
        PORTB &= ~0x01;
        _delay_loop_2(val << 6);
        _delay_loop_2(val << 6);
        _delay_loop_2(val << 6);
        _delay_loop_2(val << 6);
    }
    return 0;
}

Serial Communication and Debugging

Attach:Atmega_Breadboard_Serial.jpg Δ

#include <avr/io.h>
#include <util/delay.h>

#define BAUDRATE 9600

void putchar(char c)
{
    while ( !(UCSR0A & (1<<UDRE0)) )
        ;
    UDR0 = c;
}

void putstring(char *s)
{
    while (*s)
    	putchar(*s++);
}

void putnum(int n)
{
    char buf[16];
    itoa(n, buf, 10);
    putstring(buf);
}

int main(void)
{
    int i = 0;
    UCSR0A = (1<<U2X0); // double speed mode
    UBRR0H = (unsigned char) ((F_CPU/8/BAUDRATE-1) >> 8);
    UBRR0L = (unsigned char) (F_CPU/8/BAUDRATE-1);        
    UCSR0B = (1<<RXEN0) | (1<<TXEN0); // enable rx and tx
    for(;;) {
    	putnum(i++);
    	putstring(" bottles of beer on the wall\r\n");
        _delay_ms(5);
    }
    return 0;
}

Resources

References:

Other useful websites include: