AVR-Tutorial: Timer

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Timer sind eines der Hauptarbeitspferde in unserem Mikrocontroller. Mit ihrer Hilfe ist es möglich, in regelmäßigen Zeitabständen Aktionen zu veranlassen. Aber Timer können noch mehr!

  • Timer können über einen Pin extern hochgezählt werden.
  • Es gibt Möglichkeiten, bei bestimmten Zählerständen einen Interrupt auslösen zu lassen.
  • Timer können aber auch völlig selbstständig Signale an einem Ausgabepin erzeugen.

Aber der Reihe nach und kurz zur Erinnerung: Die Beispiele passen zu einem ATmega8 und ggf. einer Reihe anderer AVR, können bei einigen Typen aber abweichen.

Was ist ein Timer?

Ein Timer ist im Grunde nichts anderes als ein bestimmtes Register im Mikrocontroller, das hardwaregesteuert fortlaufend um 1 erhöht (oder verringert) wird (statt „um 1 erhöhen“ sagt man auch inkrementieren, und das Gegenstück, dekrementieren, bedeutet „um 1 verringern“). Anstatt also Befehle im Programm vorzusehen, die regelmäßig ausgeführt werden und ein Register inkrementieren, erledigt dies der Mikrocontroller ganz von alleine. Dazu ist es möglich, den Timer mit dem Systemtakt zu verbinden und so die Genauigkeit des Quarzes auszunutzen, um ein Register regelmäßig und vor allen Dingen unabhängig vom restlichen Programmfluss (!) hochzählen zu lassen.

Davon alleine hätte man aber noch keinen großen Gewinn. Nützlich wird das Ganze erst dann, wenn man bei bestimmten Zählerständen eine Aktion ausführen lassen kann. Einer der „bestimmten Zählerstände“ ist zum Beispiel der Overflow. Das Zählregister eines Timers kann natürlich nicht beliebig lange inkrementiert werden – z. B. ist der höchste Zählerstand, den ein 8-Bit-Timer erreichen kann, 28 − 1 = 255. Beim nächsten Inkrementierschritt tritt ein Überlauf (engl. overflow) auf, der den Timerstand wieder zu 0 werden lässt. Und hier liegt der springende Punkt. Wir können uns nämlich an diesen Overflow „anhängen“ und den Controller so konfigurieren, dass beim Auftreten des Timer-Overflows ein Interrupt ausgelöst wird.

Detaillierte Informationen findet man im Datenblatt des Controllers (PDF; 6,3 MB) sowie in der Application Note AVR130: Setup and Use of AVR Timers (PDF; 512 KB).

Der Vorteiler (Prescaler)

Wenn also der Quarzoszillator mit 4 MHz schwingt, dann würde auch der Timer 4 Millionen Mal in der Sekunde erhöht werden. Da der Timer jedes Mal von 0 bis 255 zählt, bevor ein Overflow auftritt, heißt das auch, dass in einer Sekunde 4.000.000 / 256 = 15.625 Overflows vorkommen. Ganz schön schnell! Nur: Oft ist das nicht sinnvoll. Um diese Raten zu verzögern, gibt es den Vorteiler, oder auf Englisch, Prescaler. Er kann z. B. auf die Werte 1, 8, 64, 256 oder 1024 eingestellt werden, je nach Timer (bitte Datenblatt konsultieren!). Seine Aufgabe ist es, den Systemtakt um den angegebenen Faktor zu teilen. Steht der Vorteiler also auf 1024, so wird nur bei jedem 1024-sten Impuls vom Systemtakt das Timerregister um 1 erhöht. Entsprechend weniger häufig kommen dann natürlich die Overflows. Der Systemtakt sei wieder 4.000.000 Hz. Dann wird der Timer in 1 Sekunde 4.000.000 / 1.024 = 3.906,25 Mal erhöht. Da der Timer wieder jedesmal bis 255 zählen muss bis ein Overflow auftritt, bedeutet dies, dass in 1 Sekunde 3906,25 / 256 = 15,25 Overflows auftreten.

 Systemtakt: 4 MHz
 
 Vorteiler    Overflows/Sekunde      Zeit zwischen
                                     zwei Overflows
 
     1            15625                0,000064 s =  64 µs
     8             1953,125            0,000512 s = 512 µs
    64              244,1406           0,004096 s ≈   4,1 ms
   256               61,0351           0,016384 s ≈  16,4 ms
  1024               15,2587           0,065536 s ≈  65,5 ms

Die Zeit (in Sekunden) zwischen zwei Overflows lässt sich sehr leicht berechnen: [math]\displaystyle{ t=\frac{2^\text{Bit des Timers} \cdot \text{Vorteiler}}{\text{Systemtakt in Hz}} }[/math]

Erste Tests

Ein Programm, das einen Timer-Overflow in Aktion zeigt, könnte z. B. so aussehen:

.include "m8def.inc"

.def temp = r16
.def leds = r17

.org 0x0000
        rjmp    main                  ; Reset Handler
.org OVF0addr
        rjmp    timer0_overflow       ; Timer Overflow Handler

main:
        ; Stackpointer initialisieren
        ldi     temp, HIGH(RAMEND)
        out     SPH, temp
        ldi     temp, LOW(RAMEND)     
        out     SPL, temp

        ldi     temp, 0xFF            ; Port B auf Ausgang
        out     DDRB, temp

        ldi     leds, 0xFF

        ldi     temp, (1<<CS00)       ; CS00 setzen: Teiler 1
        out     TCCR0, temp

        ldi     temp, (1<<TOIE0)      ; TOIE0: Interrupt bei Timer Overflow
        out     TIMSK, temp

        sei

loop:   rjmp    loop

timer0_overflow:                      ; Timer 0 Overflow Handler
        out     PORTB, leds
        com     leds
        reti

Das Programm beginnt mit der Interrupt-Vektoren-Tabelle. Dort ist an der Adresse OVF0Addr ein Sprung zur Marke timer0_overflow eingetragen. Wenn also ein Overflow-Interrupt vom Timer 0 auftritt, so wird dieser Interrupt durch den rjmp weitergeleitet an die Stelle timer0_overflow.

Das Hauptprogramm beginnt ganz normal mit der Belegung des Stackpointers. Danach wird der Port B auf Ausgang geschaltet, wir wollen hier wieder die LED anschließen.

Durch Beschreiben von TCCR0 mit dem Bitmuster 0b00000001, hier ausgedrückt durch (1<<CS00), wird der Vorteiler auf 1 gesetzt. Für die ersten Versuche empfiehlt es sich, das Programm mit dem AVR-Studio zunächst zu simulieren. Würden wir einen größeren Vorteiler benutzen, so müsste man ziemlich oft mittels F11 einen simulierten Schritt ausführen, um eine Änderung im Timerregister zu erreichen.

Die nächsten Anweisungen setzen im Register TIMSK das Bit TOIE0. Sinn der Sache ist es, dem Timer zu erlauben, bei Erreichen eines Overflows einen Interrupt auszulösen.

Zum Schluss noch die Interrupts generell mittels sei freigeben. Dieser Schritt ist obligatorisch. Im Mikrocontroller können viele Quellen einen Interrupt auslösen. Daraus folgt: Für jede mögliche Quelle muss festgelegt werden, ob sie einen Interrupt erzeugen darf oder nicht. Die Oberhoheit hat aber das globale Interrupt-Flag. Mit ihm können alle Interrupts, egal von welcher Quelle sie kommen, unterdrückt werden.

Damit ist die Initialisierung beendet und das Hauptprogramm kann sich schlafen legen. Die Schleife loop: rjmp loop macht genau dieses.

Tritt nun ein Overflow am Timer auf, so wird über den Umweg über die Interrupt-Vektor-Tabelle der Programmteil timer0_overflow angesprungen. Dieser gibt einfach nur den Inhalt des Registers leds am Port B aus. Danach wird das leds-Register mit einer com-Operation negiert, so dass aus allen 0-Bits eine 1 wird und umgekehrt. Die Overflow-Behandlung ist damit beendet und mittels reti wird der Interrupt-Handler wieder verlassen.

Simulation im AVR-Studio

Es lohnt sich, den ganzen Vorgang im AVR-Studio simulieren zu lassen. Dazu am besten in der linken „I/O View“-Ansicht die Einträge für PORTB und TIMER_COUNTER_0 öffnen. Wird mittels F11 durch das Programm gegangen, so sieht man, dass ab dem Moment, ab dem der Vorteiler auf 1 gesetzt wird, der Timer 0 im TCNT0-Register zu zählen anfängt. Mit jedem Druck auf F11 erhöht sich der Zählerstand. Irgendwann ist dann die Endlosschleife loop erreicht. Drücken Sie weiterhin F11 und beobachten Sie, wie TCNT0 immer höher zählt, bis der Overflow erreicht wird. In dem Moment, in dem der Overflow erreicht wird, wird der Interrupt ausgelöst (hierfür muss für das Debuggen im AVR-Studio „Mask interrupts while stepping“ deaktiviert (false) sein, zu finden in Tools → Options -> Tools -> Tool settings ). Mit dem nächsten F11 landen Sie in der Interrupt-Vektor-Tabelle und von dort geht es weiter zu timer_0_overflow. Weitere Tastendrücke von F11 erledigen dann die Ausgabe auf den Port B, das Invertieren des Registers r17 und der Interrupt ist damit behandelt. Nach dem reti macht der Mikrocontroller genau an der Stelle weiter, an der er vom Interrupt unterbrochen wurde. Und der Timer 0 hat in der Zwischenzeit weitergezählt! Nach exakt weiteren 256 Schritten, vom Auftreten des ersten Overflows an gerechnet, wird der nächste Overflow ausgelöst.

Wie schnell schaltet der Port?

Eine berechtigte Frage. Dazu müssen wir etwas rechnen. Keine Angst, es ist nicht schwer, und wer das Prinzip bisher verstanden hat, der sollte keine Schwierigkeiten haben, die Berechnung nachzuvollziehen.

Der Quarzoszillator schwingt mit 4 MHz. Das heißt, in 1 Sekunde werden 4.000.000 Taktzyklen generiert. Durch die Wahl des Vorteilers von 1 bedeutet das auch, dass der Timer 4.000.000 Mal in der Sekunde erhöht wird. Von einem Overflow zum nächsten muss der Timer 256 Zählvorgänge ausführen. Also werden in 1 Sekunde 4.000.000 / 256 = 15.625 Overflows generiert. Bei jedem Overflow schalten wir die LEDs jeweils in den anderen Zustand. D. h. die LEDs blinken mit einer Frequenz von 15.625 / 2 = 7812,5 Hz. Das ist zu viel, als dass wir es noch sehen könnten.

Was können wir also tun, um diese Blinkfrequenz zu verringern? Im Moment ist unsere einzige Einflussgröße der Vorteiler. Wie sieht die Rechnung aus, wenn wir einen Vorteiler von 1.024 wählen?

Wiederum: Der Systemtakt sei 4 MHz. Durch den Vorteiler von 1.024 werden daraus 4.000.000 / 1.024 = 3.906,25 Pulse pro Sekunde für den Timer. Der zählt wiederum 256 Zustände von einem Overflow zum nächsten. 3.906,25 / 256 = 15,2587. Und wiederum: Im Overflow werden die LEDs ja abwechselnd ein- und ausgeschaltet, also dividieren wir noch durch zwei: 15,2587 / 2 = 7,629. Also rund 8 Hz. Diese Frequenz müsste man schon mit bloßem Auge sehen. Die LEDs werden ziemlich schnell vor sich hin blinken.

Reicht diese Verzögerung noch immer nicht, dann haben wir zwei Möglichkeiten:

  • Entweder wir benutzen einen anderen Timer. Timer 1 beispielsweise ist ein 16-Bit-Timer. Der Timer zählt also nicht von 0 bis 255, sondern von 0 bis 65.535. Bei entsprechender Umarbeitung des Programms und einem Vorteiler von 1.024 bedeutet das, dass die LEDs einen Ein/Aus-Zyklus in 33 Sekunden absolvieren.
  • Oder wir schalten die LEDs nicht bei jedem Timer-Overflow um. Man könnte zum Beispiel in einem Register bis 7 zählen und nur dann, wenn dieses Register 7 erreicht hat, wird
    • das Register wieder auf 0 gesetzt und
    • die LEDs umgeschaltet.

Timer 0

Timer 0 ist ein 8-Bit-Timer.

  • Overflow-Interrupt

TCCR0

TCCR – Timer/Counter Control Register

TCCR0
CS02 CS01 CS00


CS02/CS00 – Clock Select

CS02 CS01 CS00 Bedeutung
0 0 0 keine (Der Timer ist angehalten)
0 0 1 Vorteiler: 1
0 1 0 Vorteiler: 8
0 1 1 Vorteiler: 64
1 0 0 Vorteiler: 256
1 0 1 Vorteiler: 1024
1 1 0 Vorteiler: Externer Takt vom Pin T0, fallende Flanke
1 1 1 Vorteiler: Externer Takt vom Pin T0, steigende Flanke

TIMSK

TIMSK – Timer/Counter Interrupt Mask Register

TIMSK
OCIE0 TOIE0


TOIE0 – Timer 0 Overflow Interrupt Enable

Ist dieses Bit gesetzt, so wird beim Auftreten eines Overflows am Timer ein Interrupt ausgelöst.

Anstatt der Schreibweise

    ldi     temp, 0b00000001      ; TOIE0: Interrupt bei Timer Overflow
    out     TIMSK, temp

ist es besser, die Schreibweise

    ldi     temp, 1 << TOIE0
    out     TIMSK, temp

zu wählen, da hier unmittelbar aus dem Ladekommando hervorgeht, welche Bedeutung das gesetzte Bit hat. Die vorher inkludierte m8def.inc definiert dazu alles Notwendige.

Timer 1

Timer 1 ist ein 16-Bit-Timer.

  • Overflow-Interrupt
  • Clear Timer on Compare Match
  • Input Capture
  • 2 Compare-Einheiten
  • diverse PWM-Modi

TCCR1B

TCCR1B
ICNC1 ICES1 WGM13 WGM12 CS12 CS11 CS10


CS12/CS10 – Clock Select

CS12 CS11 CS10 Bedeutung
0 0 0 keine (Der Timer ist angehalten)
0 0 1 Vorteiler: 1
0 1 0 Vorteiler: 8
0 1 1 Vorteiler: 64
1 0 0 Vorteiler: 256
1 0 1 Vorteiler: 1024
1 1 0 Vorteiler: Externer Takt vom Pin T1, fallende Flanke
1 1 1 Vorteiler: Externer Takt vom Pin T1, steigende Flanke

ICES1 – Input Capture Edge Select

ICES1 = 0 , falling edge ICES1 = 1 , rising edge

ICNC1 – Input Capture Noise Canceler

TCCR1A

TCCR1A
COM1A1 COM1A0 COM1B1 COM1B0 FOC1A FOC1B WGM11 WGM10


OCR1A

 


 


OCR1B

 


 


ICR1

 


 


TIMSK1

TIMSK
TICIE1 OCIE1A OCIE1B TOIE1


TICIE1 – Timer 1 Input Capture Interrupt Enable

OCIE1A – Timer 1 Output Compare A Match Interrupt Enable

OCIE1B – Timer 1 Output Compare B Match Interrupt Enable

TOIE1 – Timer 1 Overflow Interrupt Enable

Timer 2

Timer 2 ist ein 8-Bit-Timer.

  • Overflow Interrupt
  • Compare Match Interrupt
  • Clear Timer on Compare Match
  • Phasenkorrekte PWM

TCCR2

TCCR2
FOC2 WGM20 COM21 COM20 WGM21 CS22 CS21 CS20


WGM21 – Waveform Generator Mode

Ist diese Option аktiviert, dann wird das Zählregister des Timers (TCNT2) wieder auf 0 gesetzt, sobald das Output Compare Register (OCR2) und TCNT2 übereinstimmen. Zu beachten ist dabei, dass, wenn das Register kleiner als 255 gestellt ist, der Timer nicht mehr überschritten und der Interrupt für den Overflow (TIMER2_OVF_vect) nicht mehr ausgelöst wird. Stattdessen gibt es den Interrupt für Compare Match (TIMER2_COMP_vect). Die Frequenz für die Interruptauslösung lässt sich dann wie folgt berechnen: [math]\displaystyle{ \text{ISR-Frequenz} = \frac{\text{F_CPU}}{\text{Prescaler} \cdot \text{OCR2}} }[/math]

CS22/CS20 – Clock Select

CS22 CS21 CS20 Bedeutung
0 0 0 keine (Der Timer ist angehalten)
0 0 1 Vorteiler: 1
0 1 0 Vorteiler: 8
0 1 1 Vorteiler: 32
1 0 0 Vorteiler: 64
1 0 1 Vorteiler: 128
1 1 0 Vorteiler: 256
1 1 1 Vorteiler: 1024

OCR2

 


TIMSK2

TIMSK
OCIE2 TOIE2


OCIE2 – Timer 2 Output Compare Interrupt Enable

TOIE2 – Timer 2 Overflow Interrupt Enable

Was geht noch mit einem Timer?

Timer sind sehr universelle Mikrocontroller-Bestandteile. Für weitergehende Studien ist es daher unerlässlich, das entsprechende Datenblatt des Mikrocontrollers zu studieren. Oft ist es z. B. möglich, dass der Timer bei Erreichen von bestimmten Zählerständen einen Ausgabe-Pin von sich aus ein-/aus-/umschaltet. Er erledigt dann das, was wir oben noch mit einem Interrupt gemacht haben, eigenständig komplett in Hardware. Bei einigen Timern ist es möglich, damit eine PWM (Pulsweiten-Modulation) aufzubauen.

Ein paar der Timermodule lassen sich auch als Zähler (Counter) verwenden. Damit kann man z. B. die Anzahl externer Ereignisse, wie Schaltvorgänge eines Inkrementalgebers, bestimmen.

Andere bieten die Möglichkeit, über einen externen Uhrenquarz getaktet zu werden (Anwendung z. B. als „Echtzeituhr“ oder als „Weckfunktion“ aus einem Standby-/Powerdownmodus).

Durch geschickte Umprogrammierung in Echtzeit lässt sich mit einem Timer eine PLL aufbauen, die sich fortwährend auf einen Eingangstakt synchronisiert. Damit wird vermieden, dass der Controller ein Signal ständig abfragen („pollen“) muss, sondern das Signal wird per Timer-Interrupt verarbeitet. Maßgeblich ist die Messung der aktuellen und Schätzung der kommenden Flanke mithilfe eines einstellbaren Taktteiler-Verhältnisses.

Literatur

Weblinks