Exempel 4. Användning av en timer Kort beskrivning av Timer/counter1 i ATmega16 Timer/counter1 i ATmega16 består av en 16-bitars räknare och antal register för olika funktioner. Vi skall bara granska hur timern fungerar i output compare mode, d.v.s. då innehållet i räknaren jämförs med innehållet i ett tidkonstantregister. I figur 1 visas ett blockschema för timer/counter1 som beskriver räknaren och jämförelseenheten. Figur 1. Timer/counter med output compare unit. Timer/counter1 är alltså en 16-bitars binärräknare, TNCT1, som kan avläsas eller skrivas till i form av två 8-bitars I/O-register, TCNT1H och TCNT1L. Skrivning och läsning måste utföras som två operationer vilket kan leda till fel då alla bitar inte uppdateras samtidigt. Därför överförs den högre byten först till ett temporärt register, TEMP. Då man vill skriva in ett nytt värde till TCNT1 måste den högre byten TCNT1H skrivas först. Data mellanlagras då i TEMP. Då man skriver den lägre byten överförs den högre byten från TEMP samtidigt som den lägre byten skrivs in till TCNT1L. Då räknaren avläses skall den lägre byten avläsas först. Den högre byten överförs samtidigt till TEMP. Det finns två 16-bitars tidkonstantregister som används vid jämförelse av värdet i räknaren, nämligen OCR1A och OCR1B. I blockschemat har bara ett register ritats ut, OCR1x. Skrivning och avläsning av dessa register sker även via två 8 bitars register OCR1xH och OCR1xL. Här måste också den högre byten skrivas först. Det finns två styrregister, TCCR1A och TCCR1B, med vars hjälp timer/counter1 kan programmeras upp för olika funktioner. TIMSK är ett maskeringsregister för de avbrott som kan utnyttjas för timern. TFIR är ett flaggregister där vissa bitar sätts vid olika händelser. Har man valt output compare mode kan man få ett avbrott (TIMER1 COMPx) då innehållet i räknaren är lika med innehållet i OCR1x. Samtidigt sätts en bit i TFIR. Det går även att sätta eller nollställa en bit på ett anslutningsstift (OC1x) via en vågformsgenerator. Utsignalen till stiftet kan även väljas som en PWM-signal (pulsbreddmodelerad signal). Vi går inte närmare in på dessa funktioner. Som insignal till räknaren kan man använda en yttre klocksignal eller I/O-klocksignalen från processorn. Den senare signalen kan ledas via en frekvensdelare (prescaler) som tar ned frekvensen med en faktor N. Värdet på N kan väljas med hjälp av tre bitar i styrregistret TCCR1B. För frekvensen på klocksignalen till räknaren, f T1, gäller
2 f = T1 N (1) Om innehållet i tidkonstantregistret betecknas T blir tiden t som det tar för TCNT1 att räkna från noll till T t = T (2) f T1 Styrregistren i timer/counter1 Timer/couner1 programmeras upp med de två styrregistren TCCR1A och TCCR1B. Figur 2. Styrregistret TCCR1A. Bitarna 6 och 7 bestämmer funktionssätt för utsignalen OC1A och bitarna 4 och 5 för utsignalen OC1B enligt följande: COM1A1/COM1B1 COM1A0/COM1B0 Beskrivning 0 0 OC1A/OC1B bortkopplade (inte i användning) 0 1 Byt tillstånd på OC1A/OC1B vid jämförelseträff 1 0 Sätt OC1A/OC1B låg vid jämförelseträff 1 1 Sätt OC1A/OC1B hög vid jämförelseträff Skrivs bitarna 2 och 3 (FOC1A och FOC1B) höga tvingas räknaren i ett tillstånd som motsvarar jämförelseträff (compare match) utan att innehållet i räknaren skulle förutsätta detta. Inget avbrott genereras dock. Bitarna WGM11 och WGM10 bestämmer operationsmetod för räknaren tillsammans med bitarna WGM13 och WGM12 i TCCR1B. De flesta av dessa 16 kombinationer har att göra med PWMfunktionerna och beskrivs inte här. Metod WGM13 WGM12 WGM11 WGM10 Beskrivning Toppvärde 0 0 0 0 0 Normal räknare 0xFFFF 4 0 1 0 0 CTC (Clear Timer on Compare match) OCR1A Vid metod 0 fungerar timer/counter1 som en normal räknare som räknar upp tills värdet blir 0xFFFF. Därefter nollas räknaren. Genom att skriva in ett värde i räknaren varje gång som den flödar över kan räkneperioden bestämmas mellan och 0xFFFF. Ett avbrott kan genereras då räknaren flödar över. Vid metod 4 jämförs innehållet i OCR1A (OCR1B kan inte användas). Då räknarens värde är lika med OCR1A nollas räknaren. Ett avbrott kan då genereras.
Bitarna i styrregister TCCR1A visas i figur 3. De två högsta bitarna har att göra med fångstregistret, som inte behandlats här. WGM13 och WGM12 är de två första bitarna i föregående tabell. Det tre lägsta bitarna CS12, CS11 och CS10 bestämmer insignalen till räknaren. 3 Figur 3. Styrregistret TCCR1B Följande tabell anger hur klocksignalen väljs. N är nedskalningsfaktorn från frekvensdelaren. CS12 CS11 CS10 Beskrivning 0 0 0 Timern stoppad 0 0 1 N = 1 (CLK-I/O direkt) 0 1 0 N = 8 (från frekvensdelaren) 0 1 1 N = 64 (från frekvensdelaren) 1 0 0 N = 256 (från frekvensdelaren) 1 0 1 N = 1024 (från frekvensdelaren) 1 1 0 Yttre klocksignal från T1-stiftet. Räknar på fallande kant 1 1 1 Yttre klocksignal från T1-stiftet. Räknar på stigande kant Maskeringsregistret TIMSK innehåller bitar för påkoppling av avbrott från timer/counter1, se figur 4. Figur 4. Maskeringsregistret TIMSK. TICIE1: Avbrott vid fångstoperation om biten 1. OCIE1A: Avbrott vid jämförelseträff med OCR1A om biten 1. OCIE1B: Avbrott vid jämförelseträff med OCR1B om biten 1. TOIE1: Avbrott då räknaren flödar över om biten 1. Flaggregistret TIFR innehåller bitar som sätts då något av de fyra ovanstående tillstånden inträffar, se figur 5. Figur 5. Flaggregistret TIFR.
4 ICF1: Biten sätts vid en fångstoperation. OCF1A: Biten sätts vid jämförelseträff med OCR1A. OCF1B: Biten sätts vid jämförelseträff med OCR1B. TOV1: Biten sätts då räknaren flödar över. Bitarna nollställs automatiskt när servicerutinen för avbrott utförs. Om inte avbrott används kan bitarna nollställas genom att skriva in en etta på bitens plats i TIFR. De bitar som inte visas i TIMSK och TIFR har med de timer0 och timer2 att göra. Uppgiften Gör en assemblerutin för väldefinierade väntetider som kan väljas mellan 0,1 s och 25 s. Gör även ett testprogram som med PB0 i PORTB signalerar SOS med Morse-kod. Tiderna för hög/låg på PB0 definieras i programminnet som en lista med ett direktiv för definiering av konstanter. Systemets klockfrekvens antas vara 4 MHz. Vänterutinerna WaitT Vi kan göra en vänterutin för valbara väntetider på följande sätt. Vi programmerar upp Timer/counter1 så att den fungerar enligt metod 4 (Clear Timer on Compare match). Vi sätter in en sådan tidkonstant i OCR1A at vi får avbrott med intervallen 0,1 s. I ett generellt register har vi en annan tidkonstant ( ett heltal med enheten 0,1 s) som minskas med ett varje gång som timern gör avbrott. Då innehållet i registret blir noll har väntetiden förflutit. Vi bestämmer först faktorn N för frekvensdelaren och tidkonstanten som skall sättas in i OCR1A med hjälp av formlerna (1) och (2). Maximalt värde vi kan sätta in i OCR1A är T max = 2 16 1. Kombinerar vi (1) och (2) och löser ut N får vi det minsta värde N kan ha. Med ett lägre värde kommer vi inte upp till 0,1 s. t 4 10 s 0,1s N min = = = = 6,1 f 16 T1 Tmax 2 1 N kan väljas 1, 8, 64, 256 eller 1024. Vi väljer N = 8. Vi löser ut tidkonstanten T. Med N = 8 får vi 6 t 4 10 s 0,1s 4 T = = = 5 10 N 8 Vi skall nu programmerar upp Timer/counter1. Vi börjat med styrregistret TCCR1A. Vi vill inte påverka några utsignaler, därför skall de 4 högsta bitarna vara nollor. För metod 4 skall vi välja WGM11 = WGM10 = 0. Vidare sätter vi FOC1A = FOC1B = 0, d.v.s. TCCR1A = 0b00000000 I TCCR1B sätts de tre högsta bitarna noll. För metod 4 skall WGM13 = 0 och WGM12 = 1. För att få N = 8 i frekvensdelaren sätts CS12 = 0, CS11 = 1 och CS10 = 0. Vi får -1 6-1 TCCR1B = 0b00001010 I OCR1A skall vi sätta tidkonstanten T :s värde 50000 1 = 49999 för räknaren räknar från 0, inte från 1! OCR1A = 49999 Observera att OCIE1A är ett registerpar vars högre byte måste skrivas in före den lägre byten!
Sedan skall vi koppla på avbrottet för TIMER1 COMPA i registret TIMSK. Biten som skall sättas ett är OCIE1A (bit 4). Då inga andra avbrott från timers behövs blir TIMSK = 0b00010000 = 1<< OCIE1A I stället för att checka att OCIE1A finns i bit fyra och sätta denna bit, kan vi i assemblerprogrammet använda ett uttryck där en etta skiftas OCIE1A steg åt vänster. Biten OCIE1A definieras som 4 i inlclude-filen m16def.inc. Till sist kopplar vi på avbrotten globalt med instruktionen SEI. För att få väntetider mellan 0,1 och 25 sekunder kan vi använda ett 8-bitars generellt register som minskas med ett varje gång timern gör avbrott. Vi kallar detta register Delay. I avbrottsrutinen, Timer1Int, minskar vi Delay med ett såvida detta register inte redan är noll. Adressen till avbrottsvektorn för TIMER1 COMPA är 0x000C. Här sätter vi in instruktionen Jmp Timer1Int Vi gör även en subrutiner i vilka programmet väntar tills väntetiden har förflutit. Vi kallar denna rutin WaitT. Förrän vi kallar på denna rutin borde vi ladda väntetiden (ett heltal med enheten 0,1 s) i. Delay. Programmet skall sedan snurra i en slinga tills Delay. Här kan vi få ett fel mellan 0 och 0,1 s beroende på vilket värde som räknaren TCNT1 råkar ha. Nollar vi TCNT1 i början av rutinerna borde vi komma i från detta fel, men om timern har gjort ett avbrott just före nollningen har kanske Delay redan minskats med ett och det blir ändå ett fel. Där för laddar vi inte in väntetiden i Delay före anropet utan i ett annat register TempT. Innehållet i detta register överförs sedan till Delay efter det TCNT1 nollats i vänterutinerna. 5 Huvudprogrammet Först initieras stackpekaren. Sedan programmeras Timer/counter1 upp enligt ovanstående riktlinjer och biten PB0 väljs som utgång. Denna bit skall sättas och nollställas enligt ett tidschema som sätts in i form av konstanter i programminnet med ett DB-direktiv. Dessa väntetider placeras i minnet f.o.m. adressen 0x002A och adressetiketten Tcode pekar på de två första byten. Tidschemat avslutas med en nolla. Programmet skall nolla PB0 och sedan hämta den första väntetiden från programminnet. Tiden sätts in i TempT och rutinen WaitT anropas. Då väntetiden är slut skall PB0 sättas = 1 och följande tid hämtas o.s.v. (En LED i utvecklingskortet STK500 är kopplad så att den lyser då utsignalbiten är låg) Vi kan hämta väntetiderna från programminnet med hjälp LPM-instruktionen och indirektadressering med postinkrement. Då utnyttjas dubbelregistret Z som adressregister. Registret måste först initieras med adressen till den första väntetiden. Här bör vi observera att adressetiketten Tcode motsvarar en adress till ett ordorganiserat minne (16 bitar/ord), men vi behöver ha en byteadress. Därför måste vi multiplicera adressen med två, d.v.s. skifta bitarna i adressen ett steg till vänster. Initieringen av Z, som måste göras i två steg blir ldi ZL, LOW(Tcode<<1) Kommentar ldi ZH, HIGH(Tcode<<1) I detta exempelprogram finns det inte behov av att göra något under väntetiden, men om det finns kan man inkludera detta i vänteslingorna. Många extra instruktioner i vänteslingorna medför dock att väntetiderna blir mindre exakta. I ett system där vi måste utföra flera uppgifter parallellt är det bättre att konstruera ett enkelt realtidsoperativsystem, där de olika uppgifterna utförs som skilda processer (trådar). Vi kunde även använda Timer/counter1 direkt utan subrutinen WaitT. För varje ny väntetid beräknar vi värdet på tidkonstanten T och sätter in den i timerns register OCR1A. Om vi väljer N = 1024 blir den maximala väntetiden 16 s.
6 Assemblerprogrammet ;==================================================================== ; Exempel på en vänterutin som utnytjar avbrott från Timer1 ; Ett exempelprogram som matar ut SOS i Morse-kod på PB0 ;====================================================================.include "m16def.inc".equ TIMECONST = 49999 ;Definition av register.def FlagSave = r12.def Delay = r13.def Temp = r16.def TempT = r18 ;Tidkonstant för timer1 ;49999 ger avbrott var 0,1:te s ;Register för flaggor vid avbrott ;Kort fördröjningsräknare ;Temporärt register ;Temporärt register för tidkonstant ;Avbrottsvektorer.cseg.org 0x0000 jmp Reset.org 0x000C jmp Timer1Int ;Morsekoden som tider med enheten 0,1 s.org 0x002A Tcode:.db 1, 9, 1, 9, 1, 12, 6, 4, 6, 4, 6, 12, 1, 9, 1, 9, 1, 30,0,0 ;--------------------------------------------------------------------- ; Avbrottsservicerutin för "Timer1 compare match on OCR1A" ; Minskar värdet i fördröjningsräknarna Delay om värdet i detta ; register är olika noll. ;--------------------------------------------------------------------- Timer1Int: in FlagSave, SREG ;Spara statusregistret i FlagSave tst Delay ;= 0? breq ExInt dec Delay ;Minska räknaren Delay med 1 ExInt: out SREG, FlagSave ;Återställ statusregistret reti ;Retur från avbrott ;-------------------------------------------------------------------- ; WaitT, vänterutin med en 8-bitars fördröjningsräknare. ; Upplösningen 0,1 s och maximal väntetid 25,5 s. Anropas med ; väntetiden i TempT med enheten 0,1 s ;-------------------------------------------------------------------- WaitT: clr Temp out TCNT1H, Temp out TCNT1L, Temp ;Synkronisera (nolla Timer1) mov Delay, TempT ;Ladda räknarregistret med tiden Test: tst Delay ;Delay = 0? brne Test ;Vänta tills Delay = 0 ret ;----------------------------------------------------------------------
7 ; Reset, huvudprogrammet startar här med att initiera stackpekaren och ; programera upp Timer1 i CTC-mod med avbrott. Biten PB0 sätts = 0, ; en väntetid hämtas från Tcode, programmet väntar i WaitT, sedan ; sätts PB0 = 1. Detta schema upprepas tills väntetiden 0 hämtas. Då ; startas schemat igen från början av Tcode. ;---------------------------------------------------------------------- Reset: ldi Temp, LOW(RAMEND) ;Initiera stackpekaren out SPL, Temp ldi Temp, HIGH(RAMEND) out SPH, Temp clr Temp ;Nolla temp out TCCR1A, Temp ;OC1A, OC1B frånkopplade ldi Temp, 0b00001010 ;Nolla T1 då T1= OCR1A, K = 8 out TCCR1B, Temp ;CTC mod ldi Temp,HIGH(TIMECONST) out OCR1AH, Temp ;Högre byten först till OCR1A!! ldi Temp,LOW(TIMECONST) out OCR1AL, Temp ;Lägre byten sedan till OCR1A ldi Temp, (1<<OCIE1A) ;Skifta 1 till OCIE1A:s plats out TIMSK, Temp ;Avbrott då TCNT1 = OCR1A ldi Temp,0b00000001 out DDRB, Temp ;PB0 utgång clr Delay ;Nolla fördröjningsräknaren sei ;Koppla på avbrotten globalt Restart: ldi ZL, LOW(Tcode<<1) ;Ordadress till byteadress med skift ldi ZH, High(Tcode<<1) ;Z pekar på först kodbyten Loop: cbi PORTB, 0 ;Nollställ PAB0 i port B lpm TempT, Z+ ;Hämta en kodbyte tst TempT ;= 0? breq Restart ;Starta på nytt från början rcall WaitT ;Vänta så lång tid som anges i koden sbi PORTB, 0 ;Sätt PB0 lpm TempT,Z+ ;Hämta följande kodbyte tst TempT ;= 0? breq Restart ;Starta om från början av koden rcall WaitT ;Vänta så lång tid som anges i koden rjmp Loop ;Fortsätt med nästa varv ;----------------------------------------------------------------------