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

 

Winterprojekt: Die Anzeige der Digitaluhr

Das neue Jahr hat begonnen und es kann mit der Anzeige weitergehen. Dieser Beitrag beschreibt insbesondere die Hardware, also den Aufbau und die Verdrahtung. Für den Test gibt es ein erstes kleines Programm.

Die eigentliche Anzeige

Eine digitale Uhr benötigt jeweils zwei Ziffern für die Stunden und die Minuten. Die Anzeige besteht demnach aus vier 7-Segment-Anzeigen. in einer ersten Hochrechnung sind folglich für die Ansteuerung 7 x 4 = 28 digitale Ausgänge erforderlich. So viele Ausgänge besitzt der Arduino allerdings nicht. Deshalb erfolgt die Ansteuerung multiplex.

Das Multiplex-Prinzip

Bei Bildschirmen oder Fernsehern gibt es immer eine Frequenzangabe, Die Angabe von beispielsweise 50 Hertz (Hz) bedeutet, dass fünfzig mal in der Sekunde das Bild erneuert wird. Unser Auge ist träge, deshalb sind schnelle, einzelne Bilder nicht mehr zu unterscheiden. Entsprechend verschmelzen sie zu einer laufenden Bewegung.

Mit den Ziffern der Uhr funktioniert es genauso. Eigentlich zeigen die 7-Segment-Anzeigen nacheinander ihre Ziffern an. Erfolgt der Wechsel allerdings schnell genug, so sehen unsere Augen vier leuchtende Ziffern.

Für die Ansteuerung benötigt dieses Verfahren sieben Ausgänge gleichzeitig für alle vier Segmente. Vier weitere Ausgänge steuern jeweils den gemeinsamen Gegenpol der Segmente und wählen die jeweils anzuzeigende Ziffer aus. Die Anzahl der benötigten Ausgänge reduziert sich immerhin von 28 auf 11 und die kann der Arduino ansteuern.

Unter Strom

Mit den Strom einer einzelne Leuchtdiode kann der Arduino umgehen. An den gemeinsamen Gegenpol geben beispielsweise für die Ziffer Acht immerhin sieben Dioden Strom ab. In Foren liest man häufiger der Arduino kann bis zu 50 mA liefern. Laut Datenblatt sind es allerdings nur 40 mA. Da die blauen Dioden sehr hell sind, könnten größere Vorwiderstände gewählt werden, beispielsweise 1 Kiloohm. Von 5 Volt fallen 2,8 Volt Spannung an der Diode ab und folglich verbleiben 2,2 Volt am Widerstand. Geteilt durch 1000 Ohm mal 7 Segmente = 15,4 mA, das funktioniert. Bei einem Experiment ergab sich allerdings das die Ausgänge meines Arduino unterschiedlich viel Strom liefern, so dass ein Segment deutlich dunkler war als die anderen. Nach einem Tausch war es wieder das Segment dieses Ausgangs, das dunkler war. Das ist unschön, deshalb habe ich den Strom mit Hilfe von Transistoren geschaltet.

Der Transistor als Schalter

In der Restekiste habe ich noch BC 547C Transistoren. Die Verstärkung (das “C”) ist für diese Anwendung eigentlich übertrieben, trotzdem erfüllen sie ihren Zweck. Sie können mit einem kleinen Strom einen großen schalten. Bei den von mir verwendeten 220 Ohm Widerständen ist das 7 x (2,2/220) = ca. 70 mA. Meine kleine Testschaltung sieht wie folgt aus:

Schalten mit dem Transistor

Auf diesem Video habe ich das Schalten durch den Arduino durch einen Taster simuliert. Diese Schaltung kann ich dementsprechend auf den gemeinsamen Pol aller 7-Segement-Anzeigen anwenden.

Der Schaltplan für die Anzeige insgesamt ist

Schaltung Anzeige

Testprogamme

Die Funktion ziffer(int) aus dem letzten Beitrag sowie das Feld mit den Ansteuerungscodes benutze ich genauso wie im letzten Beitrag. Die Initialisierungsschleife des Setup nimmt zudem vier weiteren Ausgänge 3 bis 6 auf, die ebenfalls als Ausgang eingestellt werden. Die Werte für die darzustellenden Ziffern enthält das Feld

int ausgabe[] = { 4, 3, 2, 1};

Im Loop rufe ich zunächst nur eine Prozedur  doAusgabe() auf, in der eine Schleife die nächste Ziffer schaltet.

for(int z=0; z<4; z++) {
  selectZiffer(z);
  ziffer(ausgabe[z]);
  delay(5);
}

Sie selektiert zunächst das zu schaltende Segment, steuert anschließend die Segmente entsprechend der zugehörigen Ziffer an und wartet. Die Prozedur selectZiffer(int) schaltet alle außer dem angeben Segment ein.

void selectZiffer(int z) {
  for(int i=0; i<4; i++) {
    if(i==z) digitalWrite(6-i, HIGH);
    else digitalWrite(6-i, LOW);
  }
}

Die Verzögerung in doAusgabe() definiert im Moment für die maximale Wiederholrate. Ist die Wartezeit zu lang, so flimmert die Anzeige oder extremer, der Wechsel ist erkennbar. Für den Funktionstest ist das allerdings so gewollt. So lässt sich der Ablauf schließlich auf gut verfolgen.

Resumee

Technisch funktioniert die Anzeige nun. Ein kleines Programm existiert bereits, doch das ist ausbaufähig. Die “softe” Seite der Anzeige ist Thema des nächsten Beitrags.

Bisherige Beiträge zum Winterprojekt