AVR-GCC. Timers/Counters στην πράξη.

Τεκμηρίωση του ATmega8

Ας υποθέσουμε πως θέλουμε να αναβοσβήνουμε ένα LED που είναι συνδεδεμένο στο pin 15 (PB1/OC1A) του ATmega8 με ρυθμό μία φορά το δευτερόλεπτο (συχνότητα 1 Hz). Ο μικροελεγκτής λειτουργεί σε συχνότητα (FCPU) 1 MHz. Ο ATmega8 έχει τρεις μετρητές τους Timer/counter0 που είναι 8-bit τον Timer/counter1 που είναι 16-bit και τον Timer/counter2 που είναι 8-bit.

Ας δούμε τι τιμή θα έχει ο καταχωρητής TCNTn (n=1,2,3) για κάθε δευτερόλεπτο που περνά (ο χρόνος που θέλουμε να αναβοσβήνει το LED) για διάφορες τιμές του Prescaler.


Οι μέγιστες τιμές που μπορεί να πάρει ο κάθε μετρητής είναι:
TCNT0=256
TCNT1=65536
TCNT2=256

Όπως βλέπουμε από τον παραπάνω πίνακα μπορεί να χρησιμοποιηθεί μόνο ο TCNT1 με Prescaler 64 (15.624). Οι μη ακέραιες τιμές (3.905,25, 975,5625) αποφεύγονται.

#include <avr/io.h>

/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "F_CPU not defined defaulting to 1000000UL"
    #define F_CPU 1000000UL
#endif

int main(void){

    DDRB |= (1<<PB1);                // Το pin PB1 ως έξοδος.
    TCCR1B |= (1<<CS11) | (1<<CS10); // Εκκίνηση του Timer/Counter1 με Prescaler 64.
                                     // Σελίδα 99 της τεκμηρίωσης του ATmega8.


    while(1){

        /* Έλεγχος για το αν η τιμή του μετρητή έφτασε
           την επιθυμητή τιμή (15624 εδώ). Αν έφτασε
           άλλαξε την κατάσταση του LED και μηδένισε τον
           μετρητή. */

        if(TCNT1 >= 15624){
            PORTB ^= (1 << PB1); // Εναλλαγή του LED.

            TCNT1 = 0; // Επαναφορά της τιμής του μετρητή.
        }

    };

    return 0;
}

Όπως είδαμε ο Timer/Counter1 υποστηρίζει και την λειτουργία CTC. Οπότε μπορούμε να κάνουμε χρήση αυτού του χαρακτηριστικού για να βελτιώσουμε το πρόγραμμά μας. Μέχρι τώρα ελέγχαμε την τιμή του μετρητή και όταν αυτή είχε την επιθυμητή τιμή πραγματοποιούσαμε αυτό που θέλαμε και μηδενίζαμε τον μετρητή. Ο τρόπος αυτός δεν είναι αποδοτικός και σπαταλά αρκετούς κύκλους του ρολογιού. Στην λειτουργία CTC (Clear Timer on Compare) ο έλεγχος αυτός γίνεται από το υλικό και όχι στο πρόγραμμα.

#include <avr/io.h>

/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "F_CPU not defined defaulting to 1000000UL"
    #define F_CPU 1000000UL
#endif

int main(void)
{
    DDRB |= (1<<PB1);     // Το pin PB1 ως έξοδος.

    TCCR1B |= (1<<WGM12); // Timer/Counter1 σε CTC λειτουργία.
                          // Σελίδα 97 της τεκμηρίωσης του ATmega8.
                          

    OCR1A=15624; // Τιμή σύγκρισης σε CTC για 1 Hz.

    TCCR1B |= (1<<CS11) | (1<<CS10); // Εκκίνηση του Timer/Counter1 με Prescaler 64.
                                     // Σελίδα 99 της τεκμηρίωσης του ATmega8.
    while(1){

        /* Έλεγχος για το αν η σημαία OCF1A του CTC
           ενεργοποιήθηκε. Για να μηδενίσουμε την
           OCF1A πρέπει να γράψουμε λογικό ένα (1)
           σε αυτή. */

        if(TIFR & (1 << OCF1A)){
            PORTB ^= (1 << PB1); // Εναλλαγή του LED.

            TIFR = (1<<OCF1A); // Καθάρισμα (0) του OCF1A γράφοντας 1 σε αυτό.
        }

    };

    return 0;
}

Παρατηρήστε πως στον μηδενισμό του OCF1A στον καταχωρητή TIFR γίνεται με απευθείας ανάθεση (TIFR = (1<<OCF1A);) και όχι με την χρήση τελεστή OR (TIFR |= (1<<OCF1A);). Θα γραφτεί ένα λογικό ένα (1) στο OCF1A που το αναγκάζει να πάει σε λογικό μηδέν (0). Τα υπόλοιπα bit δεν θα επηρεαστούν αφού γράφοντάς τα σε λογικό μηδέν (0) δεν αλλάζει την κατάστασή τους. Ο κώδικας που παράγει ο μεταγλωττιστής θα είναι μικρότερος αφού δεν έχουμε την σειρά Ανάγνωση-Τροποποίηση-Εγγραφή.

Ας δούμε τον κώδικα σε assembly που πράγεται (.lss αρχείο).

Απευθείας ανάθεση (TIFR = (1<<OCF1A);):

TIFR = (1<<OCF1A); // Καθάρισμα (0) του OCF1A γράφοντας 1 σε αυτό.
5e: 98 bf out 0x38, r25 ; 56
60: f8 cf rjmp .-16 ; 0x52

Με την χρήση τελεστή OR (TIFR |= (1<<OCF1A);):

TIFR |= (1<<OCF1A); // Καθάρισμα (0) του OCF1A γράφοντας 1 σε αυτό.
5c: 88 b7 in r24, 0x38 ; 56
5e: 80 61 ori r24, 0x10 ; 16
60: 88 bf out 0x38, r24 ; 56

62: f6 cf rjmp .-20 ; 0x50

Μπορούμε να βελτιώσουμε ακόμη περισσότερο το πρόγραμμά μας κάνοντας χρήση των Interrupt και της συνάρτησης ISR.

#include <avr/io.h>
#include <avr/interrupt.h> // Αρχείο κεφαλίδας για τα Interrupts.

/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "F_CPU not defined defaulting to 1000000UL"
    #define F_CPU 1000000UL
#endif

int main(void)
{
    DDRB |= (1<<PB1);     // Το pin PB1 ως έξοδος.

    TCCR1B |= (1<<WGM12); // Timer/Counter1 σε CTC λειτουργία.
                          // Σελίδα 97 της τεκμηρίωσης του ATmega8.

    TIMSK |= (1<<OCIE1A); // Ενεργοποίηση του CTC Interrupt.
                          // Σελίδα 99 της τεκμηρίωσης του ATmega8.

    OCR1A=15624; // Τιμή σύγκρισης σε CTC για 1 Hz.

    sei(); // Ενεργοποίηση των καθολικών Interrupt.

    TCCR1B |= (1<<CS11) | (1<<CS10); // Εκκίνηση του Timer/Counter1 με Prescaler 64.

    while(1){

    };

    return 0;
}

/* Συνάρτηση ISR που εκτελείτε κάθε ένα δευτερόλεπτο. */

ISR(TIMER1_COMPA_vect){

    PORTB ^= (1 << PB1); // Εναλλαγή του LED.

}

Παρατηρήστε πως δεν χρειάζεται να καθαρίζουμε την σημαία του CTC. Αυτό γίνεται αυτόματα από το υλικό μόλις ενεργοποιηθεί η συνάρτηση ISR. Όλη η λειτουργία του μετρητή πλέον από την στιγμή που θα ενεργοποιηθεί ρυθμίζεται αποκλειστικά από το υλικό και όχι από το πρόγραμμα μας. Ο κύριος βρόχος του προγράμματος είναι τώρα άδειος και περιμένει τις εντολές μας.

Μπορούμε να βελτιώσουμε περαιτέρω το παραπάνω πρόγραμμα εναλλαγής του LED; Ας κοιτάξουμε λίγο το pin PB1 του ATmega8. Θα δούμε πως και αυτό όπως άλλα pins έχει διπλή λειτουργία. Βλέπουμε λοιπόν και μία λειτουργία OC1A. Ρίχνοντας και μία ματιά στον Timer/counter1 θα δούμε στον καταχωρητή TCCR1A τα bit COM1A1 και COM1A0. Τα bit αυτά ελέγχουν την κατάσταση του pin OC1A (PB1) σε περίπτωση ταύτισης στην CTC λειτουργία. Από τον σχετικό πίνακα για τα COM1A1, COM1A0 βλέπουμε πως όταν το COM1A0 είναι σε λογικό ένα (1) τότε έχουμε εναλλαγή στο pin OC1A (PB1). Ακριβώς αυτό που θέλουμε δηλαδή.

#include <avr/io.h>

/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "F_CPU not defined defaulting to 1000000UL"
    #define F_CPU 1000000UL
#endif

int main(void)
{
    DDRB |= (1<<PB1); // Το pin PB1 ως έξοδος.

    TCCR1B |= (1<<WGM12);  // Timer/Counter1 σε CTC λειτουργία.

    TCCR1A |= (1<<COM1A0); // Εναλλαγή του OC1A (PB1) κατά την ταύτιση CTC


    OCR1A=15624; // Τιμή σύγκρισης σε CTC για 1 Hz.


    TCCR1B |= (1<<CS11) | (1<<CS10); // Εκκίνηση του Timer/Counter1 με Prescaler 64.

    while(1){

    };

    return 0;
}

Η εναλλαγή του LED στο παραπάνω πρόγραμμα γίνεται εξολοκλήρου από το υλικό του μικροελεγκτή μας χωρίς καθόλου κώδικα πέρα από αυτόν για την αρχικοποίηση του μετρητή μας Timer/Counter1.

Ας δούμε ακόμη έναν τρόπο για να εναλλάσσουμε το LED μας με ρυθμό ενός δευτερολέπτου. Όπως έχουμε πει όταν ο μετρητής μας πάρει την μέγιστη τιμή τότε η επόμενη τιμή είναι η τιμή 0. Τότε έχουμε υπερχείλιση (overflow) του μετρητή. Σε περίπτωση υπερχείλισης υπάρχει και το αντίστοιχο interrupt.

Σε διάστημα ενός δευτερολέπτου με συχνότητα λειτουργίας 1 MHz και Prescaler 64 ο μετρητής έχει όπως είδαμε πάρει την τιμή 15.624. Η μέγιστη τιμή του μετρητή μας των 16-bit είναι 2^16-1=65.536-1=65535. Η τιμή που απομένει για να γεμίσει ο μετρητής μας και να προκύψει το interrupt υπερχείλισης είναι 65.535-15.624=49.911. Πρέπει λοιπόν να προφορτώσουμε τον μετρητή μας με την τιμή 49.911 ώστε να προκύψει το interrup υπερχείλισης μετά από ένα δευτερόλεπτο ή 15.624 “ticks” του μετρητή.

#include <avr/io.h>
#include <avr/interrupt.h> // Αρχείο κεφαλίδας για τα Interrupts.

/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "F_CPU not defined defaulting to 1000000UL"
    #define F_CPU 1000000UL
#endif

int main (void)
{
    DDRB |= (1 << PB1);    // Το pin PB1 ως έξοδος.

    TIMSK |= (1 << TOIE1); // Ενεργοποίηση του Interrupt υπερχείλησης για τον
                           // Timer/Counter1. Σελίδα 100 της τεκμηρίωσης
                           // του Atmega8.

    sei(); // Ενεργοποίηση των καθολικών Interrupt.

    TCNT1 = 49911; // Προφόρτωση του μετρητή με την υπολογισμενη τιμή.

    TCCR1B |= ((1 << CS10) | (1 << CS11)); // Εκκίνηση του Timer/Counter1 με 
                                           // Prescaler 64.

   while (1){

   };
}

/* Συνάρτηση ISR που εκτελείτε κάθε ένα δευτερόλεπτο. */

ISR(TIMER1_OVF_vect){

    PORTB ^= (1 << PB1); // Εναλλαγή του LED.
    TCNT1 = 49911; // Προφόρτωση του μετρητή με την υπολογισμένη τιμή.
}

Πολλές φορές σε ένα πρόγραμμα είναι πολύ χρήσιμο να γνωρίζουμε για πόση ώρα τρέχει το πρόγραμμα. Στους μικροελεγκτές συνηθίζουμε να μετράμε τον χρόνο αυτό σε χιλιοστά του δευτερολέπτου msec. Θα ήταν πολύ βολικό επομένως να έχουμε μία συνάρτηση που όποτε την καλούμε να μας επιστρέφει τον χρόνο που τρέχει το πρόγραμμά μας. Θέλουμε λοιπόν μία μεταβλητή που θα αυξάνεται κατά ένα κάθε χιλιοστό του δευτερολέπτου. Για να υλοποιήσουμε την συνάρτηση αυτή θα χρειαστούμε την βοήθεια ενός μετρητή.

Ας δούμε τι τιμή θα έχει ο καταχωρητής TCNTn (n=1,2,3) για κάθε χιλιοστό του δευτερόλεπτου που περνά (ο χρόνος που θέλουμε να αυξάνεται η μεταβλητή μας κατά ένα) για διάφορες τιμές του Prescaler.

Βλέπουμε πως μόνο οι τιμές 999 (στον Timer/Counter1 μόνο) και 124 μπορούν να χρησιμοποιηθούν. Θα επιλέξουμε την τιμή 124 με Prescaler 8 που μπορεί να χρησιμοποιηθεί σε όλους τους μετρητές του ATmega8.

Θα επιλέξουμε τον Timer/Counter0. O Timer/Counter0 όπως είδαμε παραπάνω δεν διαθέτει λειτουργία CTC οπότε θα χρησιμοποιήσουμε την μέθοδο της υπερχείλισης που είδαμε στο τελευταίο παράδειγμα εναλλαγής του LED.

Σε διάστημα ενός msec με συχνότητα λειτουργίας 1 MHz και Prescaler 8 ο μετρητής έχει όπως είδαμε πάρει την τιμή 124. Η μέγιστη τιμή του μετρητή μας των 8-bit είναι 2^8-1=256-1=255. Η τιμή που απομένει για να γεμίσει ο μετρητής μας και να προκύψει το interrupt υπερχείλισης είναι 255-124=131. Πρέπει λοιπόν να προφορτώσουμε τον μετρητή μας με την τιμή 131 ώστε να προκύψει το interrupt υπερχείλισης μετά από ένα msec ή 124 “ticks” του μετρητή.

Η μεταβλητή που θα κρατά τα χιλιοστά του δευτερολέπτου πρέπει να είναι όσο το δυνατόν περισσότερο χώρο. Η μεταβλητή αυτή είναι ένας μη προσημασμένος ακέραιος τον 32 bit (uint32_t). Η μέγιστη τιμή της μεταβλητής μας θα είναι 2^32-1=4.294.967.295. Κάθε μέρα (24 ώρες) έχει 1.000*3.600*24=86.400.000 mseconds. Δηλαδή η μεταβλητή μας θα γεμίσει σε περίπου 50 ημέρες. Αφού γεμίσει θα υπερχειλίσει και θα επιστρέψει στο 0.

#include <avr/io.h>
#include <avr/interrupt.h> // Αρχείο κεφαλίδας για τα Interrupts.

/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "F_CPU not defined defaulting to 1000000UL"
    #define F_CPU 1000000UL
#endif

uint32_t millis(void);

void millis_init(void);

volatile uint32_t msecs; /* Μεταβλητή που θα κρατά τα χιλιοστά
                            δευτερολέπτου Που τρέχει το πρόγραμμα.
                            Τύπου volatile εφόσον θα είναι μέσα
                            στην ISR. */

int main (void){

    uint32_t temp_time;  // Προσωρινή μεταβλητή για τον χρόνο.
    DDRB |= (1 << PB1);  // Το pin PB1 ως έξοδος.

    millis_init();       // Αρχικοποίηση του Timer/Counter0.

    temp_time = millis();

   while (1){
       // Κάθε 1000 msec ή 1 sec εναλλαγή του LED.
       if (millis()-temp_time >= 1000) {
          PORTB ^= (1<<PB1);
          temp_time = millis();
       };

   };
}

void millis_init(){

    TIMSK |= (1 << TOIE0); // Ενεργοποίηση του Interrupt υπερχείλισης
                           // για τον Timer/Counter1.
                           // Σελίδα 100 της τεκμηρίωσης του
                           // ATmega8.

    sei();                 // Ενεργοποίηση των καθολικών Interrupt.

    TCNT0 = 131;           // Προφόρτωση του μετρητή με την υπολογισμένη τιμή.

    TCCR0 |= (1 << CS01);  // Εκκίνηση του Timer/Counter0 με Prescaler 8.
                           // Σελίδα 72 της τεκμηρίωσης του Atmega8.
}

uint32_t millis(void) {

    uint32_t temp;          // Προσωρινή μεταβλητή.

    uint8_t oldSREG = SREG; // Αποθήκευση του Status Register.

    cli(); /* Απενεργοποίηση των καθολικών interrupt
              ώστε αν μην υπάρξει κατακερματισμός δεδομένων
              στην msecs. */

    temp=msecs;

    SREG=oldSREG; // Επαναφορά του SREG, ενεργοποίηση των
                  // Καθολικών interrupts.

    return temp;
}

/* Συνάρτηση ISR που εκτελείτε κάθε msec. */

ISR(TIMER0_OVF_vect){

    uint32_t temp; /* Προσωρινή μεταβλητή. Χρησιμοποιούμε προσωρινές
                     τοπικές μεταβλητές διότι μεταβλητές τύπου
                     volatile τοποθετούνται στην RAM. Οι τοπικές
                     μεταβλητές πάνε σε καταχωρητές στην CPU.
                     Έτσι έχουμε λιγότερες κλήσεις στην RAM. */
   temp = msecs;

   temp++;

   msecs = temp;

   TCNT0 += 131; // Προφόρτωση της υπολογισμένης τιμής στον
                 // μετρητή.
}

Μέχρι τώρα αναβοσβήναμε το LED μας απότομα. Ας δούμε τώρα πως μπορούμε να κάνουμε το LED μας να αναβοσβήνει σταδιακά, να ξεθωριάζει (fading). Αυτό μπορούμε να το καταφέρουμε με τον μετρητή μας σε PWM λειτουργία. Θα επιλέξουμε τον Timer/Counter1 σε Fast PWM των 10-bit.

Το LED θα ξεκινά από σβηστό σταδιακά θα ανεβαίνει η ένταση του και στην συνέχεια θα μειώνεται μέχρι να σβήσει πάλι.

#include <avr/io.h>
#include <util/delay.h>    // Αρχείο κεφαλίδας για το delay.
#include <avr/interrupt.h> // Αρχείο κεφαλίδας για τα Interrupts.

/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "F_CPU not defined defaulting to 1000000UL"
    #define F_CPU 1000000UL
#endif

int main(void){

    uint16_t i=0; // Βοηθητικές μεταβλητές i,j;
    int8_t j=1;

    DDRB |= (1 << PB1); // Το pin PB1 ως έξοδος.

    TCCR1A |= (1<<WGM10)|(1<<WGM11)|(1<<COM1A1); // Αρχικοποίηση του Timer/Counter1.


    TCCR1B |= (1<<WGM12);                        // Fast PWM στα 10-bit (TOP 1023).

    TCCR1B |= (1<<CS10) ;                        // Εκκίνηση του μετρητή.



    while(1){


        if (i==1023){
            j=-1;    // Όταν το i γίνει TOP το j=-1.
        }

        if (i==0){
            j=1;    // Όταν το i γίνει 0 το j=1.
        }

        i=i+j;      /* Όσο το i είναι μικρότερο από το TOP
                       και το j είναι 1 η τιμή του i και
                       κατά συνέπεια του OCR1A θα αυξάνεται.
                       Όταν το i φτάσει το TOP το j γίνεται
                       -1 και το i αρχίζει να μειώνεται. */

        OCR1A=i;

        _delay_ms(1); // καθυστέρηση της τάξης των 100 usec
                        // για να είναι ορατό το ξεθώριασμα.


    };

    return 0;
}

Πως θα ήταν η τάση στο LED αν την βλέπαμε σε έναν παλμογράφο; Παρακάτω βλέπουμε την PWM σε έναν παλμογράφο.

Όπως έχουμε δει οι μετρητές των AVR υποστηρίζουν αρκετές λειτουργίες PWM. Ποια λειτουργία πρέπει να επιλέξουμε για κάποια εργασία; Παρακάτω ακολουθούν μερικά παραδείγματα και τι PWM λειτουργία πρέπει να επιλέξουμε.

Έλεγχος της φωτεινότητας μία λάμπας/LED.
Οποιαδήποτε PWM λειτουργία σε συχνότητα μεγαλύτερη των 50 Hz είναι κατάλληλη.

Έλεγχος Servo.
Phase Correct λειτουργία.

Έλεγχος ταχύτητας κινητήρα.
Phase Correct, όσο ποιο υψηλή συχνότητα τόσο το καλύτερο.

Παραγωγή ήχων.
Phase & Frequency Correct, αλλαγή της συχνότητας για παραγωγή διαφορετικών τόνων.

DAC (Digital to Analog Converters).
Fast PWM, όσο ποιο υψηλή συχνότητα τόσο το καλύτερο.

Advertisements
This entry was posted in AVR, Electronics and tagged , , , , , . Bookmark the permalink.

2 Responses to AVR-GCC. Timers/Counters στην πράξη.

  1. Pingback: AVR-GCC. Σερβοκινητήρες (μοντελισμού). Aka servos. | My humble Blog.

  2. Pingback: Ένα απλό συχνόμετρο. | My humble Blog.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s