8051 Timer 0/1

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

Timer 0/1 bei 8051-Controllern

Einleitung

Die Timer 0 und 1 sind bei jedem 8051-Kompatiblen vorhanden, bei allen gleich, und sie unterscheiden sich nur gering (Mode 3). Der Timer 2 ist erst ab 8052 (aber auch neuere Derivate wie z.B. AT89C2015 und AT89C4015 haben teilweise keinen Timer 2) enthalten und kann abweichende Register und Funktionen haben. Die Timer 0/1 sind Aufwärtszähler. Bei Zählerüberlauf wird ein Flag gesetzt (TF0 bzw. TF1), das aber nicht unbedigt ausgewertet werden muss (z.B. bei Nutzung als Baudratengenerator). Die Timer laufen nach Überlauf einfach weiter. Der Zählerstand kann jederzeit gelesen und auch geändert werden. Beim 16-Bit Mode muss der Zählerstand aus 2 Bytes TH0:TL0 gebildet werden. Im 8-Bit Mode erfolgt ein automatischer Reload aus dem TH-Register in das TL-Register. Die Quelle der Timer ist umschaltbar zwischen Oszillator/12 (Maschinentakte) und externem Eingang. Dies geschieht mit dem Bit C/T im TMOD-Register. C/T auf 0 bedeutet Timer-Betrieb (bekannte Frequenz wird gezählt -> Zeit), C/T auf 1 bedeutet Counter-Betrieb (externe Ereignisse werden gezählt -> z.B. Abfüllen von Werkstücken, Besucherzähler etc.) Die Timer müssen gestartet werden. Dazu muss das TR0 (bzw. TR1) Bit gesetzt werden. Sie können jederzeit angehalten und wieder gestartet werden. Der Zählerstand bleibt gespeichert. Mit gesetztem Gate-Bit läuft der Timer nur dann, wenn zusätzlich der Gate-Eingang auf 1 liegt. Damit können z.B.: Impulslängen gemessen werden. Praktischerweise ist dieser Eingang auch gleichzeitig ein Interrupt-Eingang, so dass man die fallende Flanke (Puls zu Ende, Timer steht, Ergebnis kann gelesen werden) per Interrupt auswerten kann.

Betriebsarten

Das Einstellen der Betriebsart erfolgt in einem Register (TMOD) für beide Timer! Die unteren 4 Bit gelten für Timer 0, die oberen für Timer 1. Die Bedeutung ist für beide gleich (ausser Mode 3).
Das TMOD-Register ist nicht Bitadressierbar und sollte daher mit UND/ODER-Maskierung verändert werden. 'Harte' Schreibzugriffe konfigurieren beide Timer gleichzeitig.

TMOD-Register
Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit0
G C/T M1 M0 G C/T M1 M0
Timer Betriebsarten
M1 M0 Betriebsart
0 0 Mode 0: 13-Bit Timer/Counter (!!)
0 1 Mode 1: 16-Bit Timer/Counter
1 0 Mode 2: 8-Bit Timer/Counter mit Auto-Reload
1 1 Mode 3: 2x 8-Bit Timer/Counter (nur Timer0, wenn Timer1 Baudrate erzeugt)

Erklärung:
Mode 0 existiert noch aus Kompatibilität zum 8048 und gehört daher ins Museum. Trotzdem schaltet man den Mode leicht unbeabsichtigt ein, wenn man TMOD falsch konfiguriert.
Mode 1 konfiguriert einen 16-Bit Timer/Counter, der Zählerstand ist auf 2 Register aufgeteilt.
Mode 2 konfiguriert einen 8-Bit Timer/Counter mit Auto-Reload, gezählt wird im Register TL0 bzw. TL1, der Reload erfolgt bei Überlauf aus TH0 bzw. TH1.
Mode 3: hier wird der Timer 0 in 2 8-Bit Timer/Counter aufgeteilt. Überläufe von TL0 sezten TF0, Überläufe von TH0 setzen TF1 (nur für Timer 0, Timer 1 macht dann z.B. Baudrate. Nur bei CPUs ohne Timer 2 oder Baudratengenerator interessant -> Museum!!)
Es bleiben also effektiv nur die beiden Betriebsarten 1 und 2 übrig.

Timer oder Counter

Mit den Bits C/T wird zwischen Timer-Betrieb und Counter-Betrieb umgeschaltet. Als Timer (C/T = 0) zählt die Baugruppe Maschinenzyklen, daher entspricht der Zählerstand einer Zeit. Als Counter (C/T = 1) wird ein Signal von einem externen Zähleingang (P3.4 für Timer 0 und P3.5 für Timer 1) gezählt.

Gate-Betrieb

Wenn das Bit 0 ist, läuft der entsprechende Timer, wenn das zugehörige Timer-Run-Flag gesetzt ist (TR0 bzw. TR1). Wenn das Bit 1 ist muss zusätzlich noch der Gate-Eingang auf 1 sein. Dies ist P3.2 für Timer 0 bzw. P3.3 für Timer 1. Damit kann man einfach Pulsdauern ausmessen oder auch Betriebsstunden (mit Hilfszählern für die Überläufe).

Timer-Kontroll-Register

Die Überlauf-Flags und die Run-Bits der beiden Timer befinden sich im Register TCON. Dort sind zusätzlich noch je 2 Bits für die Konfiguration der beiden externen Interrupts untergebracht (der reinste Gemischtwarenladen...)
Dies ist nicht tragisch, weil das Register Bitadressierbar ist. Um einen Timer zu starten schreibt man also einfach:

TR0 = 1;   // Timer 0 starten
TR1 = 1;   // Timer 1 starten
bzw. in Assembler
setb TR0   ; Timer 0 starten
setb TR1   ; Timer 1 starten

Beim Überlauf (im 8-Bit Mode nach 2^8 = 256 Maschinenzyklen, im 16-Bit Mode nach 2^16 = 65536 Maschinenzyklen) wird das Überlauf-Flag TF0 bzw. TF1 gesetzt. Damit erkennt man, dass der Timer überglaufen ist. Bei der Bearbeitung muss man das Flag zur Bestätigung löschen, im Interrupt-Betrieb werden die Flags automatisch gelöscht.

Schnelleinstieg für Ungeduldige

Von den Timern beim 8051 nimmst Du am Anfang nur Timer0/1 im 16-Bit Mode. => TMOD = 0x11; setzt beide Timer in den Mode 1. Nur wenn du serielle Schnittstelle verwendest, brauchst du evtl. den Timer 1 für die Baudrate. Wenn Du keinen Startwert und keinen Reload-Wert setzt, dann läuft der Timer 65536 Maschinenzyklen (also bei 12MHz ohne X2-Mode 65,5ms). Bei jedem Timerüberlauf musst du das Timer-Flag wieder löschen (entfällt im Interrupt-Betrieb, aber zunächst erstmal ohne) und dann kann man etwas tun.

Grundgerüst

void main (void)
{
   // Initialisierung
   TMOD = 0x11;       // setzt beide Timer in den 16-Bit Mode
                      // für andere Modi musst du doch noch oben lesen
   TR0 = 1;           // Timer 0 läuft jetzt, mit jedem Maschinenzyklus
                      // wird jetzt TH0:TL0 um 1 hochgezählt

   while(1)
   {
   // in der Endlosschleife
      if (TF0 == 1)   // Flag wird beim Überlauf auf 1 gesetzt
      {   
         TF0 = 0;     // Flag erstmal löschen, der Timer zählt bereits munter weiter
         Tu_Was( );   // für was wolltest Du die Zeit nochmal nutzen??
      }
      Weitere_Aktionen( );
   }
}

Für den Timer 1 änderst/ergänzt Du TR0 -> TR1, TF0 -> TF1.
Was soll jetzt mit dem Timer gesteuert werden? Lassen wir zunächst mal die LED an P2.0 blinken. Statt Tu_Was(); kommt jetzt also P2_0 = ~P2_0; hin. Wie groß ist jetzt die Blinkfrequenz? Der Timer läuft im 16-Bit Mode. Also ist die Zeit zwischen 2 Überlaufen 2^16 Maschinenzyklen. Der dauert bei den meisten 8051-Controllern 12 Oszillatortakte. Mit einem 12MHz Oszillator kommt man also auf genau 1µs pro Maschinenzyklus und damit auf 65536µs zwischen 2 Überläufen.
Doch zurück zur LED. Die ist jetzt also für 65,5ms an und danach für 65,5ms aus. Die Periodendauer beträgt hier also die doppelte Timerzeit, die Frequenz ergibt sich also zu
[math]\displaystyle{ f = \frac 1 T = \frac 1 {2 \cdot Timerzeit} }[/math]
Hier also ein symmetrisches Rechtecksignal mit einer Periodendauer von 131,072ms und einer Frequenz von 7,63Hz.

Variable Timerzeit

Auf Dauer ist das Geblinke aber langweilig... Also müssen die Zeiten verändert werden.
Länger geht beim 8051 nur mit einem Hilfszähler oder mit Veränderung der Quarzfrequenz (der ATMega und andere Controller haben hier ja einen Vorteiler). Für kürzere Zeiten kann nur der Startwert verändert werden, weil der Überlauf immer nach dem Zählerstand 65535=0xFFFF stattfindet. Wenn man also bei 15536 beginnt zu zählen sind es nur noch 50000 Maschinenzyklen bis zum Überlauf. Dieser Startwert muss nun auf 2 8-Bit Zählregister THx und TLx aufgeteilt werden. Dafür gibt es verschiedene Möglichkeiten von der Berechnung im Kopf, Taschenrechner, Tabellenkalkulation etc...
Der (meiner Meinung nach) eleganteste Weg ist aber, den Compiler die Berechnung erledigen zu lassen (wozu hat man denn einen PC). Es muss also die Differenz zwischen Maximalwert 65536 und gewünschter Dauer (hier 50000) gebildet und aufgeteilt werden. Dazu schreibt man einfach:

// Beispiel für Timer 0, für Timer 1 TH1 und TL1 einsetzen
TH0 = (65536 - 50000)/256;  // gibt das High-Byte von 15536 => 0x3C
TL0 = (65536 - 50000)%256;  // gibt das Low-Byte von 15536 => 0xB0
oder in Assembler:
mov TH0, #HIGH(65536-50000) ; ausprobieren, ob der eigene Assembler
mov TL0, #LOW(65536-50000)  ; dies unterstützt!

Durch diese Schreibweise sieht man sofort die erzeugte Timerzeit und die Umrechnung erfolgt durch den Compiler (es wird also auf dem Controller keine Berechnung mehr durchgeführt). Etwas anders ist es, wenn keine Konstanten sondern Variablen dastehen. Dann sollte man etwas anders schreiben:

unsigned short Timerzeit, Reloadwert;
Timerzeit = irgendeine_Berechnung( );
Reloadwert = 65536 - Timerzeit;
TH0 = Reloadwert / 256;
TL0 = Reloadwert % 256;

Auf den ersten Blick etwas umständlich, aber so erkennt der Compiler, dass nur eine Aufteilung in Low- und High-Byte gewünscht ist und weist einfach die Register zu statt Division und Modulo zu berechnen. Bei der Programmierung in Assembler muss man hier 16-Bit Arithmetik berechnen, für Anfänger hört hier der Spass auf (=> erstmal bei konstanten Zeiten bleiben).

Längere Wartezeiten erzeugen

(zum größten Teil aus meinen Beiträgen hier übernommen: http://www.mikrocontroller.net/topic/264647#2752818 )

Dazu zählst du eine Variable hoch. Nimm mal

unsigned int Ticks;

Bei jedem Überlauf wird Ticks erhöht und wenn die benötigte Anzahl (=Zeit in s / 65,5ms) erreicht ist, machst Du was und setzt Ticks wieder auf 0.

void main (void)
{
  // Timer-Initialisierung
  TMOD = 0x11;
  //TH0 = (65536 - Benoetigte_Zeit)/256;  // für Startwert oder Reload 
  //TL0 = (65536 - Benoetigte_Zeit)%256;  // für Startwert oder Reload 
  TR0 = 1;    // Timer 0 starten  - TR1 für Timer 1

  while(1)  // Endlosschleife
  {
     if (TF0 == 1) // Timer 0 ist überglaufen
     {  Ticks++;   // sonst braucht man erstmal nix.
        TF0 = 0;   // Flag löschen
     }
     if (Ticks == 0)
     {  Schalte_Ein( ); //einschalten -> wird mehrfach ausgeführt 
                        // oder du fügst ein Merker-Flag ein
     }
     if (Ticks == Ausschaltzeit)
     {  Schalte_Aus( ); // ausschalten -> wird nur einmal ausgeführt 
        Ticks = 0;      // wenn Ticks gelöscht wird
     }
  }
}

Damit kannst Du zunächst einen Vorgang realisieren. Wenn Du die Ticks einfach weiterlaufen lässt, dann kann man auch sowas machen:

if (Taster1 == 1) 
{  Ausschaltzeit1 = Ticks + Dauer1; //zur aktuellen Zeit die Dauer addieren
   Einschalten(1);
}
if (Ticks == Ausschaltzeit1)
{  Ausschalten(1);
}
if (Taster2 == 1) 
{  Ausschaltzeit2 = Ticks + Dauer2; //zur aktuellen Zeit die Dauer addieren
   Einschalten(2);
}
if (Ticks == Ausschaltzeit2)
{  Ausschalten(2);
}

Das funktioniert mit (fast) beliebig vielen Signalen und es ist auch kein Problem, wenn es einen Überlauf gibt. Wenn Ticks z.B. auf 65000 steht und du willst eine Dauer von 60 Sekunden (= 60000ms/65,536ms = 915 Ticks), dann ergibt die Addition für die Ausschaltzeit 379 (gleicher Datentyp für alle Schaltzeiten!).

kurze Zeiten erzeugen

Die Timer zählen nach dem Überlauf sofort ab 0x0000 weiter. Also muss man wieder einen Startwert (Reload) laden, so dass der Timer nach einer kürzeren Zeit überläuft. Die Formeln im Quelltext kann man problemlos mit Konstanten füllen, die Division und Modulo wird dabei nicht wirklich berechnet, sondern der Compiler fügt die Ergebniswerte in den Code ein. (anders sieht es für variable Zeiten aus, dazu später mehr) Für einen Grundtakt von 1ms schreibt man also einfach nach jedem Überlauf wieder in die Zählregister:

TH0 = (65536 - 1000)/256;  // 1ms = 1000MZ, davon das High-Byte in TH0
TL0 = (65536 - 1000)%256;  // das Low-Byte in TL0

Da der 8051 bei den Timern 0 und 1 keine getrennten Zähl- und Reload-Register hat, muss man diesen Wert nach jedem Überlauf möglichst schnell wieder in die Register schreiben (daher nimmt man da dann meist Interrupts).
Programmausschnitt:

while(1)  // Endlosschleife
{
     if (TF0 == 1) // Timer 0 ist überglaufen
     {  TH0 = (65536 - 1000)/256;  // 1ms = 1000MZ, Hih-Byte
        TL0 = (65536 - 1000)%256;  // Low-Byte in TL0
        TF0 = 0;   // Flag löschen
        Ticks++;   // sonst braucht man erstmal nix.
     }
     // Rest des Programms
}

Bei einer Millisekunde läuft die Variable Ticks bereits nach 65536ms, also etwa einer Minute über. Wenn man längere Zeiten benötigt, dann entweder die Ticks alle 10, 20 oder 50ms einstellen oder (in C ja kein echtes Problem) unsigned long Ticks; verwenden. Damit kannst Du in ms-Auflösung Zeiten bis 1193 Stunden erzeugen, bevor der Zähler überläuft... Braucht aber natürlich mehr Speicher (auch für die Schaltzeiten!). Ohne Interrupts kann es aber passieren, dass die Bearbeitung des restlichen Programms länger als die Timer-Zeit dauert. Dann werden die Zeiten sehr ungenau. Also erstmal nicht wundern...

Reload mit variablen Zeiten

Für variable Zeiten (z.B. PWM für Modellbau-Servo mit 1-2ms Impuls und 20ms Periodendauer) darf man die (16Bit) Variablen nicht einfach in die obige Zeile einsetzen. Dabei wird sonst die Division und die Modulo-Operation wirklich ausgeführt (die sich ergebenden Zeiten sind dann für die Tonne...). Hier muss man (am besten eine Phase vorher, wenn der Timer gerade übergelaufen war) die Variable berechnen.

while(1)  // Endlosschleife
{
     if (TF0 == 1) // Timer 0 ist überglaufen
     {  TH0 = (65536 - Pulszeit)/256;  // Hih-Byte
        TL0 = (65536 - Pulszeit)%256;  // Low-Byte in TL0
        TF0 = 0;   // Flag löschen
        Ticks++;   // sonst braucht man erstmal nix.
        Pulszeit = 1000 + 4 * Dip_Schalter; // ergibt 1-2ms
     }
     // Rest des Programms
}

Timer-Interrupt ohne Schrecken

Die Ungenauigkeit wird mit dem Interrupt beim Timer-Reload vermieden. Sobald der Timer bei aktiviertem Interrupt überläuft, springt der Prozessor in die Interrupt-Service-Routine und führt den dortigen Code aus. Danach geht es an der alten Stelle im Hauptprogramm weiter. (der sollte daher möglichst kurz sein) Interrupts aktiviert man erstmal einzeln für jede Quelle, danach noch den Hauptschalter EA.
Zum Thema Interrupts gibt es ein extra Kapitel.

TMOD = 0x11;
TH0 = (65536 - 1000)/256;  // für Startwert 
TL0 = (65536 - 1000)%256;  // für Startwert 
TR0 = 1;    // Timer 0 starten  - TR1 für Timer 1
ET0 = 1;    // Interrupt für Timer 0 aktivieren
EA  = 1;    // Globalen Interrupt aktivieren -- ab jetzt geht's rund

Die Interrupt-Service-Routine ersetzt die if(TF0)-Abfrage aus dem Hauptprogramm. Das ist kein Hexenwerk, es kommt nur ein Schlüsselwort hinter den Funktionsnamen und eine Nummer, die die Quelle angibt. (gilt für den Compiler RC51 aus RIDE)

void ISR_Timer0 (void) interrupt 1
{
   TH0 = (65536 - 1000)/256;  // Reload
   TL0 = (65536 - 1000)%256;  // Reload
   // TF0 = 0;   // Flag löschen geht jetzt automatisch! Der reine Luxus!
   Ticks++;   // sonst braucht man erstmal nix.
}

Ticks muss jetzt als globale Variable deklariert werden, damit sowohl die ISR als auch main() darauf zugreifen können. Erbsenzähler rechnen natürlich noch aus, wieviele µs es bis zum Relaod dauert und korrigieren damit den Reload-Wert:

// wenn es z.B. 6us bis zum Sprung in die ISR dauert:
   TH0 = (65536 - 1000 + 6)/256;  // korrigiert um 6us
   TL0 = (65536 - 1000 + 6)%256;  // korrigiert

Es ist aber nicht immer möglich, exakt auf 1µs genau zu arbeiten (ein laufender ASM-Befehl kann nicht unterbrochen werden und dauert 1, 2 oder 4 MZ). Aber zuviel überlegungen nutzen eh nichts, weil der Quarztakt ja auch nicht exakt ist. (eine Uhr mit Timer-Interrupt läuft aber trotzdem mit einer Abweichung von wenigen Sekunden/Woche)

Timer im 8-Bit Modus

Todo

Counter-Betrieb

ToDo.

Kritische Betrachtung, Fallstricke

Jetzt werden viele "Aber..." rufen. Wenn man genau hinschaut, gibt es natürlich den Fall, dass das Low-Byte übergelaufen ist, bis man das High-Byte geschrieben hat. Ebenso können die Timer-Zeiten daneben liegen, wenn in der Hauptschleife andere zeitaufwändige Aktionen drinstehen. Daher kann man in kritischen Fällen den Timer von dem Reload stoppen und anschliessend wieder starten. In dieser einfachen Einführung habe ich auf solche Details aber für die Übersicht verzichtet...
Nachricht an den Autor B. Spitzer