Timere og avbrudd på AVR

Etter å ha lest om timere og avbrudd («interrupts») en stund nå, begynner endelig ting å falle på plass. Til å begynne med var denne delen av AVR veldig uoversiktlig, fordi det finnes et hav av bits i forskjellige registre som man må kjenne til og forstå.

Men så begynte jeg å innså at man ikke trenger å fullt ut forstå alle mulige konfigurasjoner, bare for å ta i bruk en timer og ha litt nytte av den .. Selv med grunnleggende forståelse kan man benytte en timer til mye forskjellig.

Og for å gjøre meg selv en tjeneste skal jeg fra nå av konsekvent kalle timere for tellere, kanskje dette fordrer mer intuitiv tenking. (Når jeg tenker på timere, så tenker jeg fort på timere i JavaScript – noe som bare forvirrer i denne sammenhengen ..)

Introduksjon

En teller har som oppgave å telle fra 0 og opp til og med en satt maksverdi. Også begynner den vanligvis på nytt igjen. Underveis kan telleren sammenligne tellerverdien med en gitt verdi (som du bestemmer).

Både å få treff ved sammenligning og å nå sluttverdien man skal telle til, gjør at det sendes et avbruddssignal man kan benytte til å kjøre kode man skriver selv.

Riktignok er det ikke bare tellere som sender avbruddssignaler. Det finnes andre kilder også, for eksempel PC6 (på ATmega328P i hvertfall) som ved 0 V skaper reset.

Praksis

Å ta i bruk tellere krever at man setter bits i minst et par registre. Skal man benytte et avbruddssignal til å kjøre kode, krever dette ett register til. Til flere endringer fra det som er standard (altså default), til flere registre og bits blir det ..

Avhengig av situasjonen kan det være nødvendig å deaktivere avbruddssignaler fordi man skal gjøre noe viktig. Også aktiverer man igjen etterpå. Dette gjøres med cli() og sei():

// Disable interrupts globally
cli();

...

// Enable interrupts globally
sei();

Dette vil da foregå i main() eller en funksjon som kjøres derifra.

Tidtaking er ganske grunnleggende å kunne kontrollere, nesten uansett hva man skal programmere. Derfor ble dette et greit sted å begynne her. Til dette valgte jeg å styre ei lysdiode med en enkel 8-bit teller (Timer2). Og med kode som kjører ved såkalt overflyt («overflow) – dvs. når telleren når maksverdien den skal telle til og begynner helt på nytt igjen:

volatile long int counter = 0;

ISR (TIMER2_OVF_vect)
{
   counter++;

   if (counter != 0 && counter == 250)
   {
      counter = 0;
      PORTB ^= 1<<PB4;
   }
}

int main(void)
{
   // 1. Prescaler
   TCCR2B |= 1<<CS22 | 1<<CS21; // 256

   // 2. Fast PWM mode
   TCCR2A |= 1<<WGM21 | 1<<WGM20;
   TCCR2B |= 1<<WGM22;

   // 3. Resolution, value to count to
   OCR2A = 0xFA; // Decimal value: 250

   // 4. Interrupt types
   TIMSK2 |= 1<<TOIE2;

   ////////////////////

   // Digital pin 12 = PB4
   DDRB |= 1<<PB4;
}

Denne kodebiten kan skru ei lysdiode på/av hvert sekund, takket være if-betingelsen som gjør at man venter lenge nok. Uten if-betingelsen ville dioden blitt tent/slukket flere hundre ganger i sekundet. (Se forklaring under EKSEMPEL lenger ned, for matematikken bak.)

Dioden er tilkoblet pinne 4 (PB4) på port D. Den skrus av/på med «bit shifting». For å sende ut må pinnen også være satt til dette i DDRB.

Om man ikke hadde vært avhengig av å sette maks tellerverdi over (i OCR2A), kunne man benyttet «Normal» i stedet for «Fast PWM». Da ville koden blitt kortere, siden flere standardinnstillinger benyttes:

int main(void)
{
   // 1. Prescaler
   TCCR2B |= 1<<CS22 | 1<<CS21; // 256

   // 2. Normal mode
   // (no settings, using default)

   // 3. Interrupt types
   TIMSK2 |= 1<<TOIE2;

   ////////////////////

   // Digital pin 12 = PB4
   DDRB |= 1<<PB4;
}

Prøver man å telle sekunder nå vil den telle for tregt. Den vil telle til 255 og ikke 250.


Etter å ha prøvd mange konfigurasjoner, hadde jeg til slutt 3 stk. lysdioder som ble styrt med hver sin teller:

For å ha noe å sammenligne med ble lysdioda på Arduino-brettet slukket og tent på "gamlemåten", i while(1) i main() med _delay_ms(1000) mellom hver endring.

Over tid viste det seg at tellerne nok ikke helt holder tiden helt presist. Lysdiodene begynte å henge etter. Så det er tydeligvis rom for forbedring.

Teori

Også over til teorien som forklarer hvordan (mye av) dette fungerer ..

Telleroppførsel

Litt tidligere i vår lærte jeg å benytte ADC-en for avlesing av analoge signaler. Da ble det naturlig at jeg lærte PWM etterpå, for å kunne generere "liksom" analoge utsignaler ut. Jeg testet både «Fast PWM» og «Phase Correct PWM».

Dermed brukte jeg en teller allerede den gangen, men uten å helt forstå hvordan de fungerte; fordi jeg måtte fikle mye med registre og bits jeg ikke skjønte meg på, før ting endelig fungerte.

«Fast PWM» og «Phase Correct PWM» er bare to av flere muligheter. Her er oppgaven til telleren å skape PWM. Men det finnes andre bruksområder også.

Timer/Counter Control Register (TCCR 0-2 A/B)

Type oppførsel («Normal», «Phase Correct PWM», «CTC» og «Fast PWM») som man ønsker telleren skal ha, styres via bits ved navn «Waveform Generation Mode» (WGM). Timer0 har WGM02, WGM01 og WGM00 fordelt over register TCCR0A og TCCR0B. Tilsvarende gjelder for Timer1 med WGM13, WGM12, WGM11 og WGM10 i TCCR1A og TCCR1B. Mens Timer2 har WGM22, WGM21 og WGM20 i TCCR2A og TCCR2B.

Legg merke til navnelikheten. Nummeret til telleren går igjen i navnene til registrene og bitsene, ellers er navnene like. Dette er vanlig for mange registre. Og kjenner man navnet på et register og eller en bit, blir det enklere å søke opp i dataarket.

Ikke-PWM

Om man ikke trenger PWM eller relatert ekstrafunksjonalitet, kan man selvsagt la være å skru på dette når man skal bruke en teller.

Da settes telleren til «Normal» eller «CTC». Sistnevnte står for «Clear Timer on Compare Match», her blir telleren deaktivert når den sammenligner to verdier og får et treff.

Pulsbreddemodulasjon (PWM)

Med PWM gis man evnen til å sende korte spenningspulser, noe som gjør at snittspenningen blir lavere enn det som ville vært tilfellet digitalt.

Med «Fast PWM» telles det fra 0 og til maksverdi (inklusiv), slik som ved «Normal». Om man derimot benytter «Phase Correct PWM» telles det ned til 0 igjen, etter at maksverdien er nådd .. Det betyr at det går ca dobbelt så lang tid å telle ferdig. Hvis PWM benyttes gir dette mer korrekte spenningspulser.

Selve PWM-funksjonen er ikke av interesse i dette innlegget. Men ved å aktivere en telleroppførsel som støtter PWM, får man annen funksjonalitet med på kjøpet. Som muligheten til å bestemme en egen maksverdi – altså hvor langt telleren skal telle, før den snur eller begynner på nytt. Les mer under Oppløsning.

Compare Output Mode (COM)

Når telleroppførsel er satt til en av «Normal», «Fast PWM», osv, må man videre bestemme hvordan man skal sammenligne verdier. Hvis dette er ønsket. Da må man også velge hva som skal gjøres når man får et treff.

Hvis en pinne skal benyttes for sende signal ut, må man også bestemme når og hvor lenge dette skal foregå. Fra teller er 0 til teller får treff? Eller fra treff til maksverdi?

For Timer0 styres alt dette via bitsene COM0A1 og COM0A0 i TCCR0A. For Timer1 og Timer2 gjelder tilsvarende, men Timer1 har flere mulige konfigurasjoner enn Timer0 og Timer2. Denne 16-bit timeren er hakket mer avansert, den gir flere muligheter.

Output Compare [pin] (OC 0-2 A/B)

I avsnittene for Compare Output Mode i dataarket ser man raskt navnene OC0A, OC0B, OC1A, OC1B og OC2A. Dette er pinnene som benyttes.

Samme pinne har flere navn – som bildet viser. Dette kan til tider være forvirrende:

Pinnene benyttes avhengig av hva telleren skal brukes til. Hver teller benytter seg av sine "egne"» pinner .. Pinnene 0C0A og 0C0B "tilhører" Timer0 osv.

Når en pinne skal benyttes må man fortsatt sette retning (inn/ut – input/output), enten 0 eller 1, i DDR-registeret. DDRB-registeret for pinner Port B, osv.

Frekvens

Hvor raskt en teller jobber kommer an på systemklokka til mikrokontrolleren og hvor mye signalet fra denne nedskaleres for telleren (med en «prescaler»). For å være helt klar; klokkesignalet til telleren blir tregere, men ikke systemklokka.

På Arduino Uno-brettet er det ekstern krystall som gir 16 MHz-klokkesignal til mikrokontrolleren (ATmega328P). Denne brukes som standard, så lenge man ikke selv har tuklet med noe.

Prescaler

Alle tellere har prescaler som støtter nivåer mellom 1 (ingen endring) og 1024. Selv med stor nedskalering blir frekvensen til en teller minst 16 000 000 Hz / 1024 = 15 625 Hz, altså 15.625 kHz. Telleren teller da 15 625 ganger per sekund (!). Ved mindre grad av nedskalering vil den telle enda raskere.

Man har også muligheten til å bruke ekstern klokkekilde, hvis man vet hvordan ..

Timer/Counter Control Register (TCCR 0-2 B)

For Timer0 på ATmega328P settes prescaler i registeret TCCR0B, med bitsene CS02 til CS00. For Timer1 er det register TCCR1B, med bits CS12 til CS10. Tilsvarende gjelder Timer2.

Oppløsning

Størrelsen på registeret til telleren bestemmer hvor langt telleren kan telle ..

På ATmega328P er det 2 stk. 8-bit tellere og 1 stk. 16-bit teller. En 8-bit teller har et register med plass til 2^{8} = 256 mulige verdier, dvs. 0 – 255. Antall mulige verdier i 16-bit telleren er 2^{16} = 65 536, altså 0 – 65 535.

Er 256 eller 65 536 muligheter best? Vel, det kommer i grunn an på hva du skal bruke telleren til og hvor høy presisjon du trenger ..

EKSEMPEL:

Du ønsker å sende en puls ut ca hvert sekund. Da må du fastslå lengden på et sekund, ellers vil pulsene bli sendt for ofte eller for sjelden.

Systemklokkefrekvensen delt på prescaler må derfor gi en fornuftig frekvens man kan jobbe med, helst et rundt tall. (Nedskalering på 256 gir tellerfrekvens lik 62 500 …, kanskje dette passer?) Denne verdien må så deles på antall verdier telleren skal telle gjennom før den sender avbruddssignalet.

Med antall tellerverdier (dvs. oppløsning) lik 250 for en 8-bit teller, og tellerfrekvens lik 62 500, vil telleren telle ferdig 62 500 / 250 = 250 ganger per sekund (!).

Ved å benytte avbruddssignalet for overflyt kjøres det kode ved hvert avbrudd. Denne koden trenger da å telle antall ganger den kjører. Og hver gang telleren er lik 250 kan et nytt sekund telles og ny puls sendes. Voila! Man har nå en tellerstyrt puls og er ikke avhengig av _delay_ms(1000) i while(1)-løkka.

Fremgangsmetoden kan virke klossete, men husk at dette er bare en av mange muligheter. Du kan telle flere ting samtidig her – i grunn hva som helst. Og du kan telle når som helst. Og uten å være avhengig av while(1)-løkka i main().

Output Compare Register (OCR 0-2 A)

For å sette maksverdi for en teller (dvs. oppløsning), må man lagre denne verdien til et register så telleren får tak i den. Timer0 benytter OCR0A, Timer1 har OCR1A og Timer2 bruker OCR2A. Siden Timer1 er 16-bit er selvfølgelig OCR1A-registeret 16-bit også. De andre to er 8-bit, slik som tellerne.

Men for at disse registrene skal virke, må valgt telleroppførsel støtte at TOP-verdien (tellerens maksverdi) defineres fra disse registrene. Tabellen i dataarket over hver telleroppførsel («Normal», «PWM, Phase Correct», osv.) lister opp hvilke som støtter bruk av dette registeret.

Merk: Timer1 kan benytte register ICR1 i stedet for OCR1A. Dermed blir OCR1A tilgjengelig for andre ting.

Avbruddsrutiner

ATmega328P har over 30 stk. avbruddssignaler. For at disse skal virke må man globalt aktivere for det med sei().

For å kjøre kode når et avbruddssignal er sendt, må man sette en bit i et avbruddsregister. Man må også skrive avbruddsfunksjonen man ønsker å kjøre.

Timer/Counter Interrupt Mask Register (TIMSK 0-2)

Når kode skal kjøres ved avbruddssignal fra teller, må det settes en bit i TIMSK-registeret som tilhører telleren man benytter. Timer0 har register TIMSK0 for dette. Timer1 og Timer2 har tilsvarende registre.

For overflytavbrudd, brukt i eksemplet lenger opp, heter biten TOIE. Dvs. TOIE0 for Timer0. Osv. For treff ved sammenligning har man egne bits for dette. Sjekk dataarket ved behov.

Konklusjon

What to say. Tellere og avbruddssignaler er litt av ei smørje når man leser om det i dataarket. Ting er spredt over titalls sider og det er mye som repeteres. Et evig virrvarr av mulige funksjoner man ikke øyeblikkelig ser vitsen med. Løsningen blir derfor å lese det som står der flere ganger og tenke igjennom hva man leser. Dette er stoff som rett og slett tar tid å bearbeide.

Tenkte først å liste opp alle relevante registre og bits i dette innlegget. Så jeg kan komme tilbake hit og sjekke ting ved en senere anledning, om jeg skulle stå fast. Dette måtte jeg etterhver gi opp. Det er simpelthen altfor mange registre og bits å beskrive. Og det er liten vits når alt uansett står i dataarket og jeg ofte må sjekke andre detaljer der uansett.

TBC

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *