Στην προηγούμενη ενότητα μάθαμε πώς να χρησιμοποιούμε τους Timers 0, 1 και 2 με την μέθοδο της δειγματοληψίας. Σε αυτή την ενότητα θα χρησιμοποιήσουμε τις διακοπές για να προγραμματίσουμε τους AVR timers.
Σημαία υπερχείλισης και διακοπές
Στην προηγούμενη ενότητα μάθαμε ότι η σημαία υπερχείλισης τίθεται σε 1 όταν ο Timer μεταβαίνει από την μέγιστη τιμή του στο 00. Επίσης είδαμε ότι κάνουμε δειγματοληψία της σημαίας υπερχείλισης με την εντολή “SBIS TIFR0, TOV0”. Με την δειγματοληψία της σημαίας TOV0 περιμένουμε μέχρι η TOV0 να γίνει 1. Το πρόβλημα με αυτή την μέθοδο είναι ότι η CPU περιμένει χωρίς να κάνει τίποτα μέχρι η TOV0 να γίνει 1. Με την μέθοδο των διακοπών η CPU μπορεί να εκτελεί άλλες εντολές μέχρι να συμβεί υπερχείλιση με τη σημαία TOV0 να γίνει 1
Όταν ενεργοποιηθούν οι διακοπές και γίνει υπερχείλιση του timer (η σημαία TOV0 γίνει 1) δηλαδή να συμβεί μια διακοπή, η CPU κατευθύνεται στο interrupt vector table και εκτελεί την αντίστοιχη ρουτίνα ISR. Έτσι μέχρι να έχουμε υπερχείλιση και να συμβεί μια διακοπή η CPU, μπορεί να εκτελεί άλλο απόσπασμα κώδικα.
Για τη χρήση μιας διακοπής θα πρέπει πρώτα να ενεργοποιήσουμε τις διακοπές, διότι όλες οι διακοπές απενεργοποιούνται κατά τη φάση του RESET. Το ΤΟΙΕx bit ενεργοποιεί την διακοπή για ένα ορισμένο timer. Τα TOIEx bit διαχειρίζονται από τους καταχωρητές TIMSKn.
Για την επίδειξη της μεθόδου των διακοπών μελετήστε το ακόλουθο πρόγραμμα.
/* Σε αυτό το πρόγραμμα, θεωρούμε ότι η PORTC είναι συνδεμένη με έξι διακόπτες και η PORTD με 6 LEDs. Αυτό το πρόγραμμα χρησιμοποιεί τον Timer0 για να παράγει μια τετραγωνική κυματομορφή στο πιν PORTB.5, ενώ ταυτόχρονα δεδομένα μεταφέρονται από την PORTC στην PORTD */
.ORG 0x0 ;θέση για το RESET
JMP MAIN
.ORG 0x20 ;θέση για την υπερχείλιση του Timer0
JMP T0_OV_ISR ;πήγαινε στο ISR για τον Timer0
;--- κυρίως πρόγραμμα
.ORG 0x100
MAIN: LDI R20, HIGH(RAMEND)
OUT SPH, R20
LDI R20, LOW(RAMEND)
OUT SPL, R20 ;αρχικοποίηση του σωρού
SBI DDRB,5 ;PB5 σαν έξοδος
LDI R20, (1<<TOIE0)
STS TIMSK0, R20 ;ενεργοποίηση διακοπής υπερχείλισης Timer0
SEI ;I=1 για καθολική ενεργοποίηση διακοπών
LDI R20, -32 ;τιμή timer για 2 us
OUT TCNT0, R20 ;φόρτωσε τον Timer0 με -32
LDI R20, 0x00
OUT TCCR0A, R20
LDI R20, 0x01
OUT TCCR0B, R20 ;Normal, internal clock, no prescaler
LDI R20, 0x00
OUT DDRC, R20 ;η PORTC σαν είσοδος
LDI R20, 0xFF
OUT PORTC, R20 ;ενεργοποίηση pull-up αντιστάσεων
OUT DDRD, R20 ;κάνε την PORTD έξοδο
;------- ατέρμων βρόγχος
HERE: IN R20, PINC ;διάβασε την PORTC
OUT PORTD, R20 ;εκχώρησε την τιμή στην PORTD
JMP HERE ;κράτησε την CPU απασχολημένη περιμένοντας διακοπή
;-------- ISR για τον Timer0
.ORG 0x200
T0_OV_ISR:
IN R16, PORTB ;διάβασε την PORTB
LDI R17, (1<<5) ;00100000 για εναλλαγή του ΡΒ5
EOR R16, R17
OUT PORTB, R16 ;εναλλαγή του ΡΒ5
LDI R16, -32 ;τιμή timer για 2us
OUT TCNT0, R16 ;φόρτωσε τον Timer0 με -32 για τον επόμενο κύκλο
RETI
Στο προηγούμενο πρόγραμμα επισημαίνουμε τα ακόλουθα σημεία:
1] Θα πρέπει να μην χρησιμοποιούμε την μνήμη προγράμματος που εκχωρείται στον interrupt vector table. Επομένως θα πρέπει να τοποθετούμε τον κυρίως κώδικα (και τον κώδικα ISR) μετά από μια ορισμένη διεύθυνση και μετά όπως π.χ. την $100. Η εντολή JMP πρέπει να είναι η πρώτη εντολή στη θέση 0000 η οποία κατευθύνει την CPU έξω από τον interrupt vector table για το λόγο ότι η CPU ξεκινά να εκτελεί εντολές από την διεύθυνση 0000 κατά τη φάση του RESET.
2] Στο απόσπασμα κώδικα ΜΑΙΝ ενεργοποιούμε τις διακοπές του Timer0 με τις ακόλουθες εντολές
LDI R20, (1<<TOIE0)
STS TIMSK0, R20 ;ενεργοποίηση της διακοπής υπερχείλισης του Timer0
SEI ;set I (καθολική ενεργοποίηση διακοπών)
3] Στο μέρος του προγράμματος ΜΑΙΝ αρχικοποιούμε τον καταχωρητή Timer0 και μετά η CPU μπαίνει σε ένα ατέρμον βρόγχο. Στον βρόγχο του προγράμματος μας η CPU διαβάζει την PORTC και στέλνει τα δεδομένα στη PORTD. Για να ενεργοποιήσουμε την διακοπή υπερχείλισης θέτουμε το bit TOIE0 σε 1. Όταν συμβεί υπερχείλιση στον Timer0 η CPU βγαίνει έξω από το βρόγχο και πηγαίνει στη διεύθυνση $0020 για να εκτελέσει την αντίστοιχη ρουτίνα ISR του Timer0. Σε αυτό το σημείο ο AVR μηδενίζει το I bit (D7 του SREG) για να δείξει ότι εκτελεί μια διακοπή και να μην εκτελέσει άλλη διακοπή μέσα σε αυτή. Με άλλα λόγια, στην αρχή της εκτέλεσης μιας ρουτίνας εξυπηρέτησης διακοπής ISR το bit I τίθεται σε 0 από την CPU για να μην έχουμε διακοπή μέσα στη διακοπή.
4] Η ρουτίνα εξυπηρέτησης διακοπής ISR για τον Timer0 είναι τοποθετημένη στη μνήμη προγράμματος στη θέση $200 για το λόγο ότι είναι πολύ μεγάλη για να χωρέσει στο διάστημα της μνήμης με διευθύνσεις $20-$21 οι οποίες είναι εκχωρημένες για την εξυπηρέτηση διακοπής του Timer0 στο interrupt vector table.
5] Στην ρουτίνα εξυπηρέτησης διακοπής ISR του Timer0 δεν χρειάζεται να μηδενίσουμε τη σημαία TOV0 διότι η CPU μηδενίζει τη σημαία TOV0 αυτόματα καθώς μπαίνει στη θέση του interrupt vector table.
6] Η εντολή RETI θα πρέπει να είναι η τελευταία εντολή της ρουτίνας ISR. Κατά την εκτέλεση της RETI η CPU αυτόματα θέτει σε 1 το bit Ι (D7 του καταχωρητή SREG) για να δείξει ότι μπορεί να εξυπηρετήσει άλλες διακοπές.
Σημείωση: Και οι δυο εντολές RET και RETI μπαίνουν και πρέπει υποχρεωτικά να υπάρχουν στο τέλος μιας ρουτίνας, για να μπορέσει η CPU να συνεχίσει από το σημείο που κάλεσε τη ρουτίνα, μετά το τέλος της εκτέλεσης της. Η RETI μπαίνει στο τέλος μιας ρουτίνας εξυπηρέτησης διακοπής για να θέσει το bit I σε 1 και να μπορεί να εκτελέσει νέες διακοπές. Αν τοποθετήσουμε μια RET σε μια ρουτίνα ISR, το bit I θα εξακολουθεί να είναι 0 μετά την εκτέλεση της, με αποτέλεσμα να μην μπορεί να εξυπηρετήσει η CPU άλλες διακοπές.
Πρόγραμμα. Το ακόλουθο πρόγραμμα χρησιμοποιεί τις διακοπές των Timer0 και Timer1 ταυτόχρονα, για να παράγει τετραγωνικές κυματομορφές στα πινς ΡΒ1 και ΡΒ3 αντίστοιχα, ενώ δεδομένα μεταφέρονται από την PORTC στη PORTD
.ORG 0x0 ;θέση για το RESET
JMP MAIN
.ORG 0x1A ;θέση της ρουτίνας ISR εξυπηρέτηση της υπερχείλισης του Timer1
JMP T1_OV_ISR ;πήγαινε στην διεύθυνση με περισσότερο μέγεθος μνήμης
.ORG 0x20 ;θέση της ρουτίνας ISR εξυπηρέτηση της υπερχείλισης του Timer0
JMP T0_OV_ISR ;πήγαινε στην διεύθυνση με περισσότερο μέγεθος μνήμης
;---κυρίως πρόγραμμα
.ORG 0x100
MAIN: LDI R20, HIGH(RAMEND)
OUT SPH, R20
LDI R20, LOW(RAMEND)
OUT SPL, R20 ;αρχικοποίηση του σωρού
SBI DDRB, 1 ;ΡΒ1 σαν έξοδος
SBI DDRB, 3 ;ΡΒ3 σαν έξοδος
LDI R20, 0x00
OUT DDRC, R20 ;κάνε την PORTC είσοδο
LDI R20, 0xFF
OUT PORTC, R20 ;ενεργοποίησε τις pull-up αντιστάσεις
OUT DDRD, R20 ;κάνε την PORTD έξοδο
LDI R20, -160 ;τιμή για 10us
OUT TCNT0, R20 ;φόρτωσε τον Timer0 με -160
LDI R20, 0x00
OUT TCCR0A, R20
LDI R20, 0x01
OUT TCCR0B, R20 ;Normal mode, int clk, no prescaler
LDI R20, HIGH(-1600) ;το υψηλότερο byte
STS TCNT1H, R20 ;φόρτωσε τον Timer1 με το υψηλότερο byte
LDI R20, LOW(-1600) ;το χαμηλότερο byte
STS TCNT1L, R20 ;φόρτωσε τον Timer1 με το χαμηλότερο byte
LDI R20, 0x00
STS TCCR1A, R20 ;Normal mode
LDI R20, 0x01
STS TCCR1B, R20 ;internal clk, no prescaler
LDI R20, (1<<TOIE0)
STS TIMSK0, R20 ;ενεργοποίησε την διακοπή υπερχείλισης του Timer0
LDI R20, (1<<TOIE1)
STS TIMSK1, R20 ;ενεργοποίησε την διακοπή υπερχείλισης του Timer1
SEI
;----- ατέρμον βρόγχος
HERE: IN R20, PINC ;διάβασε την PORTC
OUT PORTD, R20 ;και μετέφερε το στην PORTD
JMP HERE ;κράτησε την CPU απασχολημένη περιμένοντας για διακοπή
;-----ISR για τον Timer0
.ORG 0x200
T0_OV_ISR:
LDI R16, -160 ;τιμή για 10us
OUT TCNT0, R16 ;φόρτωσε τον Timer0 με -160
IN R16, PORTB ;διάβασε την PORTB
LDI R17, 0x02 ;R17=00000010 για εναλλαγή του ΡΒ1
EOR R16, R17
OUT PORTB, R17
RETI ;επέστρεψε μετά την εξυπηρέτηση της διακοπής
;-----ISR για τον Timer1
.ORG 0x300
T1_OV_ISR:
LDI R18, HIGH(-1600)
STS TCNT1H, R18 ;φόρτωσε τον Timer1 με το υψηλό byte
LDI R18, LOW(-1600)
STS TCNT1L, R18 ;φόρτωσε τον Timer1 με το χαμηλό byte
IN R18, PORTB ;διάβασε την PORTB
LDI R19, (1<<3) ;R19=00001000 για εναλλαγή του ΡΒ3
EOR R18, R19
OUT PORTB, R18
RETI ;επέστρεψε από την διακοπή
Σημειώστε ότι οι διευθύνσεις $0100, $0200 και $0300 που χρησιμοποιούμε στο παραπάνω πρόγραμμα, είναι επιλέξιμες και μπορούμε να τις αλλάξουμε σε όποια διεύθυνση θέλουμε. Οι μόνες διευθύνσεις που δεν μπορούμε να αλλάξουμε είναι η θέση reset 0000, η θέση εξυπηρέτησης υπερχείλισης του Timer0 $0020 και η θέση εξυπηρέτησης υπερχείλισης του Timer1 $001A στο interrupt vector table διότι είναι προκαθορισμένες κατά τη φάση σχεδίασης του ATmega328.
Το ακόλουθο πρόγραμμα έχει δυο διακοπές: (1) PORTC απαριθμεί κάθε φορά που ο Timer1 υπερχειλίζει. Υπερχειλίζει μια φορά το δευτερόλεπτο. (2) Ένας παλμός τροφοδοτεί τον Timer0 όπου ο Timer0 χρησιμοποιείται σαν απαριθμητής. Όταν αυτός ο απαριθμητής μετρήσει 200 παλμούς, εναλλάσσει το πιν PORTB.5. (3) Εναλλάσσει το PORTB.1 συνεχόμενα.
.ORG 0x0 ;θέση για reset
JMP MAIN
.ORG 0x1A ;θέση ISR για εξυπηρέτηση της ρουτίνας υπερχείλισης Time1
JMP T1_OV_ISR ;πήγαινε στην διεύθυνση με μεγαλύτερο χώρο μνήμης
.ORG 0x20 ;θέση ISR για εξυπηρέτηση της ρουτίνας υπερχείλισης Time0
JMP T0_OV_ISR ;πήγαινε στην διεύθυνση με μεγαλύτερο χώρο μνήμης
;----- κυρίως πρόγραμμα
.ORG 0x40
MAIN: LDI R20, HIGH(RAMEND)
OUT SPH, R20
LDI R20, LOW(RAMEND)
OUT SPL, R20 ;αρχικοποίηση σωρού
LDI R20, 0xFF
OUT DDRC, R20 ;PORTC σαν έξοδος
LDI R18, 0 ;R18=0
OUT PORTC, R18 ;PORTC=0
SBI DDRB,5 ;PB5 σαν έξοδος
SBI PORTD, 4 ;ενεργοποίηση pull-up για το PD4 (T0)
LDI R16, -200
OUT TCNT0, R16 ;φόρτωσε τον Timer0 με -200
LDI R20, 0x00
OUT TCCR0A, R20 ;Normal, T0 pin falling edge, no scale
LDI R20, 0x06
OUT TCCR0B, R20 ;Normal, T0 pin falling edge, no scale
LDI R20, (1<<TOIE0)
STS TIMSK0, R20 ;ενεργοποίηση διακοπής υπερχείλισης του Timer0
LDI R19, HIGH(-62500) ;τιμή timer για 1 δευτερόλεπτο
STS TCNT1H, R19 ;φόρτωσε στον Timer1 με το υψηλότερο byte
LDI R19, LOW(-62500)
STS TCNT1L, R19 ;φόρτωσε στον Timer1 με το χαμηλότερο byte
LDI R20, 0
STS TCCR1A, R20 ;Timer1 Normal mode
LDI R20, 0x04
STS TCCR1B, R20 ;int clk, rescale 1:256
LDI R20, (1<<TOIE1)
STS TIMSK1, R20 ;ενεργοποίηση διακοπής υπερχείλισης του Timer1
SEI ;θέσε Ι=1 καθολική ενεργοποίηση διακοπών
SBI DDRB, 1 ;ΡΒ1 σαν έξοδος
;------ ατέρμον βρόγχος
HERE: SBI PORTB, 1
CBI PORTB, 1
JMP HERE
;------ISR για τον Timer0 για την εναλλαγή μετά από 200 κύκλους ρολογιού
.ORG 0x200
T0_OV_ISR:
IN R16, PORTB ;διάβασε την PORTB
LDI R17, 1<<5 ;R17=00100000 για εναλλαγή του ΡΒ5
EOR R16, R17
OUT PORTB, R16 ;εναλλαγή ΡΒ4
LDI R16, -200
OUT TCNT0, R16 ;φόρτωσε τον TIMER0 με -200
RETI ;επιστροφή από τη διακοπή
;----- ISR for Timer1
.ORG 0x300
T1_OV_ISR:
INC R18 ;αύξηση σε κάθε υπερχείλιση
OUT PORTC, R18 ;προβολή στη PORTC
LDI R19, HIGH(-62500)
STS TCNT1H, R19 ;φόρτωσε τον Timer1 με το υψηλότερο byte
LDI R19, LOW(-62500)
STS TCNT1L, R19 ;φόρτωσε τον Timer1 με το χαμηλότερο byte
RETI ;επιστροφή από τη διακοπή