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)
.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 mainloopTo compile the programm you can use this command:
> avra start.asmand 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
// 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.lssBut 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 installIn the archive for this example, there are two more examples included: blinking.c, clock.cc
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
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 -OsThe 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)".
# to use avrdude with Linux: #TTY=/dev/ttyUSB0 # to use avrdude with MacOSX: TTY=/dev/tty.serial-0001Lines 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=stk500v2Same here, just select your programmer by commenting out the other.
all: start.hexWith "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.lssThis 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) -vSimilar 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:iSame 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:mWith "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 *.hexWith "make clean" or "make clean_all" we can remove files we dont need anymore.
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".
#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.
#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; }
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.
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.
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.
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.
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.
//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