AVR-Tutorial: 7-Segment-Anzeige

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

Die Ausgabe von Zahlenwerten auf ein Text-LCD ist sicherlich das Nonplusultra, aber manchmal liegen die Dinge sehr viel einfacher. Um beispielsweise eine Temperatur anzuzeigen ist ein LCD etwas Overkill. In solchen Fällen kann die Ausgabe auf ein paar 7-Segment-Anzeigen gemacht werden. Außerdem haben 7-Segment-Anzeigen einen ganz besonderen Charme :-)

Typen von 7-Segment-Anzeigen

Eine einzelne 7-Segment-Anzeige besteht aus sieben (mit Dezimalpunkt acht) einzelnen LEDs in einem gemeinsamen Gehäuse. Aus praktischen Gründen wird einer der beiden Anschlüsse jeder LED mit den gleichen Anschlüssen der anderen LEDs verbunden und gemeinsam aus dem Gehäuse herausgeführt. Das spart Pins am Gehäuse und später bei der Ansteuerung. Dementsprechend spricht man von Anzeigen mit gemeinsamer Anode (engl. common anode) bzw. gemeinsamer Kathode (engl. common cathode).

Interne Verschaltung der 7-Segment-Anzeigen


Eine einzelne 7-Segment-Anzeige

Schaltung

Eine einzelne 7-Segment-Anzeige wird nach dem folgenden Schema am Port D des ATmega8 angeschlossen. Port D wurde deshalb gewählt, da er am ATmega8 als einziger Port aus den vollen 8 Bit besteht. Die 7-Segment-Anzeige hat neben den Segmenten a bis g eine gemeinsame Anode CA sowie einen Dezimalpunkt dp (siehe folgende Abbildung).

Ansteuerung einer einzelnen 7-Segment-Anzeige

Welcher Pin an der Anzeige welchem Segment (a…g) bzw. dem Dezimalpunkt entspricht, wird am besten dem Datenblatt zur Anzeige entnommen. Hat man kein Datenblatt, dann kann man auch empirisch die Pinbelegung feststellen, indem man mit einer 5-Volt-Quelle und einem 1-kΩ-Widerstand einfach probeweise alle Pins „abklappert“. 1 kΩ deswegen, damit man im Zweifelsfall Low-Current-LEDs (für 2 mA) nicht überfordert. Eine 7-Segment-Anzeige, die auf die üblichen 10 bis 20 mA ausgelegt ist, wird damit immer noch sichtbar leuchten, wenn auch schwach. Aber mehr braucht es ja auch nicht, um die Pinbelegung feststellen zu können.

Pinbelegung einer 7-Segment-Anzeige

Im Folgenden wird von oben abgebildeter Segmentbelegung ausgegangen. Wird eine andere Belegung genutzt, dann ist das problemlos möglich, jedoch müsste das in der Programmierung (→Codetabelle) berücksichtigt werden.

Da eine 7-Segment-Anzeige konzeptionell sieben einzelnen LEDs entspricht, ergibt sich im Prinzip keine Änderung in der Ansteuerung einer derartigen Anzeige im Vergleich zur LED-Ansteuerung, wie sie im Kapitel IO-Grundlagen gezeigt wird. Genau wie bei den einzelnen LEDs wird eine davon eingeschaltet, indem der zugehörige Port-Pin auf 0 gesetzt wird. Aber anders als bei einzelnen LEDs möchte man mit einer derartigen Anzeige eine Ziffernanzeige erhalten. Dazu ist es lediglich notwendig, für eine bestimmte Ziffer die richtigen LEDs einzuschalten.


Codetabelle

Die Umkodierung von einzelnen Ziffern in ein bestimmtes Ausgabemuster kann über eine sogenannte Codetabelle geschehen: Die auszugebende Ziffer wird als Offset zum Anfang dieser Tabelle aufgefasst und aus der Tabelle erhält man ein Byte (Code), welches direkt auf den Port ausgegeben werden kann und das entsprechende Bitmuster enthält, sodass die für diese Ziffer notwendigen LEDs ein- bzw. ausgeschaltet sind.

Darstellung der Ziffer „3“
Beispiel
Um die Ziffer 3 anzuzeigen, müssen auf der Anzeige die Segmente a, b, c, d und g aufleuchten. Alle anderen Segmente sollen dunkel sein.

Aus dem Anschlußschema ergibt sich, dass die dazu notwendige Ausgabe am Port binär 10110000 lauten muss. Untersucht man dies für alle Ziffern, so ergibt sich folgende Tabelle:

    .db  0b11000000     ; 0: a, b, c, d, e, f
    .db  0b11111001     ; 1: b, c
    .db  0b10100100     ; 2: a, b, d, e, g
    .db  0b10110000     ; 3: a, b, c, d, g
    .db  0b10011001     ; 4: b, c, f, g
    .db  0b10010010     ; 5: a, c, d, f, g
    .db  0b10000010     ; 6: a, c, d, e, f, g
    .db  0b11111000     ; 7: a, b, c
    .db  0b10000000     ; 8: a, b, c, d, e, f, g
    .db  0b10010000     ; 9: a, b, c, d, f, g

Programm

Das Testprogramm stellt nacheinander die Ziffern 0 bis 9 auf der 7-Segment-Anzeige dar. Die jeweils auszugebende Zahl steht im Register count und wird innerhalb der Schleife um jeweils 1 erhöht. Hat das Register den Wert 10 erreicht, so wird es wieder auf 0 zurückgesetzt. Nach der Erhöhung folgt eine Warteschleife, welche dafür sorgt, dass bis zur nächsten Ausgabe eine gewisse Zeit vergeht. Normalerweise benutzt man keine derartig langen Warteschleifen, aber hier geht es ja nicht ums Warten, sondern um die Ansteuerung einer 7-Segment-Anzeige. Einen Timer dafür zu benutzen wäre zunächst zuviel Aufwand.

Die eigentliche Ausgabe und damit der in diesem Artikel interessante Teil findet jedoch direkt nach dem Label loop statt. Die bereits bekannte Codetabelle wird mittels .db-Direktive (define byte) in den Flash-Speicher gelegt. Der Zugriff darauf erfolgt über den Z-Pointer und den Befehl lpm. Zusätzlich wird vor dem Zugriff noch der Wert des Registers count und damit der aktuelle Zählerwert zum Z-Pointer addiert.

Beachtet werden muss nur, dass der Zählerwert verdoppelt werden muss. Dies hat folgenden Grund: Wird die Tabelle so wie hier gezeigt mittels einzelnen .db-Anweisungen aufgebaut, so fügt der Assembler sogenannte Padding-Bytes zwischen die einzelnen Bytes ein, damit jede .db-Anweisung auf einer geraden Speicheradresse liegt. Dies ist eine direkte Folge der Tatsache, dass der Flash-Speicher wortweise (16 Bit) und nicht byteweise (8 Bit) organisiert ist. Da aber somit von einem .db in der Tabelle zum nächsten .db eine Differenz von 2 Bytes vorliegt, muss dies in der Berechnung berücksichtigt werden. Im zweiten Beispiel auf dieser Seite wird dies anders gemacht. Dort wird gezeigt, wie man durch eine andere Schreibweise der Tabelle das Erzeugen der Padding-Bytes durch den Assembler verhindern kann.

Aus dem gleichen Grund wird auch der Z-Pointer mit dem 2-fachen der Startadresse der Tabelle geladen: Die Startadresse wird vom Assembler in wortweiser Adressierung eingesetzt, lpm möchte die Zugriffsadresse aber als Byteadresse angegeben haben.

Interessant ist auch, dass in der Berechnung ein Register benötigt wird, welches den Wert 0 enthält. Dies deshalb, da es im AVR keinen Befehl gibt, der eine Konstante mit gleichzeitiger Berücksichtigung des Carry-Bits addieren kann. Daher muss diese Konstante zunächst in ein Register geladen werden und erst dann kann die Addition mithilfe dieses Registers vorgenommen werden. Das Interessante daran ist nun, dass dieser Umstand in sehr vielen Programmen vorkommt und es sich bei der Konstanten in der überwiegenden Mehrzahl der Fälle um die Konstante 0 handelt. Viele Programmierer reservieren daher von vorne herein ein Register für diesen Zweck und nennen es das Zero-Register. Sinnvollerweise legt man dieses Register in den Bereich r0…r15, da diese Register etwas zweitklassig sind (ldi, cpi etc. funktionieren nicht damit).

.include "m8def.inc"

.def zero  = r1
.def count = r16
.def temp1 = r17

.org 0x0000
           rjmp    main                ; Reset Handler

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

           ldi     temp1, $FF        ; die Anzeige hängt am Port D
           out     DDRD, temp1       ; alle Pins auf Ausgang

           ldi     count, 0          ; und den Zähler initialisieren
           mov     zero, count

loop:
           ldi     ZL, LOW(Codes*2)  ; die Startadresse der Tabelle in den
           ldi     ZH, HIGH(Codes*2) ; Z-Pointer laden

           mov     temp1, count      ; die wortweise Adressierung der Tabelle
           add     temp1, count      ; berücksichtigen

           add     ZL, temp1         ; und ausgehend vom Tabellenanfang
           adc     ZH, zero          ; die Adresse des Code-Bytes berechnen

           lpm                       ; dieses Code-Byte in das Register r0 laden

           out     PORTD, r0         ; und an die Anzeige ausgeben

           inc     count             ; den Zähler erhöhen, wobei der Zähler
           cpi     count, 10         ; immer nur von 0 bis 9 zählen soll
           brne    wait
           ldi     count, 0

wait:      ldi     r17, 10           ; und etwas warten, damit die Ziffer auf
wait0:     ldi     r18, 0            ; der Anzeige auch lesbar ist, bevor die
wait1:     ldi     r19, 0            ; nächste Ziffer gezeigt wird
wait2:     dec     r19
           brne    wait2
           dec     r18
           brne    wait1
           dec     r17
           brne    wait0

           rjmp    loop              ; auf zur nächsten Ausgabe

Codes:                               ; Die Codetabelle für die Ziffern 0 bis 9:
                                     ; Sie regelt, welche Segmente für eine bestimmte
                                     ; Ziffer eingeschaltet werden müssen.
                                     ;
           .db     0b11000000        ; 0: a, b, c, d, e, f
           .db     0b11111001        ; 1: b, c
           .db     0b10100100        ; 2: a, b, d, e, g
           .db     0b10110000        ; 3: a, b, c, d, g
           .db     0b10011001        ; 4: b, c, f, g
           .db     0b10010010        ; 5: a, c, d, f, g
           .db     0b10000010        ; 6: a, c, d, e, f, g
           .db     0b11111000        ; 7: a, b, c
           .db     0b10000000        ; 8: a, b, c, d, e, f, g
           .db     0b10010000        ; 9: a, b, c, d, f, g

Mehrere 7-Segment-Anzeigen (Multiplexen)

Mit dem bisherigen Vorwissen könnte man sich jetzt daran machen, auch einmal drei oder vier Anzeigen mit dem ATmega8 anzusteuern. Leider gibt es da ein Problem, denn für eine Anzeige sind acht Portpins notwendig – vier Anzeigen würden demnach 32 Portpins benötigen. Die hat der ATmega8 aber nicht. Dafür gibt es aber mehrere Auswege. Schieberegister sind bereits in einem vorhergehenden Kapitel beschrieben worden. Damit könnte man sich ganz leicht die benötigten 32 Ausgangsleitungen mit nur 3 Portpins erzeugen. Das Prinzip der Ansteuerung unterscheidet sich in nichts von der Ansteuerung einer einzelnen 7-Segment-Anzeige, lediglich die Art und Weise, wie die „Ausgangspins“ zu ihren Werten kommen ist anders und durch die Verwendung von Schieberegistern vorgegeben. An dieser Stelle soll aber eine andere Variante der Ansteuerung gezeigt werden. Im Folgenden werden wir uns daher das Multiplexen einmal näher ansehen.

Ansteuerung von vier 7-Segment-Anzeigen per Zeit-Multiplex

Multiplexen bedeutet, dass nicht alle vier Anzeigen gleichzeitig eingeschaltet sind, sondern immer nur eine für eine kurze Zeit. Geschieht der Wechsel zwischen den Anzeigen schneller als wir Menschen das wahrnehmen können, so erscheinen uns alle vier Anzeigen gleichzeitig in Betrieb zu sein, obwohl immer nur eine kurzzeitig aufleuchtet. Dabei handelt es sich praktisch um einen Sonderfall einer LED-Matrix mit nur einer Zeile. Die vier Anzeigen können sich dadurch die einzelnen Segmentleitungen teilen und alles, was benötigt wird, sind 4 zusätzliche Steuerleitungen für die 4 Anzeigen, mit denen jeweils eine Anzeige eingeschaltet wird. Dieses Ein-/Ausschalten wird mit einem pnp-Transistor in der Versorgungsspannung jeder Anzeige realisiert, die vom ATmega8 am Port C angesteuert werden.

Ein Aspekt dieser Ansteuerungsart ist die Multiplexfrequenz, also ein kompletter Zyklus des Weiterschaltens von einer Anzeige zur nächsten. Sie muss hoch genug sein, um ein Flimmern der Anzeige zu vermeiden. Das menschliche Auge ist träge, im Kino reichen 24 Bilder pro Sekunde, beim Fernseher sind es 50. Um auf der sicheren Seite zu sein, dass auch Standbilder ruhig wirken, soll jedes Segment mit mindestens 100 Hz angesteuert werden, es also höchstens alle 10 ms angeschaltet ist. In Ausnahmefällen können aber selbst 100 Hz noch flimmern, z. B. wenn die Anzeige schnell bewegt wird oder wenn es zu Interferenzerscheinungen mit künstlichen Lichtquellen kommt, die mit Wechselstrom betrieben werden.

Bei genauerer Betrachtung fällt auch auf, dass die vier Anzeigen nicht mehr ganz so hell leuchten wie die eine einzelne Anzeige ohne Multiplexen. Bei wenigen Anzeigen ist dies praktisch kaum sichtbar; erst bei höherer Anzahl von Anzeigen wird es deutlich. Um dem entgegenzuwirken, lässt man pro Segment einfach mehr Strom fließen; bei LEDs dürfen dann 20 mA überschritten werden. Als Faustregel gilt, dass der n-fache Strom für die (1/n)-fache Zeit fließen darf. Details finden sich im Datenblatt unter den Punkten Peak Current (Spitzenstrom) und Duty Cycle (Tastverhältnis).

Allerdings gibt es noch ein anderes Problem, wenn insgesamt zu viele Anzeigen gemultiplext werden. Die Pulsströme durch die LEDs werden einfach zu hoch. Die meisten LEDs kann man bis 8:1 multiplexen, manchmal auch bis 16:1. Hier fließt aber schon ein Pulsstrom von 320 mA (16 × 20 mA), was nicht mehr ganz ungefährlich ist. Energie lässt sich durch Multiplexen nicht sparen, denn die verbrauchte Leistung ändert sich beim n-fachen Strom für 1/n der Zeit nicht. Kritisch wird es aber, wenn das Multiplexen deaktiviert wird (Ausfall der Ansteuerung durch Hardware- oder Softwarefehler) und der n-fache Strom dauerhaft durch eine Segment-LED fließt. Bei 320 mA werden die meisten LEDs innerhalb von Sekundenbruchteilen zerstört. Hier muss sichergestellt werden, dass sowohl Programm (Breakpoint im Debugger) als auch Schaltung (Reset, Power-On, Watchdog, siehe Forumsthread [1]) diesen Fall verhindern. Prinzipiell sollte man immer den Pulsstrom und die Multiplexfrequenz einmal überschlagen, bevor der Lötkolben angeworfen wird.

Sollten die Anzeigen zu schwach leuchten, wie bereits beschrieben, können die Ströme durch die Anzeigen erhöht werden. Dazu werden die 330-Ω-Widerstände kleiner gemacht. Da hier 4 Anzeigen gemultiplext werden, würden sich Widerstände in der Größenordnung von 100 Ω anbieten. Auch muss dann der Basiswiderstand der Transistoren verkleinert werden. Zudem muss berücksichtigt werden, dass der ATmega8 in Summe an seinen Portpins und an den Versorgungsleitungen nicht beliebig viel Strom liefern oder abführen kann. Auch hier ist daher wieder ein Blick ins Datenblatt angebracht und gegebenenfalls muss wieder ein Transistor als Verstärker eingesetzt werden (oder eben fertige Treiberstufen in IC-Form).

Programm

Das folgende Programm zeigt eine Möglichkeit zum Multiplexen. Dazu wird ein Timer benutzt, der in regelmäßigen Zeitabständen einen Overflow-Interrupt auslöst. Innerhalb der Overflow-Interrupt-Routine wird

  • die momentan erleuchtete Anzeige abgeschaltet,
  • das Muster für die nächste Anzeige am Port D ausgegeben und
  • die nächste Anzeige durch eine entsprechende Ausgabe am Port C eingeschaltet.

Da Interruptfunktionen kurz sein sollten, holt die Interrupt-Routine das auszugebende Muster für jede Stelle direkt aus dem SRAM, wo sie die Ausgabefunktion hinterlassen hat. Dies hat 2 Vorteile:

  • Zum einen braucht die Interrupt-Routine die Umrechnung einer Ziffer in das entsprechende Bitmuster nicht selbst machen.
  • Zum anderen ist die Anzeigefunktion dadurch unabhängig von dem, was angezeigt wird. Die Interrupt-Routine gibt das Bitmuster so aus, wie sie es aus dem SRAM liest. Werden die SRAM-Zellen mit geeigneten Bitmustern gefüllt, können so auch einige Buchstaben oder Balkengrafik oder auch kleine Balken-Animationen abgespielt werden. Insbesondere letzteres sieht man manchmal bei Consumer-Geräten kurz nach dem Einschalten des Gerätes, um eine Art Defektkontrolle zu ermöglichen oder einfach nur als optischer Aufputz.

Die Funktion out_number ist in einer ähnlichen Form auch schon an anderer Stelle vorgekommen: Sie verwendet die Technik der fortgesetzten Subtraktionen, um eine Zahl in einzelne Ziffern zu zerlegen. Sobald jede Stelle feststeht, wird über die Codetabelle das Bitmuster aufgesucht, welches für die Interrupt-Funktion an der entsprechenden Stelle im SRAM abgelegt wird.

Achtung: Anders als bei der weiter oben gezeigten Variante wurde die Codetabelle ohne Padding-Bytes angelegt. Dadurch ist es auch nicht notwendig, derartige Padding-Bytes in der Programmierung zu berücksichtigen.

Der Rest ist wieder die übliche Portinitialisierung, Timerinitialisierung und eine einfache Anwendung, indem ein 16-Bit-Zähler laufend erhöht und über die Funktion out_number ausgegeben wird. Wie schon im ersten Beispiel wurde auch hier kein Aufwand getrieben: Zähler um 1 erhöhen und mit Warteschleifen eine gewisse Verzögerungszeit einhalten. In einer realen Applikation wird man das natürlich nicht so machen, sondern ebenfalls einen Timer für diesen Teilaspekt der Aufgabenstellung einsetzen.

Weiterhin ist auch noch interessant: Die Overflow-Interrupt-Funktion ist wieder so ausgelegt, dass sie völlig transparent zum restlichen Programm ablaufen kann. Dies bedeutet, dass alle verwendeten Register beim Aufruf der Interrupt-Funktion gesichert und beim Verlassen wiederhergestellt werden. Dadurch ist man auf der absolut sicheren Seite, hat aber den Nachteil, etwas Rechenzeit für manchmal unnötige Sicherungs- und Aufräumarbeiten zu „verschwenden“. Stehen genug freie Register zur Verfügung, dann wird man natürlich diesen Aufwand nicht treiben, sondern ein paar Register ausschließlich für die Zwecke der Behandlung der 7-Segment-Anzeige abstellen und sich damit den Aufwand der Registersicherung sparen (mit Ausnahme von SREG natürlich!).

.include "m8def.inc"

.def temp  = r16
.def temp1 = r17
.def temp2 = r18

.org 0x0000
           rjmp    main                ; Reset Handler
.org OVF0addr
           rjmp    multiplex

;
;********************************************************************
; Die Multiplexfunktion
;
; Aufgabe dieser Funktion ist es, bei jedem Durchlauf eine andere Stelle
; der 7-Segment-Anzeige zu aktivieren und das dort vorgesehene Muster
; auszugeben.
; Die Funktion wird regelmäßig in einem Timer-Interrupt aufgerufen.
;
; Verwendet werden 6 Bytes im SRAM (siehe .DSEG weiter unten im Programm)
;    NextDigit    Bitmuster für die Aktivierung des nächsten Segments
;    NextSegment  Nummer des nächsten aktiven Segments
;    Segment0     Ausgabemuster für Segment 0
;    Segment1     Ausgabemuster für Segment 1
;    Segment2     Ausgabemuster für Segment 2
;    Segment3     Ausgabemuster für Segment 3
;
; NextSegment ist einfach nur ein Zähler, der bei jedem Aufruf der Funktion
;   um 1 weitergezählt wird und bei 4 wieder auf 0 zurückgestellt wird.
;   Er wird benutzt, um ausgehend von der Adresse von Segment0 auf das
;   jeweils als nächstes auszugebende Muster aus Segement0, Segment1,
;   Segment2 oder Segment3 zuzugreifen. Die Adresse von Segment0 wird
;   in den Z-Pointer geladen und NextSegment addiert.
;
; NextDigit enthält das Bitmuster, welches direkt an den Port C ausgegeben
;   wird und den jeweils nächsten Transistor durchschaltet.
;   Dazu enthält NextDigit am Anfang das Bitmuster 0b11111110, welches
;   bei jedem Aufruf um 1 Stelle nach links verschoben wird. Beim nächsten
;   Aufruf findet sich dann 0b11111101 in NextDigit, dann 0b11111011 und
;   zu guter Letzt 0b11110111. Wird beim nächsten Schiebevorgang 0b11101111
;   erkannt, dann wird NextDigit auf 0b11111110 zurückgesetzt (und NextSegment
;   auf 0) und das ganze Spiel beginnt beim nächsten Funktionsaufruf wieder
;   von vorne.
;
; Segment0 .. 3  enthalten die auszugebenden Bitmuster für die Einzel-LEDs
;   der jeweiligen 7-Segment-Anzeigen. Diese Muster werden so wie sie sind
;   einfach ausgegeben. Soll eine der Anzeigen etwas bestimmtes anzeigen
;   (z. B. eine Ziffer), so obliegt es dem Code, der Werte in diese SRAM-
;   Zellen schreibt, das dafür zuständige Bitmuster dort zu hinterlassen.
;   Die Multiplexroutine kümmert sich nicht darum, dass diese Bitmuster
;   in irgendeiner Art und Weise sinnvoll (oder was man dafür halten könnte)
;   sind.
;
; veränderte CPU-Register: keine
;
multiplex:
           push    temp                ; Alle verwendeten Register sichern
           push    temp1
           in      temp, SREG
           push    temp
           push    ZL
           push    ZH

           ldi     temp1, 0            ; Die 7 Segment ausschalten
           out     PORTC, temp1

                                       ; Das Muster für die nächste Stelle ausgeben
                                       ; Dazu zunächst mal berechnen, welches Segment als
                                       ; nächstes ausgegeben werden muss
           ldi     ZL, LOW( Segment0 )
           ldi     ZH, HIGH( Segment0 )
           lds     temp, NextSegment
           add     ZL, temp
           adc     ZH, temp1

           ld      temp, Z             ; das entsprechende Muster holen und ausgeben
           out     PORTD, temp

           lds     temp1, NextDigit    ; Und die betreffende Stelle einschalten
           out     PORTC, temp1

           lds     temp, NextSegment
           inc     temp
           sec
           rol     temp1               ; beim nächsten Interrupt kommt reihum die
           cpi     temp1, 0b11101111   ; nächste Stelle dran.
           brne    multi1
           ldi     temp, 0
           ldi     temp1, 0b11111110

multi1:
           sts     NextSegment, temp
           sts     NextDigit, temp1

           pop     ZH                  ; die gesicherten Register wiederherstellen
           pop     ZL
           pop     temp
           out     SREG, temp
           pop     temp1
           pop     temp
           reti
;
;************************************************************************
; 16-Bit-Zahl aus dem Registerpaar temp (=low), temp1 (=high) ausgeben
; Die Zahl muss kleiner als 10000 sein, da die Zehntausenderstelle
; nicht berücksichtigt wird.
; Werden mehr als vier 7-Segment-Anzeigen eingesetzt, dann muss dies
; natürlich auch hier berücksichtigt werden.
;
out_number:
           push    temp
           push    temp1

           ldi     temp2, -1            ; Die Tausenderstelle bestimmen
_out_tausend:
           inc     temp2
           subi    temp, low(1000)      ; -1000
           sbci    temp1, high(1000)
           brcc    _out_tausend

           ldi     ZL, low(2*Codes)     ; für diese Ziffer das Codemuster für
           ldi     ZH, high(2*Codes)    ; die Anzeige in der Codetabelle nachschlagen
           add     ZL, temp2

           lpm
           sts     Segment3, r0         ; und dieses Muster im SRAM ablegen
                                        ; die OvI-Routine sorgt dann für die Anzeige
           ldi     temp2, 10

_out_hundert:                           ; die Hunderterstelle bestimmen
           dec     temp2
           subi    temp, low(-100)      ; +100
           sbci    temp1, high(-100)
           brcs    _out_hundert

           ldi     ZL, low(2*Codes)     ; wieder in der Codetabelle das entsprechende
           ldi     ZH, high(2*Codes)    ; Muster nachschlagen
           add     ZL, temp2

           lpm
           sts     Segment2, r0         ; und im SRAM hinterlassen

           ldi     temp2, -1
_out_zehn:                              ; die Zehnerstelle bestimmen
           inc     temp2
           subi    temp, low(10)        ; -10
           sbci    temp1, high(10)
           brcc    _out_zehn

           ldi     ZL, low(2*Codes)     ; wie gehabt: Die Ziffer in der Codetabelle
           ldi     ZH, high(2*Codes)    ; aufsuchen
           add     ZL, temp2

           lpm
           sts     Segment1, r0         ; und entsprechend im SRAM ablegen

_out_einer:                             ; bleiben noch die Einer
           subi    temp, low(-10)       ; +10
           sbci    temp1, high(-10)

           ldi     ZL, low(2*Codes)     ; ... Codetabelle
           ldi     ZH, high(2*Codes)
           add     ZL, temp

           lpm
           sts     Segment0, r0         ; und ans SRAM ausgeben

           pop     temp1
           pop     temp

           ret
;
;**************************************************************************
;
main:
           ldi     temp, HIGH(RAMEND)
           out     SPH, temp
           ldi     temp, LOW(RAMEND)  ; Stackpointer initialisieren
           out     SPL, temp
;                                     die Segmenttreiber initialisieren
           ldi     temp, $FF
           out     DDRD, temp
;                                     die Treiber für die einzelnen Stellen
           ldi     temp, $0F
           out     DDRC, temp
;                                     Initialisieren der Steuerung für die
;                                     Interrupt-Routine
           ldi     temp, 0b11111110
           sts     NextDigit, temp

           ldi     temp, 0
           sts     NextSegment, temp

           ldi     temp, (1 << CS01) | (1 << CS00)
           out     TCCR0, temp

           ldi     temp, 1 << TOIE0
           out     TIMSK, temp

           sei

           ldi     temp, 0
           ldi     temp1, 0

loop:
           inc     temp
           brne    _loop
           inc     temp1
_loop:
           rcall    out_number

           cpi     temp, low(4000)
           brne    wait
           cpi     temp1, high(4000)
           brne    wait

           ldi     temp, 0
           ldi     temp1, 0

wait:      ldi     r21, 1
wait0:     ldi     r22, 0
wait1:     ldi     r23, 0
wait2:     dec     r23
           brne    wait2
           dec     r22
           brne    wait1
           dec     r21
           brne    wait0

           rjmp    loop

Codes:
    .db  0b11000000, 0b11111001     ; 0: a, b, c, d, e, f
                                    ; 1: b, c
    .db  0b10100100, 0b10110000     ; 2: a, b, d, e, g
                                    ; 3: a, b, c, d, g
    .db  0b10011001, 0b10010010     ; 4: b, c, f, g
                                    ; 5: a, c, d, f, g
    .db  0b10000010, 0b11111000     ; 6: a, c, d, e, f, g
                                    ; 7: a, b, c
    .db  0b10000000, 0b10010000     ; 8: a, b, c, d, e, f, g
                                    ; 9: a, b, c, d, f, g

           .DSEG
NextDigit:   .byte 1         ; Bitmuster für die Aktivierung des nächsten Segments
NextSegment: .byte 1         ; Nummer des nächsten aktiven Segments
Segment0:    .byte 1         ; Ausgabemuster für Segment 0
Segment1:    .byte 1         ; Ausgabemuster für Segment 1
Segment2:    .byte 1         ; Ausgabemuster für Segment 2
Segment3:    .byte 1         ; Ausgabemuster für Segment 3

Forenbeiträge