21. Januar 2017

Die Uhrzeit im Griff mit Timer und Unterbrechung

Die Anzeige ist aufgebaut, jetzt fehlt noch die Software mit dem richtigen Timing und die Uhr läuft. Damit das funktioniert gilt es nun die im Mikrocontroller enthaltenen Timer nutzbar zu machen.

Warum nicht wie im Testprogramm?

Ja, das Testprogramm hat die Anzeige gesteuert. Allerdings zeitlich nicht sehr präzise. Ein genauerer Blick auf das Blink-Programm macht den Haken jedoch deutlich.

void loop() {
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
}

Das Programm schaltet den Ausgang ein, wartet eine Sekunde, schaltet den Ausgang aus und wartet wieder. Anschließend beginnt der Zyklus von vorne. Die Verzögerungen mögen exakt eine Sekunde dauern, aber wie lange dauert das Schalten? Schon die Zahl der Kommandos kann durch bedingte Ausführungen, beispielsweise einen Übertrag auf die nächste Stelle unterschiedlich sein.  Folglich ist die Laufzeit unterschiedlich. Das Multiplex braucht nur ausreichend schnell sein, so dass die Anzeige nicht flimmert. Für eine genaue Uhr taugt das allerdings nicht.

Die Zeit im Griff durch Unterbrechung

Was meint das? Im Mikrocontroller des Arduino sind drei sogenannte Timer/Counter integriert. Im Grunde handelt es sich um Zähler. Sie zählen Ereignisse wie den Systemtakt und führen eine gegeben Funktion aus, wenn ein definierter Wert oder die Zählerobergrenze erreicht ist. Das Ausführen startet der Timer mit Hilfe einer Unterbrechung – einem Interrupt. Der Interrupt unterbricht das laufende Programm und ruft den Unterbrechungscode auf. Ein Beispiel dafür ist der Reset, der durch einen Taster auf dem Arduino-Board ausgelöst werden kann. Der Mikrocontroller beginnt dann mit der Ausführung seines Programmes von ganz vorne.

Die Timer zählen vollständig unabhängig vom laufenden Programm. Zählen sie nun den Systemtakt,  bilden sie eine gute Basis für eine Uhr. Der ist immerhin quarzgenau bei 16000000 Impulse in der Sekunde.

Was gibt es für Timer?

Das hängt vom zugrundeliegen Controller ab. Auf dem Arduino stehen die beiden 8-Bit-Timer/Counter 0 und 2 sowie der 16-Bit-Timer/Counter 1 zur Verfügung.

Die 8-Bit Zähler können bis 255 Zählen, Timer 1 schafft es immerhin bis 65535. Anschließend beginnen sie wieder bei 0. Wenn sie von vorn beginnen, erfährt der Zähler einen Überlauf. Den kann er durch einen Interrupt markieren. Genauso ist ein Interrupt konfigurierbar, sobald ein bestimmter Wert erreicht ist.

 

Timer 2 könnte auch externe Ereignisse zählen. An den dafür vorgesehenen Eingängen betreibt der Arduino allerdings seinen Quarz. Eine mögliche Taktquelle für die Timer ist wie gesagt der Systemtakt. Die 16 MHz sind recht schnell für den Zahlenumfang der Timer und die Zähler laufen in Bruchteilen einer Sekunde über. Für längere Distanzen kann der Takt vor dem Zähler vorgeeilt werden. Als Vorteile sind die 8/32/64/128/256 und 1024 möglich.

Die Programmierung

Die Timer besitzen diverse Kontrollregister. Die Register sind Adressen im Programmraum. Die Programmierung erfolgt durch setzen bestimmter Steuer-Bits dieser Register. Die Arduino IDE basiert letztlich auf dem C++ für die Atmel-Prozessoren. Die Header definieren Konstanten für die Adressen und die Steuerbits, mit denen die Programmierung einbänglicher ist.

Die benötigten Register sind im Einzelnen

  • OCRxy
    Output Compare Register. Hier können zwei Vergleichswerte hinterlegt werden, bei deren Erreichen ein TIMERx_COMPy_vect-Interrupt ausgelöst wird. x ist die Nummer des Timers (0,1, 2). y steht für die beiden möglichen Werte (A und B)
  • TCCRxy
    Timer/Counter Control Register: Einstellen des Vorteile
  • TIMSKx
    Timer/Counter Interrupt Mark Register: welche Interrupts soll ein Timer aus auslösen

Weiterhin sind noch die Funktionen cli() – Interrupts deaktivieren – und das Gegenstück sei() – Interrupt aktivieren wichtig. Während der Timerprogrammierung sollten die Interrupts deaktiviert sein, damit die Programmierung nicht unterbrochen wird und ein bisher erfolgte Programmierung ein ungewolltes Verhalten hinterlässt.

Der auszuführende Code für einen Interrupt ist in einer Callback-Funktion zu hinterlegen:

ISR(Interruptname) {
   // auszuführender Code
}

Damit die Interrupts sich nicht „selbst überholen“ sollte der Code natürlich möglichst kurz sein.

Eine Übungsprogramm

Um mich mit der Programmierung vertraut zu machen, schadet ein einfaches Beispielsprogramm nicht. Ich mag das Blink-Programm dafür mit Hilfe eines Timers umsetzen. Der dabei entstandene Code findet sich auch auf Github.

Natürlich ist bei diesem Programm ein abweichender Setup-Code erforderlich. Die eigentliche Aktivität erfolgt allerdings in der Interrupt-Behandlung (ISR(.)) infolge dessen ist die Loop-Prozedur leer. Der Arduino wartet. Ist die vorgegebene Zeit des Timers um, schaltet die Interrupt-Behandlung den Ausgang der Onboard-LED um.

Bevor ich kurz auf die Timereinstellungen eingehe, möchte ich noch kurz auf eine Besonderheit bei der Deklaration der Zustandsvariable für den Status des Ausgangs hinweisen:

volatile boolean blink = false;

Der Compiler versucht zu optimieren. Stellt er nun fest, dass eine Variable in einem Codeblock nicht geschrieben wird, könnte er sie infolgedessen durch eine Konstante ersetzen. Soll also eine Variable  in einem Block festgelegt und in einem anderen ausgelesen werden, müssen das dem Compiler mitteilen. Genau dafür ist die Variable blink als volatile deklariert.

Kommen wir nun zu den Einstellungen. Ich möchte die delay(.)-Anweisungen durch die Interrupt-behandlung ersetzen. Diese schaltet die Leuchtdiode ein und vice versa. Der Aufruf sollte entsprechend einmal in der Sekunde erfolgen.

Dafür wähle ich den 16-Bit-Timer. Er erhält den Systemtakt geteilt durch 1014. Damit ist die Eingangsfrequenz 16.000.000 / 1014 = 15.625 Hz. Zählt der Timer nun bis 15624 und startet mit dem nächsten Takt den Interrupt, ist folglich der Aufruf je Sekunde erreicht.

Die Details zu den Steuerregistern

Die Bits der Register sind ausführlich im Datenblatt von Atmel beschrieben. Damit der Timer bei erreichen des Zielwertes zurückgesetzt wird, ist in das TCCR1B Register die WGM-Bits entsprechend zu setzen WGM12 auf 1 die beiden anderen auf 0. Sie befinden sich im TCCR1A-Register.

Der Precaller wird über die CS-Bits gesteuert, die ebenfalls im TCCR1B-Register zu finden sind. Daraus ergeben sich die Zeilen

TCCR1A = 0;
TCCR1B = 0;
// Set CS12 and CS10 bits for 1:1024 prescaler
TCCR1B |= (1 << CS12) | (1 << CS10);
// turn on CTC mode
TCCR1B |= (1 << WGM12);

Das Vergleichsregister A ist mit den oben genannten 15624 zu füllen (es könnte genauso gut B Verwendung finden)

OCR1A = 15624;

Zuletzt muss der Vergleichseinheit A gesagt werden, den Interrupt bei erreichen zu aktivieren

TIMSK1 |= (1 << OCIE1A);

Vor der Programmierung des Timers sind Interrupts zu deaktivieren und anschließend wieder zu aktivieren.

Resumee

Das Blink-Programm auf Timer Basis zeigt einen sehr schönen Weg zur Steuerung der Digitaluhr auf. Die Zeitmessung damit ist quarzgenau. Die Programmierung kann durch diese “Event”-Behandlung auch etwas leichter werden.

Einen Zeitgeber für eine Sekunde ist nun schon gefunden. Der nächste Beitrag betreibt die Anzeige und Zählt dann die Sekunden.

Bisherige Beiträge zum Winterprojekt

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

I accept that my given data and my IP address is sent to a server in the USA only for the purpose of spam prevention through the Akismet program.More information on Akismet and GDPR.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.