Du må være registrert og logget inn for å kunne legge ut innlegg på freak.no
X
LOGG INN
... eller du kan registrere deg nå
Dette nettstedet er avhengig av annonseinntekter for å holde driften og videre utvikling igang. Vi liker ikke reklame heller, men alternativene er ikke mange. Vær snill å vurder å slå av annonseblokkering, eller å abonnere på en reklamefri utgave av nettstedet.
  5 1975
God kveld. Skriver denne lille guiden de som vil komme i gang med, eller er nysgjerrig på, programmering av Atmels mikrokontrollere, AVR (8bit).

De som ikke har programmert AVR før, kan lese den andre tråden min, "Kom i gang med AVR".

Jeg kommer til å bruke en ATmega128 (litt overkill for dette, men det var den som lå foran PCen) med en trykknapp og en LED. Om du bruker en annen AVR, kan det hende du må porte koden (endre registernavn, vektornavn osv.).

Programmet er veldig enkelt og virker kanskje meningsløst, men ved å forstå grunnleggende bruk av timere og interrupts med AVR, kommer man langt.

Utstyr
-AVR med en LED tilkoblet hvilken som helst digital I/O-pin, og en trykknapp tilkoblet en ekstern interrupt-pin.
-ISP- eller JTAG-programmerer
-AVRStudio (for windows. linux og mac, sjekk avrgcc + avrdude)
-AVRCalc (link)
-Om du ikke konverterer mellom desimal, binær og hexadesimal i hodet kan du bruke en kalkulator som du finner over alt på nettet.

Interrupts
Når en interrupt oppstår, avbrytes programmet, ISRen (Interrupt Service Routine) som tilhører interruptet kjøres og programmet fortsetter der det ble avbrutt når ISRen er ferdig.
Om du feks. har et program som kjører konstant, men vil gjøre noe akkurat i det en sensor blir aktivert, uten å måtte vente på tur i hovedloopen. Kan du interrupte programmet når sensoren aktiveres og feks. aktivere en alarm.
Kanskje ikke det beste eksempelet, men en dag får alle bruk for interrupts.

Timere
Timere lar deg, som navnet tilsier, time ting. Du kan feks. bruke de til PWM, eller sammen med interrupts, gjøre ting uavhengig av hoved-loopen i programmet.

Muligens dårlig forklart, men det blir forhåpentligvis mer forståelig snart.

Programmet
Programmet vi vil lage, skal gjøre følgende:
-Toggle Pin 1 på PORTC hvert 500ms i hovedloopen
-Toggle Pin 0 på PORTC hvert 250ms ved hjelp av timer
-Hvis knappen trykkes, skal Pin 0 toggles hvert 100ms
-Hvis knappen trykkes igjen, skal Pin 0 toggles hvert 250ms igjen osv..


Enkelt skjema for kretsen.

La oss begynne med LEDen som toggles hvert 500ms i hovedloopen.


Ganske selvforklarende, men vi går igjennom den for sikkerhets skyld.

Kode

#define F_CPU 16000000
Her definerer vi klokkefrekvensen AVRen kjører på. Jeg bruker en 16MHz krystall, derfor 16M. Dette tallet brukes av delay-funksjonen senere i programmet for å regne seg frem til riktig tid.

Kode

#include <avr/io.h>
#include <util/delay.h>
Inkluder header-filer. "io.h" for registernavn, vectornavn osv. "delay.h" for delayfunksjonen vi bruker senere i programmet.

Kode

DDRC = 0x02;				//Set PORTC.1 som utgang
Set pinne 1 på PORTC som utgang ved å skrive 1 til DDRC-registeret. 0x02 (hex) er 00000010 binært. Altså pin 1 når vi starter å telle fra 0.

Kode

PORTC ^= 0x02;			//Toggle PORTC.1
Denne kan være litt verre for noen. La oss bryte den ned litt.
PORTC ^= 0x02 (hex)
PORTC ^= 00000010 (bin)
PORTC = PORTC XOR 00000010

Kort oppsummering av XOR for de som ikke kjenner til den.
A XOR B = C
0 XOR 0 = 0
1 XOR 0 = 1
0 XOR 1 = 1
1 XOR 1 = 0

Så om pin 1 er høy (1) vil den bli satt lav (0) og omvendt. Slik kan vi toggle den.

Kode

_delay_ms(500);			//Hvert 500 ms
La AVRen gjøre ingenting i 500 ms.

Så, hva gjør vi hvis vi vil toggle pin 0 i et annet tempo enn pin 1? Her kan en timer gjøre nytte for seg i mange tilfeller.

Det viktigste verktøyet du har er databladet for AVRen du bruker. Selv om det kan være vanskelig å finne fram i starten, er det til stor hjelp når du lærer deg å lese det.


Vi skal bruke timeren i CTC (Clear Timer on Compare match) mode. Det vil si at timeren teller opp til en verdi vi setter, når den når den verdien, trigger den en interrupt og begynner på nytt. Hvor høyere vi setter denne verdien, hvor lengre tidsintervall får vi.

Nå må vi beregne verdien den skal telle opp til for å få tidsintervallene 250ms og 100ms. Det er dette vi har AVRCalc til.
Så åpne AVRCalc, gå til fanen "Timer", velg klokkefrekvensen AVRen kjører på (I mitt tilfelle 16MHz) og ønsket tidsintervall.

Nå vil den vise OCR-verdiene som er den verdien timeren teller til. Men verken en 8 bit eller 16 bit timer vil kunne telle så lenge som 250ms ved 16MHz. Derfor har vi prescaler, som rett og slett deler klokkefrekvensen, slik at vi kan telle lengre. Sett prescaler til 256 slik at den deler klokken på 256, mao. 16M/256 = 62500 = 65.2kHz. 8 bit timeren vil fortsatt ikke greie det, ikke en gang med prescaler på 1024. Så det enkleste er å bruke en 16 bit timer.



Slik, 0% error. Gjør det samme med 100ms og skriv ned OCR-verdiene du får. De skal vi bruke i programmet. Ved 16MHz og prescaler på /256 fikk jeg 0x3D08 for 250ms og 0x1869 for 100ms.

Nå kan vi sette opp timeren. Da må vi konfiguere noen registere, på samme måte som vi gjør med feks. DDRx.


Som jeg sa tidligere, vil vi ha timeren i CTC mode og bruke OCR til å velge tidsintervall. Det må da bli mode 4. Her kan vi lese av at vi må sette WGMn2 høy (1). n er nummeret på timeren, og siden vi bruker timer 1, blir det da WGM12 vi må sette høy.

Blar vi da litt videre ned i databladet finner vi et register kalt TCCR1B (Timer/Counter1Control Register B) hvor WGM12 befinner seg.


Kode

TCCR1B |= (1<<WGM12);
For de som ikke skjønner denne:
Navnene TCCR1B og WGM12 er definert i "io.h" kan man si.WGM12 er egentlig bit-nummeret til WGM12, som vi kan se på bildet er 3.
Så la oss forenkle linjen.

TCCR1B er mest sannsynlig 00000000 til å starte med.
Deretter lager vi en byte med (1<<WGM12) som egentlig er (1<<3). Dette skyver 1, 3 plasser til venstre slik at vi får 00001000. Så bruker vi | som er OR, eller ELLER på godt norsk. Da får vi:

00000000
00001000 OR
00001000

Her kunnet vi like godt skrevet "TCCR1B=(1<<WGM12)", uten OR. Men om registeret ikke hadde vært blankt, ville vi da skrevet over de andre verdiene. Derfor er det god skikk å bruke OR i slike tilfeller.

Nå må vi også sette prescaleren. Som du ser på bildet av TCCR1B, har vi 3 bit kalt CS10, CS11 og CS12. Dette er for å sette prescaleren.

Som vi ser må vi sette CSn2, altså CS12 for timer 1, for å få en prescaler på 256. Siden dette er samme register som WG12 er i, kan vi ta det på samme linje slik:

Kode

TCCR1B |= (1<<WGM12) | (1<<CS12);
Når vi først er i gang, la oss aktivere interrupt når timeren når OCR. For å gjøre det må vi skrive til registeret TIMSK.

Ved å sette OCIE1A aktiverer vi interrupt når timeren når OCR, eller OCR1A for å være nøyaktig.

Kode

TIMSK |= (1<<OCIE1A);
La oss også sette OCR1A til 250ms med verdien vi kom frem til.

Kode

OCR1A = 0x3D08;
Nå må vi inkludere filen "avr/interrupt.h" i toppen av programmet for å kunne bruke interrupts.

Kode

#include <avr/interrupt.h>
Vi må også sette pin 0 på PORTC som utgang i tillegg til pin 1.

Kode

DDRC = 0x03;		//Sett PORTC.0 og .1 som utganger
For at interrupts skal fungere _må_ man kalle funksjonen sei().

Kode

sei();			//Aktiver interrupts globalt
Så må vi lage en ISR som kjøres når timeren når OCR1A.

Kode

ISR(TIMER1_COMPA_vect){					//Timer 1 compare A interrupt
	PORTC ^= ~0xFE;						//Toggle PORTC.7
}
Liste over interruptvectornavn som "TIMER1_COMPA_vect" finner du her (link).

Programmet så langt:

Kode

#define F_CPU 16000000

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

int main(void){
	DDRC = 0x03;						//Sett PORTC.0 og .1 som utganger

	//Sett opp Timer 1 (16 bit)
	TCCR1B |= (1<<WGM12) | (1<<CS12); 	//CTC mode, /256 prescaler
	TIMSK |= (1<<OCIE1A); 				//Aktiver CTC interrupt
	OCR1A = 0x3D08;						//250ms ved 16MHz, prescaler /256

	sei();								//Aktiver interrupts globalt

	while(1){
		PORTC ^= 0x02;					//Toggle PORTC.1
		_delay_ms(500);					//Hvert 500ms
	}

	return 0;
}

ISR(TIMER1_COMPA_vect){					//Timer 1 compare A interrupt
	PORTC ^= ~0xFE;						//Toggle PORTC.7
}
Nå vil LEDen på pin 0 blinke hvert 250ms og på pin 1 blinke hvert 500ms.
Nå trenger vi bare å endre timeren til 100ms når vi trykker på knappen og tilbake til 250ms når vi trykker igjen.

La oss bla opp på "External Interrupts" i databladet.

Siden jeg har koblet bryteren til INT7, og vil trigge en interrupt når signalet stiger (går på) må jeg sette ISC70 og ISC71 høy.

Kode

EICRB |= (1<<ISC71) | (1<<ISC70);
(har nådd 10-bildegrensen fortsetter i neste post)

På neste side i databladet ser vi at vi må aktivere interrupts for INT7 ved å sette INT7 høy i EIMSK.

Kode

EIMSK |= (1<<INTF7);
Vi må også sette pin 7 på PORTE (INT7) til inngang og aktivere den interne pull-up-motstanden.

Kode

DDRE = 0x00;                        //Sett PORTE som inngang
PORTE = 0x80;                        //Aktiver intern pull-up på PORTE.7
Hvis vi skal endre farten frem og tilbake, må vi ha noe til å huske på om vi blinker på 100 eller 250ms. Derfor lager vi en ny variabel utenfor main() siden den skal brukes av ISRen til INT7.

Kode

//Representerer nåværende timerverdi 0 = 250ms, 1 = 100ms
volatile uint8_t SpeedFlag;
Grunnen til at jeg setter den volatile er fordi optimizeren ikke skal optimisere den bort (link).

Vi må også lage ISRen for INT7.

Kode

ISR(INT7_vect){                            //Ekstern interrupt 7 ISR
    if(SpeedFlag){                        //Hvis 100ms
        OCR1A = 0x3D08;                    //Sett timer til 250ms
        SpeedFlag = 0;                    //Sett flagg til 250ms
    }else{                                //Hvis 250ms
        OCR1A = 0x1869;                    //Sett timer til 100ms
        SpeedFlag = 1;                    //Sett flagg til 100ms
    }
}
Vårt ferdige program:

Kode

#define F_CPU 16000000

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

//Representerer nåværende timerverdi 0 = 250ms, 1 = 100ms
volatile uint8_t SpeedFlag;

int main(void){
    DDRC = 0x03;                        //Sett PORTC.0 and .1 as output
    DDRE = 0x00;                        //Sett PORTE som inngang
    PORTE = 0x80;                        //Aktiver intern pull-up på PORTE.7
    
    //Sett opp Timer 1 (16 bit)
    TCCR1B |= (1<<WGM12) | (1<<CS12);     //CTC mode, /256 prescaler
    TIMSK |= (1<<OCIE1A);                 //Aktiver CTC interrupt
    OCR1A = 0x3D08;                        //250ms ved 16MHz med prescaler /256

    EICRB |= (1<<ISC71) | (1<<ISC70);     //Sett opp ekstern interrupt ved stigende signal på PORTE.7(INT7)
    EIMSK |= (1<<INTF7);                 //Aktiver ekstern interrupt 7

    sei();                                //Aktiver interrupts globalt

    while(1){
        PORTC ^= 0x02;                    //Toggle PORTC.1
        _delay_ms(500);                    //Hvert 500ms
    }

    return 0;
}

ISR(TIMER1_COMPA_vect){                    //Timer 1 compare A ISR
    PORTC ^= ~0xFE;                        //Toggle PORTC.7
}

ISR(INT7_vect){                            //Ekstern interrupt 7 ISR
    if(SpeedFlag){                        //Hvis 100ms
        OCR1A = 0x3D08;                    //Sett timer til 250ms
        SpeedFlag = 0;                    //Sett flagg til 250ms
    }else{                                //Hvis 250ms
        OCR1A = 0x1869;                    //Sett timer til 100ms
        SpeedFlag = 1;                    //Sett flagg til 100ms
    }
}
Du kan selvfølgelig gjøre koden enklere, men syns jeg fikk med en del viktige ting å huske på.

Håper noen drar nytte av denne. Om noen lurer på noe så bare spør i tråden eller send PM.
Oj!

Dette så noe mer komplisert ut enn Ardunio
En liten feil.

Kode

EIMSK |= (1<<INTF7);
Skal selvfølgelig være

Kode

EIMSK |= (1<<INT7);
Det vil uansett fungere fordi de har samme plass i hvert sitt register, men det er lurt å bruke de rette navnene.

Stigern:
Det er ikke så ille som det kan se ut som. Når man først kommer i gang, går det fort.
▼ ... over en uke senere ... ▼
hei, takk for en fin guide! skal få satt meg inni den og testa ut og lært meg mer
sitte og koder på en attiny13, skal lage led-lampe til å ha i klesskapet, kor den skal lyse i X tid, og ligge i sleepmode, men trenger visst en ekstern klokke.

Men fint å se en slik guide her. Har fag om dette, så prøver å lære meg mye på hobbybasis, kjøpt en Dragon
▼ ... over en uke senere ... ▼
En veldig god guide du har skrevet