AVR microcontrollers

Needed documents - benötigte Dokumente

To program the ATmega8 you will need from atmels datasheets at least doc8159.pdf (ATmega8A)
And for assembler instructions you will need doc0856.pdf (8-bit AVR Instuction Set)

Um den ATmega8 zu programmieren braucht man von den Atmel-Datenblättern mindestens doc8159.pdf (ATmega8A)
Und für die Assembler-Instruktionen braucht man doc0856.pdf (8-bit AVR Instuction Set)

Needed software - benötigte Software

To start programming we need avr-gcc for c and c++, avra for assembler, avrdude to transfer compiled programs to the programmer, and a text editor.
With Linux-Ubuntu these packages are needed:
For c and c++ we will use avr-gcc in both cases. The compiler will decide between c and c++ by filename. Use name.c for c programs and name.cc or name.cpp for c++ programs.
Instead of emacs you could also use any other text editor. But emacs is what I use, and know howto directly compile from the editor.

Contents - Inhaltsverzeichnis:

First examples in assembler - Erste Assembler-Beispiele
First examples in c or c++
Makefiles
Blinking LEDs - Blinkende Leuchtdioden
Reading switches and buttons - Schalter und Druckknöpfe einlesen
Interrupts
Timer
Debounce - Entprellung
Links

First examples in assembler - Erste Assembler-Beispiele

First very simple assembler program for Atmega8:
        .equ DDRB = $17  ;defining address of DDRB using a hexadezimal number 
        .equ PORTB = $18 ;defining address of PORTB
main:   ldi r16, 1       ;load immediate register16 with 1
        out DDRB, r16    ;output to Data Direction Register B, will set PB0 aus output
        out PORTB, r16   ;output to port B, will turn on the LED connected to PB0
L1:     rjmp L1          ;relative jump back to label L1, so it will stay in ths loop for ever (until reset).
For a useful program we need a little bit more. Definitions of DDRB and PORTB we dont need to make ourself, instead we just include the file "m8def.inc". We will also define the whole table for available interrupts. So in case we need one of them, we will have only to set a rjmp at the appropriate position. For now we use only the first, jumping to main. In the main program we should first define the stackpointer. This will be used to store return address for subroutines and to save and restore registers. In the next example we will do this, and let the LEDs (connected to PB0, PB1, and PB2) blinking. So we can see the controller is really working.

First useful simple assembler program for Atmega8:

;-----------------------------------------------------------------------------
;* start.asm
;* lines beginning with ; are comments
;*
;-----------------------------------------------------------------------------
        .include "m8def.inc"   ;include definitions for ATmega8
; Reset and interrupt vectors   ;VNr. Meaning
begin:  rjmp main               ; 1   Power On Reset
        reti                    ; 2   Int0-Interrupt
        reti                    ; 3   Int1-Interrupt
        reti                    ; 4   TC2 Compare Match
        reti                    ; 5   TC2 Overflow
        reti                    ; 6   TC1 Capture
        reti                    ; 7   TC1 Compare Match A
        reti                    ; 8   TC1 Compare Match B
        reti                    ; 9   TC1 Overflow
        reti                    ; 10  TC0 Overflow
        reti                    ; 11  SPI, STC = Serial Transfer Complete
        reti                    ; 12  UART Rx Complete
        reti                    ; 13  UART Data Register Empty
        reti                    ; 14  UART Tx Complete
        reti                    ; 15  ADC Conversion Complete
        reti                    ; 16  EEPROM ready
        reti                    ; 17  Analog Comperator
        reti                    ; 18  TWI (I2C) Serial Interface
        reti                    ; 19  Store Program Memory ready
;-----------------------------------------------------------------------------
; Start, Power On, Reset
main:   ldi r16, RAMEND&0xFF
        out SPL, r16            ; Init Stackpointer L
        ldi r16, (RAMEND>>8)
        out SPH, r16            ; Init Stackpointer H

;; Init-Code
        ldi r16, 0xFF
        out DDRB, r16
        ldi r16, 0b001      ; load binary number to r16
        out PORTB, r16      ; output of r16 to PORTB
        ldi r20, 5

;-----------------------------------------------------------------------------
mainloop:
        adiw r24, 1
        brne mainloop           ;waiting loop
        subi r20, 1
        brne mainloop           ;longer waiting loop
        ldi r20, 5
        rol r16                 ;Rotation left
        cpi r16, 0x08           ;compare for last LED
        brne t1                 ;branch if not equal
        ldi r16, 1              ;otherwise set again to first LED
t1:     out PORTB, r16          ;LEDs blinking
        rjmp mainloop

To compile the programm you can use this command:
> avra start.asm
and to send the resulting start.hex to the programmer you could use this command:
> avrdude -p m8  -c avr910  -P /dev/ttyUSB0  -U flash:w:start.hex:i
(instead of "avr910" use the name of your programmer. /dev/ttyUSB0 depends on where your computer finds the connection. On Mac it could be /dev/tty.serial-0001)

The more convenient way to compile and send the program to the programmer is to use a makefile:

> make
> make check
> make install
(See chapter Makefiles for more about makefiles)

Archive with examples and makefile: mega8asmexample.tar.gz


First examples in c or c++

Very simple example C-Programm for Atmega8:
// start.c
#define __AVR_ATmega8__  //can be commented out when defined in makefile
#include <avr/io.h>

int main(void)
{
  DDRB  = 0xff; //PortB defining as output
  PORTB = 0x03; //B0 and B1 at H, other bits at L

  while(1)    //main-loop will never end
   {
   }
 
  return 0; /* will not be reached */
}
To compile the program you could use these commands:
> avr-gcc start.c -o start.elf
> avr-objcopy -O ihex -R .eeprom start.elf start.hex
> avr-objdump -h -S start.elf > start.lss
But it is easier to use a makefile. Then you need only to use the command "make". The makefile is also useful to do other things, for instance transfering the code to the controller. Typically you will use these commands to compile, check the connection to the controller, and put the programm to the controller:
> make
> make check
> make install
In the archive for this example, there are two more examples included: blinking.c, clock.cc
With "make" these programs are also compiled, and to install you can use "make install-blinking" and "make install-clock".
These programs are explained in next chapters.
For a new project you can make a new folder, copy the simpler-makefile to this folder and rename it to "makefile". Then replace in this makefile every "start" to an other name (name of the project).

If "make check" fails:
Find out how your computer names the USB serial connection.
Find out what is the name and version of your programmer.
Edit the makefile, comment out the line "TTY=" and type a new line "TTY=your_usb_connection" (for windows maybe COM1).
Comment out the line "P=avr910" and set name of your programmer (for example "P=avr911")

Archive with examples and makefile: mega8gccexample.tar.gz


Makefiles

Here an archive with updated example makefiles: mustermakefiles.tar.gz

Lets look a bit closer to the example makefile for compiling c programs:

M=atmega8
N=m8
C=avr-gcc -mmcu=$M -Wall -Os -c
L=avr-gcc -mmcu=$M -Wall -Os
The first line will define M to "atmega8". Later this definition is used by "$M". This will be replaced by "atmega8". When defining something with several letters, like "TTY", we will need brackets to use it: "$(TTY)".
In the next two lines "$C" and "$L" are defined to compile programs. Everything after the first "=" sign will be taken for the definition, even the second "=" sign. So "$C" will be replaced by "avr-gcc -mmcu=atmega8 -Wall -Os -c".
# to use avrdude with Linux:
#TTY=/dev/ttyUSB0
# to use avrdude with MacOSX:
TTY=/dev/tty.serial-0001
Lines beginning with "#" are comments. So we can comment out lines we dont need at the moment. So for switching between using the makefile with Linux and with MacOSX, we need just to comment out the right part.
# select the programmer:
P=avr900
#P=stk500v2
Same here, just select your programmer by commenting out the other.
all: start.hex
With "all:" we define what should happen when just calling the makefile by "make". In this case it should build the file "start.hex".
start.hex: start.c
        $L start.c -o start.elf
        avr-objcopy -O ihex -R .eeprom start.elf start.hex
        avr-objdump -h -S start.elf > start.lss
This defines how the file "start.hex" can be built. Needed files to do it are listed after the ":". In this case "start.c". Then the indented lines are the commands to be executed. This indentation must be done by tabulator (ASCII code 9).
check:
        avrdude -p $N -c $P -P $(TTY) -v
Similar to above "start.hex" here make will try to make the file "check". In this case the list of needed files is just empty. And the given command not really creates a file "check". But every time we type "make check" make will try again to make the file "check" by the given command. So we can use it to check the connection to the controller.
install: start.hex
        avrdude -p $N -c $P -P $(TTY) -U flash:w:start.hex:i
Same thing as with "check". Here we will send the program to the controller by typing "make install".
setfuses: #for ATmega8 with Quartz
        avrdude -p m8 -c $P -P $(TTY) -U lfuse:w:0xFF:m
        avrdude -p m8 -c $P -P $(TTY) -U hfuse:w:0xD9:m
With "make setfuses" we can set the fuses of the controller. Be careful with this. Wrong fuses can make your controller not usable anymore.
clean:
        rm -f *~ *.o *.elf
clean_all:
        rm -f *~ *.o *.elf *.lss *.hex
With "make clean" or "make clean_all" we can remove files we dont need anymore.

Blinking LEDs - Blinkende Leuchtdioden

In the example blinking.c we will make LEDs blinking. To do this we just define a variable "counter", increase it every cycle in the main loop, and put it to the output port. For not beeing too fast for our eyes we will have to implement a wait function.
So the main loop will look like this:
 while(1)    //main-loop will never end
   {
    wait();
    counter++;
    PORTB = counter;
   }
 
A simple way to implement the wait() function is just counting variables. For long waiting times we could make nested loops.
void wait()
{
 volatile unsigned int i,j;
 for(i=0;i<100;i++)
  {
   for(j=0;j<1000;j++)
    {
    }
  }
}
But this has some disadvantages. It depends on the cpu clock, and worse it depends on how good the compiler optimizes code. To avoid optimize out the whole waiting loops we need to define the variables with "volatile".
A bit better way for waiting loops is to use the library "delay.h":
#define F_CPU 3686400UL  //cpu clock (quartz frequency)
#include <util/delay.h>

void wait()
{
 int i;
 for(i=0;i<300;i++) //wait about 300 milliseconds
  {
   _delay_ms(1);
  }
}
But the best way for waiting is to use a timer and interrupts.

Reading switches and buttons - Schalter und Druckknöpfe einlesen

Lets connect a button to the controller. Connect one pin of the button to ground and the other pin directly to the PD2 pin (Or use a resistor of about 1k to protect the controller against bad programming mistakes). The button will give contact when pressed so the input pin will have zero when button pressed and 1 when button released.
Here an example to use this button:
#include <avr/io.h>

int main(void)
{
 DDRB  = 0x07; //PortB PB0-2 defining as output
 PORTB = 0; //all LEDs off
 DDRD  = 0x00; //PortD all inputs (not really necessairy because default anyway)
 PORTD = (1<<2); //set pullup resistor for PD2

 while(1) //main loop
   {
    //to read from PORTD we need PIND !
    if((PIND&(1<<2))==0) //is button pressed?
      PORTB |= 1;  //yes: turn LED on
    else
      PORTB &= ~1; //no: turn LED off
   }

 return 0;
}

Interrupts

To activate interrupts there is an interrupt bit in the status register. We can set it with the assembler instruction "sei" and clear it with "cli". To decide which interrupts should be activated we have to set interrupt enable bits at different places in the IO-registers. All these interrupt enable bits becomes only really active after a "sei" instruction.
For example: to activate the external interrupt "INT0" we have to set the bit "INT0" in the IO-register "GICR". Additionally we can decide if the pin "INT0" should react to rising or falling edge or generate interrupts at low level. This is done by the bits "ISC01" and "ISC00" in the IO-register "MCUCR".
If now an interrupt occures the subroutine "ISR(INT0_vect)" will be called. The meaning of "ISR" is Interrupt-Sub-Routine. It is a macro who will create the really name (which we dont need to know) of the subroutine.

Um Interrupts zu aktivieren gibt es im Statusregister ein Interrrupt-Bit. Dieses kann man mit dem Assemblerbefehl "sei" setzen und wieder löschen mit "cli". Um festzulegen welche Interrupts aktiviert werden sollen gibt es noch diverse Bits in den IO-Registern. All diese Interrupt-Enable-Bits sind aber erst wirklich aktiv wenn wir "sei" gemacht haben.
Zum Beispiel: um den externen Interrupt "INT0" zu aktivieren müssen wir das Bit "INT0" im IO-Register "GICR" setzen. Zusätzlich können wir noch festlegen mit welcher Flanke am Pin INT0 ein Interrupt ausgelöst werden soll. Dies machen wir mit den Bits "ISC01" und "ISC00" im IO-Register "MCUCR".
Wenn jetzt ein Interrupt ausgelöst wird, dann wird das Unterprogramm "ISR(INT0_vect)" aufgerufen. Dieses "ISR" bedeutet InterruptSubRoutine und ist ein Makro, das den tatsächlichen Namen (den wir nicht wissen müssen) des Unterprogramms erstellt.

Here is a first example using an interrupt:

//int01.cc  interrupt example for INT0 with ATmega8

#include <avr/io.h>
#include <avr/interrupt.h>

void interrupts_init()
{
 MCUCR = (0<<ISC01) | (1<<ISC00); //any logical change on INT0 generates an interrupt
 GICR = (1<<INT0); //turn on interrupts for INT0
}

volatile unsigned char button1=0;

ISR(INT0_vect) //Interrupt SubRoutine for INT0
{
 button1=1;
}

int main(void)
{
 DDRB  = 0x07; //PortB PB0-2 defining as output
 PORTB = 0; //all LEDs off
 DDRD  = 0x00; //PortD all inputs (not really necessairy because default anyway)
 PORTD = (1<<2); //set pullup resistor for PD2 (INT0)
 interrupts_init();

 sei(); //Interrupts turning on

 while(1) //main loop
   {
    if(button1!=0)
     {
      //to read from PORTD we need PIND !
      if((PIND&(1<<2))==0) //is button pressed?
        PORTB |= 1;  //yes: turn LED on
      else
        PORTB &= ~1; //no: turn LED off
      button1 = 0;
     }
   }

 return 0;
}
The example in the archive will use interrupts for both, INT0 and INT1.

Archive with examples and makefile: interruptexample.tar.gz

Timer

The Atmega8 has several timers. Here we will just use "Timer1", a 16 bit timer. The idea of a timer is quite simple: a 16 bit register will be counted up continuously. When reaching the maximum value it will start just again from zero. We can read and write this counter by the IO-registers TCNT1H and TCNT1L. (We need two 8 bit registers to represent a 16 bit value). In c just use TCNT1.
Instead of read this counter all the time to wait for a certain value, we can set a compare register OCR1A (again splited in two 8 bit registers OCR1AH and OCR1AL), and set an interrupt enable. Then every time the counter reaches this value we will get an interrupt. Because the timer can also do some other things there are some bits we can set to tell the timer what we want. These bits are in the IO-registers TCCR1A, TCCR1B, and TIMSK. (In TIMSK are also some bits for the other timers.)
In the example clock.cc we dont need to change default values for TCCR1A, but we will set some bits in the other control registers.
So in c the timer initialization will look like this:
 TCCR1B = (1<<WGM12)|(1<<CS10); //CTC-OCR1A-Modus, Prescaler=1
 OCR1A = NormalerCompwert; //compare value
 TCNT1 = 0; //Start value of Timer1
 TIMSK = 1<<OCIE1A; //Timer1 Interrupts at compare value
 sei(); //Interrupts turning on
"Prescaler=1" just means the clock for the timer is the same as our cpu clock.
"NormalerCompwert" will be calculated from cpu clock to get every millisecond an interrupt.
With quartz frequency of round number of megaherz this will be easy, just dividing F_CPU by 1000. A bit more difficult is it when for instance F_CPU is 3.6864 MHz. The trick is just using the rounded down value (3686 instead of 3686.4) and make some corrections every second. This we can do in the interrupt routine by setting OCR1A every time to a new value. This way we can correct also systematic errors of the quartz frequency.
(For a slow quartz we could even correct errors below 1 Hz. Then we would also make corrections every minutes and every hour.)
The code part in c doing this looks like this:
static volatile uint millisec=0,days=0;
static volatile uchar sec=0,min=0,hours=0;

#define F_CPU 3686400UL //exactly 3.6864 MHz (UL means unsigned long)

//Example of measured frequency: with exactly 3.6864 MHz after 10 hours the clock was 0.35 sec late:
// so the frequency was a bit lower, calculated in ticks per second:
// 3686400 * 0.35 / (3600*10) = 35.84
// corrcted frequency: 3686400-35.84 = 3686364.160
//#define F_CPU 3686364UL //measured frequency
//#define MilliHz 160     //very exactly measured frequency (not used in this example)

#define REST1 (F_CPU%1000) //every second needed correction
//#define REST2 (MilliHz*60/1000) //every minute needed correction (not used in this example)
//#define REST3 (((MilliHz*60%1000)*60+500)/1000) //every hour needed (not used in this example)

#define NormalerCompwert (F_CPU/1000-1)
//one less because it will be counted from 0 up to and including the value
//eins weniger weil von 0 bis und mit NormalerCompwert gezaehlt wird

ISR(TIMER1_COMPA_vect)
{
 uint ms=millisec;
 if(++ms==1000)
  {ms=0;
   if(++sec==60)
    {sec=0;
     if(++min==60)
      {min=0;
       if(++hours==24)
        {hours=0; ++days;}
      }
    }
  }
 if(ms>=REST1) OCR1A = NormalerCompwert; //usual compare value
 else          OCR1A = NormalerCompwert+1; //one more
 millisec=ms;
}
So we will have (independent of the main program) counting up time in the static variables sec, min, and hours.

For the whole example see above in the archive of first examples.


Debounce - Entprellung

Task: try to change the button example to turn on the LED when pressing button first time, and turn off only when pressing button next time. Not turning off when release button.

If you wrote this program but some strange things happend when trying out, it could be because of this:
A mechanically button will have some bouncing effects when pressed. This results in switching on and off several times during some milliseconds. Same thing when releasing it.
So this code would not work as expected:

 while(1) //main loop
   {
    if((PIND&(1<<2))==0) //is button pressed?
     {
      PORTB ^= 1;  //exclusive or of first bit: when 1 it changes to 0, when 0 it changes to 1
      while((PIND&(1<<2))==0) //is button still pressed?
           {} //wait until released
     }
   }
We have several possibilities to avoid these effects.
A simpel way is just waiting a bit every time the state of the button changes.
Or we could wait until state of the button is the same when reading several times:
 while(1) //main loop
   {
    if((PIND&(1<<2))==0) //is button pressed?
     {
      wait(); //waiting about 5 milliseconds
      if((PIND&(1<<2))==0) //is button pressed?
        {
         PORTB ^= 1;  //change LED state
         while((PIND&(1<<2))==0) {} //wait until released
         wait(); //waiting about 5 milliseconds
         while((PIND&(1<<2))==0) {} //wait until released
        }
     }
   }
To make it more convenient we can do the whole debouncing stuff in the timer interrupt.
Here is an example to do it for several buttons at the same time:
//entprellung.cc
#define F_CPU 3686400UL

#include <avr/io.h>
#include <avr/interrupt.h>
#define uchar unsigned char
#define uint unsigned int

static volatile uint millisec=0;
static volatile uchar sec=0,min=0,hours=0;

#define PIN_TASTER PIND  //Buttons connected to PORTD
void entprell(); //Vordeklaration fuer Tasten-Entprellung - declaration for debouncing function

#define NormalerCompwert ((F_CPU/1000)-1)
#define REST1 (F_CPU%1000)

ISR(TIMER1_COMPA_vect)
{
 uint ms=millisec;
 if(++ms==1000)
  {ms=0;
   if(++sec==60)
    {sec=0;
     if(++min==60)
      {min=0;
       ++hours;
      }
    }
  }
 OCR1A = (ms>=REST1) ? NormalerCompwert : NormalerCompwert+1; //compare value
 if(ms%4==0) entprell(); //Tastenentprellung alle 4 Millisekunden - Debouncing every 4 milliseconds
 millisec=ms;
}

//fuer Tastenentprellung:
static volatile char entprelltetasten=0xFF;
static char status1=0xFF,status2=0xFF;
static volatile char tastegedrueckt=0;

void entprell() //Tasten-Entprellung fuer mehrere Tasten unabhaengig voneinander
{               //Debouncing several buttons independent at the same time
 char pin,ung;
 pin = PIN_TASTER;
 //In "ung" diejenigen Bits setzen die ungleich zu status1 oder status2 sind:
 //Set bits to 1 if different to status1 or different to status2:
 ung = (pin^status1) | (status2^status1);
 //Bits so lassen wenn sie in "ung" auf 1 sind, oder gleich wie in "pin" setzen wenn sie in "ung" auf 0 sind:
 //Set bits to previous state when it is 1 in "ung", or set to same as "pin" when it is 0 in "ung":
 entprelltetasten = (entprelltetasten & ung) | (pin & (ung^0xFF));
 status2=status1;
 status1=pin;
 tastegedrueckt |= (entprelltetasten^0xFF);
}

int main(void)
{
 DDRB = 0x07;
 PORTD = (1<<2); //set pullup resistor for PD2

 //initialize timer1:
 TCCR1B = (1<<WGM12)|(1<<CS10); //CTC-OCR1A-Modus, Prescaler=1
 OCR1A = NormalerCompwert; //compare value
 TCNT1 = 0; //Start value of Timer1
 TIMSK = 1<<OCIE1A; //Timer1 Interrupts at compare value
 sei(); //Interrupts turning on

 while(1) //Hauptschlaufe
   {
    if((tastegedrueckt&(1<<2))!=0) //button PD2 pressed?
     {
      PORTB ^= 1;  //change LED state
      while((entprelltetasten&(1<<2))==0) {} //wait until button released
      tastegedrueckt &= ~(1<<2); //reset pressed state for the button
     }
   }

 return 0; //wird nie erreicht.
}

maybe useful link: www.micahcarrick.com/avr-tutorial-switch-debounce.html


Some useful Links - Einige nützliche Links


Last update: 4.Nov.2012 / Rolf                                                                                 Validator