Pfad: Home => AVR-Übersicht => Anwendungen => DCF77-Weckuhr m16 => Datum und Uhrzeit mit dem AVR   This page in english: Flag EN
DCF77 Weckuhr Anwendungen von
AVR-Einchip-Prozessoren AT90S, ATtiny, ATmega und ATxmega
DCF77 Weckuhr mit LCD
Datum und Uhrzeit

Logo

Datum und Uhrzeit mit dem AVR

Es kommt gelegentlich vor, dass man das Datum und die Uhrzeit auf einem beliebigen AVR aktuell halten und darstellen muss. Diese Seite zeigt einige Möglichkeiten auf, wie man das auf einem AVR in Assembler programmieren kann.

0 Inhalt

  1. Sekunden korrekt messen
  2. Uhrzeit-Formate
  3. Uhrzeit und Datum
  4. Ganzzahl-Datum
Diese Seite als PDF zum Download, 13 Seiten, 463 kB.

1 Sekunden korrekt messen

1.1 Schleifen zum Timing

Hat der AVR sonst nichts anderes zu tun (was selten vorkommt), kann man die Sekunden mit Verzögerungsschleifen abzählen. Das geht so.

Da der AVR ziemlich schnell ist, braucht man zum Abzählen einer Sekunde bei 1 MHz Takt mindestens einen Zähler der bis ca. 300.000 zählen kann. Für diese Zahl braucht man 20 Bits, ein 8- oder 16-Bit-Zähler alleine reicht daher nicht aus. Mit einer 16-Bit-Schleife und einer äußeren 8-Bit-Schleife kommt man zurecht. Die 16-Bit-Schleife sieht so aus:

  .equ schleifendurchlaeufe = 62499 ; Anzahl Durchlaeufe definieren 
  ldi R25,High(Schleifendurchlaeufe) ; Zaehler laden, ein Takt
  ldi R24,Low(Schleifendurchlaeufe) ; noch ein Takt
Schleife16:
  sbiw R24,1 ; Abwaerts zaehlen, zwei Takte
  brne Schleife16 ; Rueckwaerts, zwei Takte bei Sprung, ein Takt
  nop
  nop
  nop

Die abschliessenden NOP-Instruktionen machen einen gewissen Sinn, denn die Schleife braucht Zusammen mit den beiden LDI-Instruktionen und den NOP ermittelt sich die Anzahl Takte zu:

N = 2 + 4 * (Schleifendurchlaeufe - 1) + 3 + 3


Das verkürzt sich zu:

N = 4 * (Schleifendurchlaeufe + 1)


Will man bei 1 MHz Takt also genau 250 ms erreichen, braucht es N = 250.000 Takte. Schleifendurchlaeufe muss dann

Schleifendurchlaeufe = N / 4 - 1 = 62.499


sein. Das schafft ein 16-Bit-Zähler gerade noch so.

Lässt man das ganze vier mal ausführen, ist man bei einer Sekunde, z.B. so:

  .equ z8 = 4
  .equ z16 = 62499
  ldi R16,z8 ; Anzahl Durchlaeufe aussen, ein Takt
Scheife8:
  ldi R25,High(z16) ; Zaehler laden, ein Takt
  ldi R24,Low(z16) ; noch ein Takt
Schleife16:
  sbiw R24,1 ; Abwaerts zaehlen, zwei Takte
  brne Schleife16 ; Rueckwaerts, zwei Takte bei Sprung, ein Takt
  dec R16 ; ein Takt
  brne Schleife8 ; zwei Takte bei Sprung, ein Takt beim letzten

Die innere Schleife braucht jetzt

N16 = 2 + 4 * (z16 - 1) + 3 = 4 * z16 + 1


und das gesamte braucht

N = 1 + z8*N16 + 3 * (z8 - 1) + 2 =
1 + z8 * N16 + 3 * z8 - 1 =
z8 * N16 + 3 * z8


Das gibt dann genau 1.000.000 Takte.

Wie gesagt: nur wenn nichts weiteres dazu kommt und wenn der Takt von einem Quarz und nicht von einem super-ungenauen internen RC-Oszillator stammt.

1.2 Timer als Taktzähler

Die elendige Taktzählerei ist man los, wenn man dem Timer das Zählen überlässt. Der kann das viel besser, genauer und zuverlässiger und lässt sich vor allem durch nichts anderes von seinem Zählen ablenken.

Wer jetzt Grundlegendes über Timer wissen will, geht in diese Webseite, dort gibt es alles über Timer, über Quarze, u.v.a.m.

Unglücklicherweise ist bei einem MHz Takt schon bei einem Vorteiler von 64 Schluss mit lustig (ergibt 15.625 Hz), Teilerraten darüber liefern zunehmend krumme Frequenzen. Da eine Uhr aber sowieso einen Quarzoszillator braucht, damit sie einigermaßen genau geht, wählt man eben was besseres aus. Im Angebot sind z. B. 2,048 MHz, 2,097152 MHz, 2,4576 Mhz, 3,072 MHz, 3,2768 MHz oder 4,194304 MHz.

Teilerraten mit 8- und 16-Bit-Timer Das hier sind die Teilerraten bei den möglichen Vorteilerwerten von 1, 8, 64, 256 und 1.024 für 8-Bit-Timer und für 16-Bit-Timer im Normalbetrieb. Man erkennt, dass der 8-Bit-Timer auch bei einem Vorteiler von 1.024 nicht so arg hoch kommt, Quarze mit diesen niedrigen Frequenzen müsste man sich handfertigen lassen und der Prozessor wäre auch lahm wie eine Ente.

Vielversprechender ist da ein 16-Bit-Timer mit einem Vorteiler von 64. Quarze mit 4,194.304 MHz gibt es an jeder Straßenecke zu kaufen und kosten gerade mal 25 Cent. Dann ist der Sekundentimer fast schon fertig, nur noch:
  1. Quarz und zwei Keramikkondensatoren von 18 pF an den AVR anschließen,

    Sekundentimer

  2. Fuses des Prozessors auf den externen Quarz umprogrammieren,
  3. die Interrupt-Vektortabelle des Prozessors mit lauter RETI schreiben, außer dem Reset-Vektor (RJMP Start und dem Überlauf-Interrupt für TC1 (RJMP TC1OvfIsr),
  4. die Überlauf-Interrupt-Service-Routine "TC1OvfIsr" für den Timer mit den zwei Instruktionen SET (Setzt die T-Flagge im Statusregister) und RETI (Rückkehr vom Interrupt) schreiben,
  5. im Hauptprogramm "Start:" den Stapel initiieren,
  6. dann den 16-Bit-Timer auf Normalbetrieb und den Vorteiler auf 64 einstellen,
  7. dort auch den TC1-Überlauf-Interrupt (TOIE1) und die Interrupts generell mit SEI einschalten,
  8. in einer Schleife immer abfragen, ob die Sekundenflagge T gesetzt ist und wenn ja, diese brav wieder löschen und das machen, was nach jeder Sekunde erledigt werden muss.
Schon ist eine sekundengenaue Quarzuhr ohne viele Umschweife fertig. Alles andere, was noch in der Schleife so gemacht wird, hat auf dieses Timing keinen Einfluss mehr: der Timer zählt stur bis er überläuft und setzt die T-Flagge.

der kann auch andere Lösungen erfinden.

Wichtig dabei ist nur, dass die gewählte Quarzfrequenz, geteilt durch den Vorteiler und die Teilerrate durch den Timer (256 oder 65,536) keine krumme Frequenz ergibt. Bleibt beim Teilen noch eine Ganzzahl übrig, kriegt man die durch Opfern eines Registers (wenn sie kleiner oder gleich 256 ist) oder eines Registerpaars (wie z. B. R25:R24, wenn sie größer 256 ist) weg.

Registerteiler

Mit den eingangs genannten Quarzen ergeben sich für einen 8-Bit-Timer die folgenden ganzzahligen Teilerwerte für den Registerzähler.

Quarze und Registerteiler Die grün hinterlegten Teilerwerte passen in ein 8 Bit breites Register, nur bei 3,072 MHz braucht man einen 16 Bit breiten Registerzähler.

In Assembler geht die Teilerei dann z. B. so: Das geht dann z. B. per Timer-Overflow-Interrupt so:

  ; Rechenkonstanten
  .equ takt = 2097152 ; Quarzfrequenz
  .equ vorteiler = 1024
  .equ timertop = 256
  .equ sekundenteiler = takt / timertop / vorteiler ; = 8 
  ;
  ; Register
  .def rSreg = R15 ; Sichern des Statusports
  .def rSekundenteiler = R17 ; Abwaertszaehler
  .def rFlag = R18 ; Flaggenregister
  .equ bSek = 0 ; Sekundenflagge
;
Tc0OvflwInt:
  in rSreg,SREG ; SREG sichern
  dec rSekundenteiler ; Teilt durch 8
  brne TC0OvflwIntRet ; Noch nicht Null
  ldi rSekundenteiler,sekundenteiler ; Neustart Teiler
  sbr rFlag,1<<bSek ; Flagge setzen: Sekunde ist um
Tc0OvflwIntRet:
  out SREG,rSreg ; Wiederherstellen SREG
  reti

Die Methode ist nicht so arg ideal, wenn man aus anderen Gründen auf eine bestimmte Taktfrequenz angewiesen ist. Nur bei 4,0 MHz Takt und einem 8-Bit-Timer durch 256 bei kommt bei einem Vorteiler von 1 die Ganzzahl 15.625 heraus. Die kann man in das Doppelregister R25:R24 packen und bei jedem Overflow-Interrupt des Timers mit SBIW R24,1 um Eins abwärts zählen. Erreicht der Zähler Null, ist die Sekunde um und der Zähler wird mit 15.625 wieder neu gestartet. Nicht bei vielen anderen Frequenzen funktioniert das so. Aber es gibt da eine weitere Methode.

Viele schönen Taktfrequenzen wie 1 oder 2 MHz kriegt man beim Teilen durch 256 oder 65.536 rein gar nicht zu ganzzahligen Resultaten und daher so nicht in den Griff. Da muss der Timer dann in den CTC-Modus gebracht werden, damit er auch andere Teilerraten als nur 256 kann. Beim CTC-Modus setzt der Timer sich auf Null zurück, wenn er beim vorausgehenden Takt einen Vergleichswert in einem Vergleichsregister (Compare A, bei 16-Bit-Zählern auch das Input Capture Register ICR) erreicht hatte. Da er immer nachfolgend Gleichheit prüft, muss der Vergleichswert um Eins niedriger eingestellt werden als man teilen möchte. Braucht man den gleichen Timer auch noch für andere Zwecke, z. B. als Pulsweitenmodulator, ändert sich dadurch natürlich dessen Auflösung.

Bei einem MHz Takt kann man den Vorteiler auf 64 stellen (15.625 kHz Takteingang am Timer), den Timer durch 125 teilen lassen (Compare Match auf 124) und kommt mit einem Registerteiler von 125 dann auf eine Sekunde. Viele andere Frequenzen kriegt man so auf die Sekunde heruntergeteilt.

Und schon hat man ein todsicher genaues Sekundensignal ganz ohne Zählgrab. Natürlich muss dazu

Seitenanfang Sekunden Formate Datum Ganzzahl-Datum

2 Uhrzeit-Formate

Uhrzeiten lassen sich in mindestens vier gebräuchlichen Formaten handhaben. Glücklicherweise lassen sich alle Formate ineinander umwandeln.

2.1 Zeit im ASCII-Format

Uhrzeit im ASCII-Format Das auf den ersten Blick einfachste Format ist das ASCII-Format. Jede Dezimalstelle des Datums wird als ASCII-Zeichen in einem Byte gespeichert. Das ASCII-Format stammt von Militärfernschreibern der 1950er ab und ist so hartnäckig wie das Inch oder der Kubikfuß, aber älter als das Fass ("barrel"). Die Null ist danach die Dezimalzahl 48 oder hexadezimal 0x30. Das Einschließen der Zahlen und Zeichen in 'x' sagt: es handelt sich um die ASCII-Repräsentation des Zeichens (bei der '1' um dezimal 49). In AVR-Assembler geht das dann so:

  ldi R16,'0' ; ASCII-Zeichen fuer Null, Dezimal 48

Das Format hat den Vorteil, dass man die so gespeicherte Uhrzeit unmittelbar auf einer LCD ausgeben kann. Daher kann man den Zahlen auch gleich die Trennzeichen : hinzufügen, die dann in einem Rutsch ebenfalls auf die LCD ausgegeben werden können.

Es werden acht Bytes gebraucht, vorzugsweise kann daher dieses Uhrzeitformat im SRAM gespeichert und gehandhabt werden. Den Platz dafür kriegt man so definiert:

.dseg ; Datensegment: nur Label und .byte-Direktiven
Zeit: ; Label mit Adresse des Zeit-Bereichs im SRAM
.byte 8 ; Acht Bytes Platz im SRAM reservieren

Um die Uhr auf Null zu stellen und die beiden Doppelpunkte schon mal in das SRAM zu schreiben kann man so vorgehen:

  ldi XH,High(Zeit) ; MSB der SRAM-Adresse in XH
  ldi XL,Low(Zeit) ; LSB in XL
  ldi R16,'0' ; ASCII-Null in R16
  st X+,R16 ; Schreibe Stunden-Zehner und erhoehe X
  st X+,R16 ; Und in die Stunden-Einer
  ldi R16,':' ; Trennzeichen
  st X+,R16 ; in die erste Trennzeichenstelle
  ldi R16,'0' ; ASCII-Null in das Register
  st X+,R16 ; und in den Minuten-Zehner
  st X+,R16 ; und in den Minuten-Einer
  ldi R16,':' ; Trennzeichen
  st X+,R16 ; in die zweite Trennzeichenstelle
  ldi R16,'0' ; ASCII-Null in das Register
  st X+,R16 ; in die Sekunden-Zehner
  st X,R16 ; und die Sekunden-Einer

Damit ist die Uhrzeit mit den beiden Trennzeichen gespeichert.

Es geht aber auch anders, nämlich ohne das andauernde Umladen des Registers:

  ldi YH,High(Zeit) ; MSB der SRAM-Adresse der Zeit in YH
  ldi YL,Low(Zeit) ; LSB in YL
  ldi R16,'0' ; ASCII-Null in R16
  st Y,R16 ; in die Stunden-Zehner
  std Y+1,R16 ; in die Stunden-Einer
  std Y+3,R16 ; in die Minuten-Zehner
  std Y+4,R16 ; in die Minuten-Einer
  std Y+6,R16 ; in die Sekunden-Zehner
  std Y+7,R16 ; und in die Sekunden-Einer
  ldi R16,':' ; Trennzeichen in R16
  std Y+2,R16 ; in die erste Trennzeichenstelle
  std Y+5,R16 ; in die zweite Trennzeichenstelle

STD (und beim Lesen: LDD) ändert die Adresse in Y nicht sondern addiert die Zahl dahinter nur temporär vor dem Schreibvorgang. Nach dem Schreiben bleibt Y gleich wie vorher. Mit dem Doppelregister X geht das nicht, nur mit Y und Z.

Um die Uhrzeit um eine Sekunde zu erhöhen, wird mit dem hintersten Byte begonnen. Überschreitet dieses Byte nach dem Erhöhen die "9", z. B. so:

  ldi YH,High(Zeit) ; MSB der SRAM-Adresse der Zeit in YH
  ldi YL,Low(Zeit) ; LSB in YL
  ldd R16,Y+7 ; Lese Sekunden-Einer in R16
  inc R16 ; Erhoehe Sekunden-Einer um Eins
  std Y+7,R16 ; Schreibe erhoehte Sekunden-Einer
  cpi R16,'9'+1 ; Vergleiche mit dem naechsten Zeichen hinter '9'
  brcs Fertig ; Wenn carry gesetzt kein Ueberlauf
  ldi rmp,'0' ; Starte Sekunden-Einer neu
  std Y+7,R16 ; Schreibe Null in Sekunden-Einer
  ldd R16,Y+6 ; Lese Sekunden-Zehner
  inc R16 ; Erhoehe Sekunden-Zehner
  std Y+6,R16 ; und schreibe zurueck in Sekunden-Zehner
  cpi R16,'6' ; Sekunden-Zehner auf Sechs?
  brcs Fertig ; Nein, schon fertig
  ; ... Minuten und Stunden in gleicher Weise
Fertig:
  ; ... Hier ist die Sekundenerhoehung fertig  

Bei der Erhöhung der Stunde ist zweierlei zu überprüfen: Die Handhabung der Sekundenerhöhung beim ASCII-Format ist dank der relativen Adressierung mit STD und LDD recht einfach.

2.2 Zeit im BCD-Format

Uhrzeit im BCD-Format Beim zweiten Format werden die Zehner und Einer von Sekunden, Minuten und Stunden nicht als ASCII-Zeichen, sondern als binär kodierte Dezimalziffern gespeichert (BCD, binary coded digit). Die Bytes haben binäre Werte zwischen 0 und 9 (Einer) bzw. 0 bis 5 (Sekunden- und Minutenzehner) bzw. 0 bis 2 (Stunden-Zehner).

Der Vergleich, ob bei den Einern die 9 überschritten wird, erfolgt jetzt mit CPI R16,10 statt mit CPI R16,'9'+1. Der Neustart der Einer erfolgt statt mit LDI R16,'0' mit CLR R16 oder mit LDI R16,0. Alles andere im oberen Erhöhungscode bleibt gleich, bis auf die beiden fehlenden Doppelpunkte.

Gibt man die BCD-Zahlen auf die LCD aus, muss man einfach die ASCII-0 addieren und das Ergebnis auf die LCD schreiben. Da der AVR keine Instruktion hat, mit der sich 48 zur BCD-Zahl addieren lässt, gibt es drei Lösungen für diesen Schritt:
  1. Man schreibt die 48 in ein anderes Register und addiert dieses andere Register mit ADD R16,R17 zu demjenigen mit der BCD-Zahl.
  2. Man setzt die Bits 4 und 5 in der BCD-Zahl mit ORI R16,0x30 oder mit SBR R16,0x30 oder auch mit SBR R16,(1<<4)|(1<<5) auf Eins. Auf diese Weise wird ebenfalls aus BCD-0 die ASCII-'0' und aus 9 die '9'.
  3. Man subtrahiert -48 von dem Register mit SUBI R16,-'0'. Da aus zwei Minusvorzeichen Plus wird, kommt dasselbe wie beim Addieren von '0' heraus.
Alle drei Methoden haben den gleichen Effekt. Nur bei der ersten Methode ist vorübergehend ein weiteres Register nötig.

Beim Ausgeben auf die LCD die beiden Trennzeichen an den richtigen Stellen nicht vergessen, sonst sieht die Uhrzeit ziemlich doof aus.

2.3 Zeit im gepackten BCD-Format

Zeit in gepacktem BCD-Format Da so eine BCD-Zahl mit vier Bits auskäme, kann man zwei von denen in ein einziges Byte quetschen. In die unteren vier Bit (0..3) kommen die Einer, in die oberen vier Bit (4..7) die Zehner. Die Uhrzeit braucht dann nur drei Bytes. Das heißt dann gepacktes BCD oder "Packed BCD".

Wenn man eine gepackte BCD-Zahl um Eins erhöhen will, kann man natürlich ebenfalls INC R16 verwenden. Es ist dann aber etwa aufwändiger festzustellen, ob das untere Nibble (halbes Byte, vier Bits) die 9 überschritten hat. Dazu wäre zuerst das obere Nibble zu löschen und danach erst zu vergleichen. Da das obere Nibble aber später wieder gebraucht wird, ist das ungünstig.

Es gibt aber eine viel einfachere Möglichkeit: das Halbübertragsbit. Es stellt beim Addieren mit ADD oder auch beim SUBI (nicht beim Inkrementieren mit INC!) fest, ob ein Übertrag vom unteren in das obere Byte erfolgt. Es ist das H-Bit im Statusregister SREG. Abhängig von der letzten Operation kann mit BRHC oder mit BRHS verzweigt werden, wenn das H-Bit gelöscht (BRHC, clear) oder gesetzt ist (BRHS, set). Addiert man statt eine 1 sieben, würde man am H-Bit erkennen, ob dabei ein Übertrag in das obere Nibble erfolgt. Ist das der Fall, dann ist alles schon ok, denn Falls das H-Flag nicht gesetzt ist, dann war vor dem Addieren keine Neun im unteren Nibble. Dann müssen von den addierten 7 wieder sechs abgezogen werden. Das klingt total einfach und ist es auch.

Eine Besonderheit tritt auf, wenn zum Addieren der Sieben nicht LDI R17,7 und ADD R16,R17 benutzt sondern die Instruktion SUBI R16,-7 verwendet wird. In diesem Fall kehrt sich das H-Flag um: H clear bedeutet dann einen Übertrag und H set keinen Übertrag.

Um festzustellen, ob die 59 Sekunden überschritten sind, genügt der Vergleich mit 0x60: tritt dabei ein Carry auf, dann sind die 59 Sekunden noch nicht überschritten. Wenn nicht wird das gepackte BCD der Sekunden einfach auf Null gesetzt.

Beim gepackten BCD-Format der Stunden ist das die Erkennung des Tageswechsels noch viel einfacher: mit CPI R16,0x24 und danach gelöschtem Carry sind die 24 Stunden voll. Nichts mit zwei Bytes vergleichen.

Der Einfachheit halber die ganze Zeiterhöherei um eine Sekunde in gepacktem BCD:

  ldi ZH,High(Zeit) ; Z zeigt auf die Stunden in gepacktem BCD
  ldi ZL,Low(Zeit)
  ldd R16,Z+2 ; Lese die Sekunden
  subi R16,-7 ; Addiere Sieben
  brhc ChkSek ; H geloescht, Zehner erhöht, Sekunden auf 60 pruefen
  subi R16,6 ; H gesetzt, Sechs wieder abziehen
ChkSek:
  std Z+2,R16 ; Sekunden schreiben
  cpi R16,0x60 ; 60 Sekunden?
  brcs Fertig ; Nein, schon fertig
  clr R16 ; Sekunden mit 0 beginnen
  std Z+2,R16 ; Und in das SRAM schreiben
  ldd R16,Z+1 ; Lese Minuten
  subi R16,-7 ; Addiere Sieben
  brhc ChkMin ; H geloescht, Zehner erhoeht, Minten auf 60 pruefen
  subi R16,6 ; H gesetzt, Sechs wieder abziehen
ChkMin:
  std Z+1,R16 ; Und in das SRAM schreiben
  cpi R16,0x60 ; 60 Minuten erreicht?
  brcs Fertig ; Nein, schon fertig
  clr R16 ; Minuten mit Null beginnen
  std Z+1,R16 ; und ins SRAM schreiben
  ld R16,Z ; Stunden lesen
  subi R16,-7 ; Addiere sieben
  st Z,R16 ; und in das SRAM schreiben
  brhc ChkStd ; H geloescht, Stunden stimmen schon
  subi R16,6 ; H gesetzt, Sechs wieder abziehen
  st Z,R16 ; und in das SRAM schreiben
ChkStd:
  cpi R16,0x24 ; 24 Stunden voll?
  brcs Fertig ; Gesetztes Carry wenn kleiner 24
  clr R16 ; Stunden mit Null beginnen
  st Z,R16 ; ins SRAM schreiben
Fertig: ; Erhoehung fertig erfolgt

Das war schon alles. Wer es nicht glaubt kann den Code mit einem Simulator überprüfen. Natürlich müssen Stunden, Minuten und Sekunden im SRAM auf einen korrekten Uhrzeitwert, z. B. auf "23:59:59", gesetzt werden.

Um gepackte BCD-Zahlen auf die LCD auszugeben, muss man die Ziffern nacheinander (das obere Nibble zuerst) in die entsprechende ASCII-Ziffer umwandeln. Um die beiden Ziffern auszugeben, muss man so vorgehen:

  ld R16,Z ; Z zeigt auf die Stunde, lese Stunden
  swap R16 ; Vertausche oberes und unteres Nibble
  and R16,0x0F ; Isoliere das untere Nibble
  subi R16,-'0' ; Addiere ASCII-Null
  ; R16 an LCD ausgeben
  ld R16,Z ; Stunden noch mal lesen
  andi R16,0x0F ; Isoliere unteres Nibble
  subi R16,-'0' ; Addiere ASCII-Null
  ; R16 an LCD ausgeben

Nach den Stunden kommt dann das Trennzeichen dran, dann die Minuten wie oben, wieder ein Trennzeichen und schließlich die Sekunden wie oben. Da die obige Routine drei Mal identisch ausgeführt wird, kann man sie auch als Unterprogramm formulieren und drei Mal aufrufen. Natürlich nachdem man den Stapel angelegt hat. Vor dem Aufruf zur Ausgabe der Minuten und Sekunden erhöht man einfach den Zeiger Z, z. B. mit ADIW ZL,1. Geht alles mit ganz wenigen einfachen Instruktionen und schon ist die Uhr fertig.

2.4 Zeit im Binärformat

Uhrzeit im Binaerformat Am Schluss die allereinfachste aller Formatierungen: Sekunden, Minuten und Stunden im binären Format. Die Einer und Zehner passen binär in ein Byte, können mit einem einfachen INC um eins erhöht werden und zum Feststellen, ob das Maximum erreicht ist, ist auch nur ein einfacher Vergleich nötig. Da nur drei Bytes nötig sind, kann man das zur Abwechslung mal in drei Registern erledigen. Das Erhöhen der Uhrzeit um eine Sekunde in Assembler geht dann so:

  .def rStd = R4 ; Stundenregister
  .def rMin = R5 ; Minutenregister
  .def rSek = R6 ; Sekundenregister
IncSek:
  ldi R16,60 ; Ende feststellen
  inc rSek
  cp rSek,R16 ; Sekunde kleiner Minutenende?
  brcs Fertig ; Nein
  clr rSek ; Sekunden neu beginnen
  inc rMin ; Minuten erhoehen
  cp rMin,R16 ; Minuten kleiner Stundenende?
  brcs Fertig ; Nein
  clr rMin ; Minuten neu beginnen
  inc rStd ; Stunden erhoehen
  ldi R16,24 ; Stundenende
  cp rStd,R16 ; Tag zu Ende?
  brcs Fertig ; Nein
  clr rStd ; Stunden auf Null
Fertig:
  ; Sekundenerhoehung beendet

Mit 14 Einfachst-Instruktionen für eine fertige Uhrzeit nicht gerade intellektuell anspruchsvoll. Jedenfalls kein Grund, irgendeine mächtige C-Datumsbibliothek zu laden.

Dafür muss jetzt erst jede Binärzahl vor der Ausgabe auf die LCD in zweistelliges ASCII verwandelt werden. Da alle drei Bytes in gleicher Weise auszugeben sind, macht das eine Unterroutine, der wir in R16 die auszugebende Binärzahl übergeben.

Bin2Dez2:
  clr R0 ; Zehner zaehlen in R0
Bin2Dez2a:
  inc R0 ; Zaehler erhoehen
  subi R16,10 ; Zehn abziehen
  brcc Bin2Dez2a ; Kein Carry, weiter abziehen
  subi R16,-10-48 ; Letztes Abziehen rueckgaengig machen (10 addieren)
                  ; und in ASCII verwandeln (48 addieren)
  push R16 ; Wird noch gebraucht, auf den Stapel
  ldi R16,'0'-1 ; Zaehler = 1 zu ASCII-Null
  add R16,R0 ; Zaehler dazu addieren
  rcall LcdChar ; R16 als Zeichen an die LCD ausgeben
  pop R16 ; Zweite Dezimalstelle vom Stapel holen
  rjmp LcdChar ; und auf die LCD ausgeben 

Die Routine LcdChar gibt das Zeichen in R16 auf die LCD aus. Wegen des RCALL sowie wegen PUSH und POP muss natürlich die Stapelverwaltung funktionieren. Zum Testen und Simulieren setzt man z. B. Z auf den Beginn des SRAM (SRAM_START) und gibt mit der Routine LcdChar den Inhalt mit ST Z+,R16 und RET die Zeichen statt in die LCD ins SRAM aus.

Die Uhrzeitanzeige ist dann denkbar einfach:

Anzeige:
  mov R16,rStd ; Stunden in R16
  rcall Bin2Dez2 ; Ausgaberoutine aufrufen
  ldi R16,':' ; Trennzeichen
  rcall LcdChar ; ausgeben
  mov R16,rMin ; Minuten in R16
  rcall Bin2Dec2 ; Ausgaberoutine aufrufen
  ldi R16,':' ; Noch ein Trennzeichen
  rcall LcdChar ; ausgeben
  mov R16,rSek ; Sekunden in R16
  rjmp Bin2Dez2 ; und ausgeben

Mit gerade mal 20 Instruktionen, davon 10 für die BCD-Anzeige, auch nicht gerade riesen-anspruchsvoll.

2.5 Das beste Format

... ist natürlich das Binärformat. Aber die anderen drei Formate haben auch so das Eine oder Andere auf ihrer Seite. Mach also was Du willst, es geht einfach alles.

Seitenanfang Sekunden Uhrzeit-Formate Datum Ganzzahl-Datum

3 Uhrzeit und Datum

Nachdem wir die Uhrzeit bewältigt haben, kann uns auch das Datum nicht mehr abschrecken. Es funktioniert ganz genau so, nur Bei der Anordnung im SRAM oder auf der LCD herrscht ebenfalls die totale Unlogik vor: Tage und Monate kommen vor die Jahre und nicht dahinter (bei den Angelsachsen ist es noch verrückter, da kommen die Monate vor den Tagen, aber die Jahre ganz hinten).

Anzahl Tage im Monat Und? Wieviel Tage hat nun der Monat?

MonatTageNKorr.N korr.Tdm=31?
Januar310b0001ja
Februar28/290b0010nein, spez.
März310b0011ja
April300b0100nein
Mai310b0101ja
Juni300b0110nein
Juli310b0111ja
August310b1000-1=0b0111ja
September300b1001-1=0b1000nein
Oktober310b1010-1=0b1001ja
November300b1011-1=0b1010nein
Dezember310b1100-1=0b0111ja


Der Februar ist erstmal ein Sondermonat und braucht seinen eigenen Algorithmus, um festzustellen, ob er 28 (kein Schaltjahr) oder 29 Tage hat (Schaltjahr). Dazu muss das Jahr gelesen und darauf getestet werden, ob die beiden untersten Bits beide Null sind (durch ein UND mit 0x03). Falls ja, ist die Zahl der Tage dieses Monats 29, sonst 28.

Bei den anderen Monaten geht es erst mal nach der Regel, dass ungerade Monate (1, 3, 5 und 7) 31 Tage haben, gerade Monate (4, 6) 30 Tage. Wie man aber bei den anderen Monaten sieht, wechselt die Regel ab dem Monat August: ab jetzt sind gerade Monate (8, 10, 12) solche mit 31 Tagen, ungerade (7, 9, 11) haben 30 Tage.

Ist der Monat größer als sieben, müssen wir also eine Eins vom Monat abziehen, damit die Regel wieder stimmt, dass ungerade Monate (nun die korrigierten) 31 Tage haben.

Das alles macht den Programmierer ganz wuschig. Um z.B. die Tage zu bestimmen, die ein Monat hat, ist nebenstehender Algorithmus nötig. Sieht kompliziert aus, ist aber in Assembler gar nicht so schlimm:

;
; Unterprogramm ermittelt die Tage des Monats
;   gibt in rTdm die Anzahl Tage des Monats (28..31) in rMonat (1..12) zurueck
;
TageMonat:
  cpi rMonat,2 ; Februar?
  brne TageMonat1 ; Nein
  ldi rTdm,28 ; Kein Schaltjahr
  mov rmp,rJahr ; Schaltjahr?
  andi rmp,0x03 ; Jahr durch vier teilbar?
  brne TageMonatRet
  ldi rTdm,29 ; Schaltjahr
TageMonatRet:
  ret ; Fertig
TageMonat1:
  ldi rTdm,31 ; 31 Tage Januar
  brcs TageMonatRet
  mov rmp,rMonat
  cpi rmp,8
  brcs TageMonat2
  dec rmp
TageMonat2:
  ldi rTdm,31 ; Monat mit 31 Tagen
  andi rmp,0x01 ; Ungerade
  brne TageMonatRet
  ldi rTdm,30
  rjmp TageMonatRet

Test der Monatstage-Routine Aufgerufen mit den Monatstagen 1 bis 12 liefert die Routine für ein Schaltjahr "S" die obere Reihe, für kein Schaltjahr "N" die untere Reihe an Ergebnissen. Man beachte, dass hier die Regeln des Herrn Gregor XIII nur unvollständig abgebildet sind (Stichwort: Jahre, die durch 100 oder 400 teilbar sind, aber bis 2100 ist das noch lange hin und wer weiß ob es dann noch funktionierende AVRs gibt).

4 Datum in das Dezimalformat und zurück umwandeln

Es gibt ein weiteres Datumsformat, das oft sinnvoll ist: das Datum als Ganzzahl. Es basiert auf den Tagen, die vom 1.1.1900 bis zum heutigen Tag vergangen sind. Jeder weitere Tag führt zu einem Anstieg um Eins.

Dieses Datumsformat hat den Vorteil, dass es nicht durch nationale Besonderheiten verunstaltet wird.

Ich habe zwei Varianten davon. Die Erste Variante, beschrieben in den Kapiteln 4.1 bis 4.3, berechnet Datumsangaben ab dem 01.01.2000. Die zweite Variante erweitert den Anwendungsbereich auf Datumsangaben zwischen dem 01.01.1900 und dem 04.06.2079 (Kapitel 4.4).

4.1 Binärdatum ab 1.1.2000 in das Dezimalformat umwandeln

Flussdiagramm der Umrechnung eines Datums in das Dezimalformat Das Verfahren geht so: man nehme einen Basiswert, hier habe ich den 31.12.1999 genommen. Dazu addiert man die Tage des Datums (hier war es binär in der SRAM-Zelle sDy gespeichert.

Nun kommen die Monate dran. War in der SRAM-Zelle sMonth die 1 für den Januar, ist man schon fertig mit den Monaten und geht direkt zu den Jahren. Wenn nicht dann gibt es eine Tabelle, die für die jeweiligen Monate die hinzuzuziehenden Tage angibt. Handelt es sich den März und später und um ein Schaltjahr (die beiden unteren Bits des Jahres in sYr sind beide Null), dann wird noch ein Schalttag hinzugezählt.

Dann geht es mit den Jahren weiter. Die stehen binär in sYear. Für jedes ganze Jahr werden 365 Tage hinzuaddiert. Die Software weiter unten macht das mit binärem Multiplizieren, das geht schneller als bloßes Addieren.

Zu allerletzt werden die Schaltjahre hinzu addiert. Die kriegt man aus den Jahren, indem man sie durch vier teilt. Das Ergebnis wird dann im SRAM abgelegt.

Hier der Quellcode für diese Umwandlungsroutine.

; Calculate date value from Day/Month/Year
Date2B:
  ; Set base date 12/31/99
  ldi XH,High(c991231)
  ldi XL,Low(c991231)
  ; Add days
  clr R1 ; MSB is zero
  lds R0,sDy ; Add days
  add XL,R0
  adc XH,R1
  ; Add monthes
  lds rmp,sMo
  dec rmp
  breq Date2BYears
  lsl rmp
  ldi ZH,High(2*MonthTable)
  ldi ZL,Low(2*MonthTable)
  add ZL,rmp
  brcc Date2BMonthes1
  inc ZH
Date2BMonthes1:
  lpm R0,Z+
  lpm R1,Z
  cpi rmp,4
  brcs Date2BMonthesAdd
  lds rmp2,sYr
  andi rmp2,0x03
  brne Date2BMonthesAdd
  inc R0
Date2BMonthesAdd:
  add XL,R0
  adc XH,R1
  ; Calculate years
Date2BYears:
  ldi ZH,High(365)
  ldi ZL,Low(365)
  lds rmp,sYr
Date2BYear:
  tst rmp
  breq Date2BLeaps
  lsr rmp
  brcc Date2BYearNext
  add XL,ZL
  adc XH,ZH
Date2BYearNext:
  lsl ZL
  rol ZH
  rjmp Date2BYear
Date2BLeaps:
  lds rmp,sYr
  lsr rmp
  lsr rmp
  add XL,rmp
  ldi rmp,0
  adc XH,rmp
  sts sDateVal,XL
  sts sDateVal+1,XH
  ret
;
MonthTable:
  .dw 0,31,59,90,120,151
  .dw 181,212,243,273,304,334

Der Quellcode für diese Unterroutine ist in datetime_value_tn13 enthalten.

4.2 Dezimalformat ab 1.1.2000 in binär verwandeln

Auch der Weg zurück ist recht einfach:
  1. Zuerst diviert man die um Eins verminderte Ganzzahl durch 365,25 (die durchschnittliche Anzahl Tage pro Jahr). Um die Verwendung der Fließkommabibliothek zu vermeiden, teilt man das Vierfache der um Eins verminderten Ganzzahl (ergibt eine 24-Bit-Zahl) durch das Vierfache von 365,25 (was 1.461 ergibt, eine 16-Bit-Zahl). Das 8-Bit-Ergebnis des Teilens gibt die Jahre seit 1900 an. Addiert man 1900 dazu, kann man das Ergebnis als dezimales ASCII an der Position der Jahre ausgeben.
  2. Aus der Anzahl an Jahren ermittelt man die Anzahl der Tage, indem man diese mit 1.461 malnimmt und das Ergebnis durch vier teilt. Diese Anzahl an Tagen zieht man von der Ausgangszahl ab und erhält den Rest, der sich auf die Monate und die Einzeltage verteilt
  3. .
  4. Um aus diesem Rest den Monat zu ermitteln, geht man die Monats-Tabelle durch und zählt so lange die Einträge, bis der Tabelleneintrag größer als dem um Eins erhöhten Rest ist. Damit hat man den Monat herausgefunden und muss ihn nur noch in eine zweistellige ASCII-Zeichenkette verwandeln.
  5. Die Ermittlung des Tages ist dann recht einfach: von Monats-/Einzeltags- Rest zieht man den nächstniedrigen Monatstageeintrag der Tabelle ab, erhöht ihn um eins, wandelt das Ergebnis in einen zweistelligen ASCII-String und fertig ist auch der Tag.
Der Quellcode ist ein wenig umfangreicher:

; Procedure that converts date value to binary and string
; The month table: days accumulated for the monthes
;   from 1 (January) to 12 (December)
;   Note that for leap years the days from February
;   to December increase by one!
DVal2B:
  ; Load result value
  lds ZH,sDateVal+1 ; MSB
  lds ZL,sDateVal ; LSB
  ; CalculateYears
  sbiw ZL,1 ; Subtract one
  clr R0
  lsl ZL ; Multiply by 4
  rol ZH
  rol R0
  lsl ZL
  rol ZH
  rol R0
  ldi XH,High(1461) ; Load 4 * 365.25 to X
  ldi XL,Low(1461)
  ldi rmp,1 ; Result byte
DVal2BYears:
  lsl ZL
  rol ZH
  rol R0
  cp ZH,XL ; Compare with divider, LSB
  cpc R0,XH ; dto., MSB
  brcs DVal2BYears1
  sub ZH,XL
  sbc R0,XH
  sec ; Shift 1 into result
  rjmp DVal2BYears2
DVal2BYears1:
  clc ; Shift 0 into result
DVal2BYears2:
  rol rmp ; Shift bit into result
  brcc DVal2BYears
  lsl ZH
  rol R0
  cp ZH,XL
  cpc R0,XH
  brcs DVal2BYears3
  inc rmp
DVal2BYears3:
  subi rmp,100
  sts sYrTo,rmp
  ; Calculate days in years
  clr ZH ; Z is result
  clr ZL
  clr rmp2
  clr R0 ; for multiplier
DVal2BDaysYears:
  tst rmp
  breq DVal2BDaysYears2
  lsr rmp
  brcc DVal2BDaysYears1
  add ZL,XL
  adc ZH,XH
  adc rmp2,R0
DVal2BDaysYears1:
  lsl XL
  rol XH
  rol R0
  rjmp DVal2BDaysYears
DVal2BDaysYears2:
  lsr rmp2
  ror ZH
  ror ZL
  lsr rmp2
  ror ZH
  ror ZL
  lds XH,sDateVal+1 ; Re-Read the original date value
  lds XL,sDateVal
  sts sDateValTo+1,XH
  sts sDateValTo,XL
  subi XL,Low(36526) ; Subtract the date value of 01/01/2000
  sbci XH,High(36526)
  brcc DVal2BDaysYears3
  clr XH
  clr XL
DVal2BDaysYears3:
  sbc XL,ZL ; Subtract the days for the years, LSB
  sbc XH,ZH ; dto., MSB
  brcc DVal2BMonthes
  clr XH
  clr XL
DVal2BMonthes:
  ; Get month from the rest
  ldi ZH,High(2*MonthTable+2)
  ldi ZL,Low(2*MonthTable+2)
  clr rmp ; rmp is result
DVal2BMonthes1:
  inc rmp
  lpm R0,Z+
  lpm R1,Z+
  inc R0
  cp XL,R0
  cpc XH,R1
  brcc DVal2BMonthes1
  sts sMoTo,rmp
  ; Get the day from the rest
  clr R0
  clr R1
  sbiw ZL,2
  cpi ZL,Low(2*MonthTable)
  breq GetVal2BDays
  sbiw ZL,2
  cpi ZL,Low(2*MonthTable)
  breq GetVal2BDays
  lpm R0,Z+
  lpm R1,Z
GetVal2BDays:
  sub XL,R0
  sbc XH,R1
  lds rmp,sYrTo
  andi rmp,0x03
  brne GetVal2BDays1
  lds rmp,sMoTo
  cpi rmp,3
  brcc GetVal2BDays2
GetVal2BDays1:
  inc XL
GetVal2BDays2:
  sts sDyTo,XL
  ret

Auch hier wird diesselbe Monatstabelle wie oben verwendet.

4.3 Fertige Software für die Umwandlungen ab 1.1.2000

Die Software datetime_value_tn13 macht das Folgende:
  1. Sie kopiert einen auswählbaren ASCII-String mit einer nullterminierten Uhrzeit aus dem Flash in das SRAM.
  2. Dann wandelt sie den ASCII-String in drei Uhrzeit-Bytes um und legt sie im SRAM ab. Erkannt werden dabei führende Nullen sowie das englische (Monat/Tag/Jahr) Datumsformat als auch das deutsche Format (Tag.Monat.Jahr). Das Jahr wird im Kurzformat in einem Byte abgelegt.
  3. Die Routine von weiter oben verwandelt dieses Binär-Datum in das Dezimalformat und legt das Ergebnis im SRAM ab.
  4. Danach wird die Routine zum Rückverwandeln des Dezimalformats in das Binär-Format und in eine ASCII-Zeichenkette gestartet. Diese gibt in der deutschen Version (cEN = 0) das Datum im Format "DD.MM.YYYY" in das SRAM aus.
Datumswerte simuliert Alle Ergebnisse stehen im SRAM herum. Hier wurde der 11.Juni 2023 simuliert. In den SRAM-Zellen 0x0070 steht das Jahr (23=0x17), in 0x0071 der Tag (11=0x0B) und in 0x0073 der Monat. In den Zellen 0x0074 und 0x0075 steht das dezimale Datum, 0xB01F oder dezimal 45.536.

Die beiden nächsten Zeilen demonstrieren die Rückverwandlung des dezimalen Datums in Byte-Werte (ab 0x0080) sowie in den Datums-String (ab 0x0090).

Natürlich erkennt die Einleseroutine auch das deutsche Kurzformat sowie die englischen Datenformate "MM/DD/YYYY" und das Kurzformat "M/D/YY". Stellt man im Kopf des Quellcodes die Konstante cEn von 0 auf 1 um, kriegt man auch die Ausgabe in der letzten Zeile in englischem Datumsformat.

Natürlich funktioniert die obige Routine nur für die Jahre ab 2000. Wer 1900-er Werte braucht, muss das alles ein bisschen umstricken.

4.4 Berechnung von Datumsangaben zwischen 01.01.1900 und 04.06.2079

Manchmal braucht man einen etwas weiter gespannten Datumsbereich, der auch Datumsangaben bis zurück nach dem Jahr 1900 kann. Das gibt es mit der Software, die hier beschrieben ist.

Das Maximaldatum 04.06.2079 kommt daher, weil das auf den 01.01.1900 bezogene Datum an diesem Tag die Grenze von 16-Bit-Variablen vollständig ausschöpft, am darauf folgenden Tag sind dafür 17 Bits nötig. Wahrscheinlich wird spätestens dann die Office-Software auf das 01.01.2000-Referenzdatum umgestellt werden.

4.4.1 Dezimalwerte von Datumsangaben zwischen 01.01.1900 und 04.06.2079

Die folgenden Darstellungen stammen aus der LibreOffice-Calc-Datei hier als auch aus der avr_sim-Simulation der Assembler-Quellcode-Datei, die es hier zum Download gibt.

Datumswerte ab 1900 Das hier sind dezimale Datumswerte in Spalte A ab dem 01.01.1900 in der Spalte B. Das Referenzdatum der Tabellenkalkulation ist nach LibreOffice schon mal gar nicht der 01.01.1900 mit dem Tag 1: dieser Tag ist schon Tag 2 nach dieser LibreOffice-Zeitrechnung.

Datum nach Excel und nach LibreOffice Und es wird noch verrückter: Microsoft's Excel, Datei hier, im linken Bild, beginnt am 01.01.1900 tatsächlich mit dem Tag 1, hat dafür aber den 29.02.1900, den es nach offizieller Zeitrechnung gar nicht gibt. Denn das Jahr 1900 ist, wie alle vollen Jahrhunderte - außer dem Jahr 2000, weil das auch noch durch 400 ohne Rest teilbar ist - gar kein Schaltjahr. Excel erfindet hier also einen Tag, den es gar nicht gibt. Microsoft erläutert hier, warum dieser Fehler noch immer nicht korrigiert wird.

Die LibreOffice-Programmierer haben keinen 29.02.1900 erfunden, haben dafür aber den Bezugszeitraum auf den 31.12.1899 als Tag 1 zurückdatiert. So kann man das Problem, dass 1900 gar kein Schaltjahr ist, auch umgehen.

Zur Beruhigung: alle Datenwerte ab dem 01.03.1900 sind bei Excel und bei LibreOffice wieder identisch. Nur der Januar und Februar 1900 unterscheiden sich um Eins.

In der Spalte A der Tabelle rechts sind viele verschiedene Datumsangaben versammelt, die ich alle zum Testen meiner Software verwendet habe. Mit Hilfe der LibreOffice-Calc-Funktionen JAHR(), MONAT() und TAG() lassen sich die Jahre, Monate und Tage aus den Datumswerten in Spalte B rückermitteln. Das funktioniert übrigens sogar mit den Daten des Jahres 1899 ,0 und 1, im LibreOffice korrekt, Excel hingegen weigert sich beharrlich, den Tag Null als Datum zu formatieren.

4.4.2 Datumswerte für den Export in Assembler-Quellcode

Datumswerte ab 1900 exportieren Um diese Testdaten in Assembler-Quellcode-Dateien exportieren zu können, sind in den Spalten E, G und I diese Datumsangaben als Tabellen in drei verschiedenen Formaten umgewandelt:
  1. Spalte E: im englischen Datumsformat als MM/DD/YYYY,
  2. Spalte G: im deutschen Datumsformat nach DIN als TT.MM.JJJJ,
  3. Spalte I: im ISO-Format mit YYYYMMDD.
Alle Tabellen enthalten noch die beiden Bytes (LSB und MSB) der Datumswerte, um die Software auf Korrektheit überprüfen zu können.

Die Assembler-Software durchläuft, mit dem Registerpaar YH:YL als Zeiger, alle diese in der Tabelle gespeicherten Datumsangaben, und
  1. ordnet die im englischen oder deutschen Format angegebenen Datumsangaben in das ISO-Format YYYYMMDD um,
  2. wandelt diese Datumsangaben in den Datumswert um (im LibreOffice- Format, nicht im Excel-Format),
  3. vergleicht den Datumswert mit der Tabellenangabe und gibt eine Fehlermeldung aus (und hält die weitere Ausführung an), wenn diese beiden nicht übereinstimmen,
  4. wandelt den Datumswert wieder in das zugehörige Datum im Binärformat um,
  5. formatiert die binären Datumsangaben in ASCII-Zeichenketten im eingestellten Datumsformat um.

4.4.3 Lesen umd umformatieren des Datums

Die Datenstruktur im SRAM Links ist die Datenstruktur im SRAM als Quellcode abgebildet. In den ersten 16 SRAM-Zellen wird der Tabelleninhalt eingelesen und - rechtsbündig darin - der binäre Sollwert dieses Datums. Alle folgenden Zeilen sind übrigens ähnlich formatiert, so dass der Vergleich einfacher ausfällt.

Kopieren und Umformatieren eines Datums in deutsch Rechts ist das Kopieren (in der ersten Zeile) und das Umformatieren dieses Datums in das ISO-Format (in der zweiten Zeile) zu sehen, für ein Datum in deutschem Format. Es ist klar, dass dies nur die reine Herumschubserei von Bytes aus dem Flash in den ersten SRAM-Bereich und von dort in den zweiten SRAM-Bereich darstellt.

4.4.4 Umwandeln der ISO-ASCII-Zeichenkette in den Datumswert

Wandlung Datum in Binaerwerte Durch Aufruf der beiden Subroutinen ConvertStr2Bin wurden hier die drei ASCII-Zeichenketten des Jahres, des Monats und des Tages in binäre Zahlen umgewandelt (Schrittfolge: Lesen der Ziffer, Subtrahieren von ASCII-Null, Zahl mal zehn, Addieren zur Zahl).

Das SRAM zeigt das Jahr 0x076C (= dezimal 1900) sowie den Monat und den Tag als Hexadezimalzahl an.

4.4.5 Berechnen des Datumswertes

Berechnung des Datumswertes Das ist nun das Ergebnis der Umwandlung der binären Zeitdaten in den Datumswert (hier nach LibreOffice-Manier) mittels der Routine Date2Val. Am Ende dieser Zeile ist der berechnete Datumswert 0x0002 zu sehen, was offenkundig korrekt ist und bei der Überprüfung nicht beanstandet wurde. Binärumwandlung, Datumswertberechnung und Überprüfung dauern zusammen etwa 400 µs bei 1 MHz Takt, also nicht allzu lange.

Bei der Berechnung des Datumswerts wird folgendermaßen verfahren. Das 16-Bit-Ergebnis im Registerpaar R1:R0 wird zunächst auf zwei gesetzt. Dann wird der Tages-Wert um Eins vermindert und hinzuaddiert. Als Zweites wird der Monat-Wert um Eins vermindert. Wenn dabei Null herauskommt, ist es ein Januar und es wird nichts addiert.

Monatstabelle Ist Monat minus Eins nicht Null, dann wird aus der links abgebildeten Monatstabelle die zugehörige Anzahl an Tagen herausgelesen. Da diese Tabelle auch noch (bei der Rückumwandlung) als Monatstabelle dient, liefert sie einen Tag mehr als die akkumulierten Tage, welcher natürlich in dieser Routine wieder abgezogen werden muss.

Zuletzt ist das Jahr dran. Von dem Jahres-Wert werden dazu 1900 abgezogen und die Anzahl Jahre mit 365 multipliziert und jeweils zum Ergebnis dazugezählt. Das dauert im Höchstfall (bei 179 Jahren) 365 µs und ist daher noch lange kein Grund, auf Hardware- Multiplikation umzustellen (was im hier verwendeten tiny24 ehedem nicht laufen würde).

Zu allerletzt sind die Schaltjahre dran. Auch hier werden wieder Spezialregeln gebraucht, weil das Jahr 1900 entgegen der sonstigen Regel kein Schaltjahr war. Ist das 8-Bit-Jahr größer als Null, das aktuelle Jahr ein Schaltjahr (8-Bit-Jahr UND 0x03 = Null) und ist der aktuelle Monat drei oder höher, dann wird von den Jahren Eins abgezogen. Die werden dann durch vier geteilt und die ungerundete Anzahl Schaltjahre zum Ergebnis hinzu addiert.

Die Routine Date2Val verwendet zusätzlich zum Registerpaar Y (Zeiger in die Flash-Tabelle) folgende Resourcen:
  1. Register rmp (R16) als Vielzweckregister,
  2. Register R0 und R1 als 16-Bit-Register,
  3. Registerpaar X mit XH und XL als 16-Bit-Register,
  4. Registerpaar Z mit ZH und ZL als 16-Bit-Register und für das Lesen aus der Flash-Tabelle (mit Y als Zwischenspeicher).

4.4.6 Datumswert in Datum rückwandeln

Rueckumwandlung Datumswert Für diese Aufgabe gibt es die Routine Val2Date. Sie liest den Datumswert (hier 0x0002) und vermindert ihn um zwei. Kommt dabei Null heraus (wie in diesem Fall), dann handelt es sich um den 01.01.1900. Wenn nicht, multipliziert die Routine ihn mit vier und teilt ihn dann durch 4*365,25 = 1.461. Die dabei herauskommende (ungerundete) 8-Bit-Zahl ist das Jahr (hier in Zelle $0079).

Dieses 8-Bit-Jahr wird dann mit 1.461 multipliziert und vom vervierfachten Datumswert abgezogen, so dass wir die Tage der Monate und der Einzeltage erhalten. Das Ergebnis wird nun monatsweise mit den Einträgen der Monatstabelle verglichen. Beim Vergleich wird berücksichtigt, ob das Jahr ein Schaltjahr war (T-Flagge = 1) und ob schon der März erreicht ist (Anzahl der nach Jahresabzug verbliebenen Tage größer als 61, in diesem Fall wird beim Vergleich mit der Monatstabelle ein weiterer Tag abgezogen). Das Gehangel durch die Monattabelle ergibt dann den Monat. Und, wenn man diesen Eintrag in der Monatstabelle, minus Eins, auch noch abzieht, kriegt man den Tag heraus.

Die Routine Val2Date verwendet folgende Resourcen:
  1. Register rmp (R16) und rmp2 (R17) als Vielzweckregister,
  2. Register R0 und R1 als 16-Bit-Register,
  3. Registerpaar X mit XH und XL als 16-Bit-Register,
  4. Registerpaar Z mit ZH und ZL, ZL auch bei Multiplikation mit vier als zweites MSB,
  5. T-Flagge im SREG (als Schaltjahr-Flagge).
Im Maximalfall braucht diese Routine 513 µs, also auch nicht allzu lange, so dass der Einsatz eines eventuell vorhandenen Hardware-Multiplizierers hier ebenfalls kaum zu rechtfertigen wäre.

4.4.7 Alle Durchläufe fertig und ohne Fehler

Alle Daten komplett geprueft Wenn die Software festgestellt hat, dass bei der Hin- und Rückumwandlung aller Testdaten kein Fehler auftrat, dann erscheint diese Meldung. Wer mag, kann beim Durchsimulieren einen Breakpoint auf die SLEEP-Anweisung im Quelltext setzen, der Simulator hält dann automatisch an dieser Stelle an.

4.4.8 Und auch noch die Uhrzeit zum Datum hinzu addieren

Die Uhrzeit ist in Tabellenkalkulationen die Summe aus Stunde / 24 plus Minute / 24 / 60 plus Sekunde / 24 / 60 / 60 und unterteilt einen ganzen Tag daher in solche kleinere Häppchen.

Da wird dann aus dem "18.10.1933 02:57:46" die "12345,12345678" und wieder rückwärts. Da dafür keine Ganzzahlen, sondern Fließkomma-Zahlen zum Einsatz kommen und wie die in binär gehen, kann man generell hier lernen oder, wenn man schon Fließkomma kann, direkt zur Zeitberechnung hier gehen.

5 Zeit und Uhrzeit im AVR realisieren

Zeit und Datum um eine Sekunde erhoehen Mit diesem Handwerkszeug kann man sich an die Programmierung von Datum und Uhrzeit machen. Als Format ist binär gewählt, die Lokalisierung im SRAM-Puffer zeigt die Anordnung oben. Nur die jeweils geänderten Werte werden auf der LCD aktualisiert. Zu Beginn ist noch ein Schalter eingebaut, der die Uhrzeit- und Datumserhöhung unterbindet, wenn daran manuell gearbeitet wird (mit Eingabetasten).

Das sieht alles sehr kompliziert aus, ist aber immer dasselbe nur mit kleinen Varianten. Hier nur die Erhöhungs-Routine ohne die LCD-Ausgaben.

;
; Sekunde erhoehen
;
IncSec:
  ldi ZH,High(DatumZeit)
  ldi ZL,Low(DatumZeit)
  ldd rmp,Z+6 ; Sekunden
  inc rmp
  std Z+6,rmp
  cpi rmp,60
  brcs IncSecRet
  clr rmp
  std Z+6,rmp
  ldd rmp,Z+5 ; Minuten
  inc rmp
  std Z+5,rmp
  cpi rmp,60
  brcs IncSecRet
  clr rmp
  std Z+5,rmp
  ldd rmp,Z+4 ; Stunden
  inc rmp
  std Z+4,rmp
  cpi rmp,24
  brcs IncSecRet
  clr rmp
  std Z+4,rmp
  ld rmp,Z ; Wochentage
  inc rmp
  st Z,rmp
  cpi rmp,7
  brcs IncDay
  clr rmp
  st Z,rmp
IncDay:
  rcall DaysOfMonth ; Tage
  inc rmp
  mov rData,rmp
  ldd rmp,Z+1
  inc rmp
  std Z+1,rmp
  cp rmp,R0
  brcs IncSecRet
  ldi rmp,1
  std Z+1,rmp
  ldd rmp,Z+2 ; Monate
  inc rmp
  std Z+2,rmp
  cpi rmp,13
  brcs IncSecRet
  ldi rmp,1
  std Z+2,rmp
  ldd rmp,Z+3 ; Jahre
  inc rmp
  cpi rmp,100
  std Z+3,rmp
  brcs IncSecRet
  clr rmp
  std Z+3,rmp
IncSecRet:
  ret

Auch das sollte bewältigbarer Code sein, wenn man die wenigen vorkommenden Instruktionen mal verstanden hat.

Sekundenerhoehung am 31.12.2017 Auch hier wieder ein paar Tests. Zuerst der Jahreswechsel am 31.12.2017. Die oberste Reihe an $0060 zeigt die Anordnung der Binärbytes an. In der Zeile darunter steht das Ausgangsdatum binär und darunter das Enddatum binär nach der Erhöhung. Die Ausgaben auf der LCD stehen darunter und sind im Textbereich der Anzeige im Klartext zu sehen. Der Jahreswechsel funktioniert einwandfrei.

In der untersten Reihe ist noch der Stapel zu sehen.

Sekundenerhoehung ohne Schaltjahr Das ist die Sekundenerhöhung am 28.02.2019, kein Schaltjahr.

Sekundenerhoehung im Schaltjahr Und das die Sekundenerhöhung am 20.02.2020, einem Schaltjahr. Alles korrekt.

Viel Erfolg beim Selbermachen.

Und wer den Wochentag aus dem Datum ermitteln muss, wird hier fündig und kriegt ein fertiges Assembler-Programm dafür hier.

Seitenanfang Sekunden Uhrzeit-Formate Datum Ganzzahl-Datum


©2018-2023 by http://www.avr-asm-tutorial.net