12. Προγραμματίζοντας με τις διακοπές των timers

Στην προηγούμενη ενότητα μάθαμε πώς να χρησιμοποιούμε τους 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                    ;επιστροφή από τη διακοπή