AVR-Tutorial: 7-Segment-Anzeige: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
(Ursprung wiederhergestellt)
K (Syntaxhighlight (asm), R/G, Tippfehler, Kleinigkeiten)
 
(37 dazwischenliegende Versionen von 13 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
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-Segmentanzeigen gemacht werden. Ausserdem haben 7-Segmentanzeigen einen ganz besonderen Charme :-)
Die Ausgabe von Zahlenwerten auf ein [[AVR-Tutorial: LCD|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==
== Typen von 7-Segment-Anzeigen ==


Eine einzelne 7-Segmentanzeige besteht aus sieben (mit Dezimalpunkt acht) einzelnen [[LED]]s in einem gemeinsamen Gehäuse. Aus praktischen Gründen wird einer der beiden Anschlüsse jeder LED mit den gleichen Anschlüssen der anderen LED 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).
Eine einzelne 7-Segment-Anzeige besteht aus sieben (mit Dezimalpunkt acht) einzelnen [[LED]]s 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'').


[[Bild:Tut_7_Seg_00.gif | framed | center | Interne Verschaltung der 7-Segmentanzeigen]]
[[Datei:Tut_7_Seg_00.gif |center |framed |Interne Verschaltung der 7-Segment-Anzeigen]]
<br clear="all" />
{{Clear}}


==Eine einzelne 7-Segment Anzeige==
== Eine einzelne 7-Segment-Anzeige ==


===Schaltung===
=== Schaltung ===


[[Bild:Tut_7_Seg_02.gif | thumb| left | 80px| Pin-<br/>belegung einer 7-Segment-<br/>anzeige]]
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).


[[Bild:Tut_7_Seg_01.gif | thumb | right |240px| Ansteuerung einer einzelnen 7-Segmentanzeige]]
[[Datei:Tut_7_Seg_01.gif |thumb |right |240px |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&nbsp;kΩ deswegen, damit man im Zweifelsfall Low-Current-LEDs (für 2&nbsp;mA) nicht überfordert. Eine 7-Segment-Anzeige, die auf die üblichen 10 bis 20&nbsp;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.


Eine einzelne 7-Segmentanzeige wird nach dem folgenden Schema am '''Port D''' des Mega8 angeschlossen. Port D wurde deshalb gewählt, da er am Mega8 als einziger Port aus den vollen 8 Bit besteht. Die 7-Segmentanzeige hat neben den Segmenten '''a''' bis '''g'''  eine gemeinsame Anode '''CA''' sowie einen Dezimalpunkt '''dp''' (siehe Bild).
[[Datei:Tut_7_Seg_02.gif |thumb |left |100px |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.


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 5V Quelle und einem 1k Widerstand einfach probweise alle Pins 'abklappert'. 1k deswegen, damit man im Zweifellsfall Low-Current LED nicht überfordert. Eine 7-Segment Anzeige, die auf die üblichen 10 bis 20mA ausgelegt ist, wird damit immer noch leuchten, wenn auch schwach. Aber mehr braucht es ja auch nicht um die Pinbelegung feststellen zu können. Im Folgenden wird von dieser Segmentbelegung ausgegangen:
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 [[AVR-Tutorial: IO-Grundlagen|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.
 
Wird eine andere Belegung genutzt dann ist das prinzipiell möglich, jedoch müsste das in der Programmierung berücksichtigt werden.
 
Da eine 7-Segmentanzeige 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 [[AVR-Tutorial: 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 LED möchte man mit einer derartigen Anzeige eine Ziffernanzeige erhalten. Dazu ist es lediglich notwendig, für eine bestimmte Ziffer die richtigen LEDs einzuschalten.


{{Clear}}
{{Clear}}


===Codetabelle===
=== Codetabelle ===


[[Bild:Tut_7_Seg_02a.gif | right | thumb | 80px | Darstellung der Ziffer "3"]]
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.
Die Umkodierung von einzelnen Ziffern in ein bestimmtes Ausgabemuster kann über eine sog. 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 LED ein- bzw. ausgeschaltet sind.


;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.
[[Datei:Tut_7_Seg_02a.gif |thumb |right |100px |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:
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:
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
     .db  0b11000000    ; 0: a, b, c, d, e, f
     .db  0b11000000    ; 0: a, b, c, d, e, f
     .db  0b11111001    ; 1: b, c
     .db  0b11111001    ; 1: b, c
Zeile 47: Zeile 46:
</syntaxhighlight>
</syntaxhighlight>


===Programm===
=== Programm ===


Das Testprogramm stellt nacheinander die Ziffern 0 bis 9 auf der 7-Segmentanzeige 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 macht man keine derartigen langen Warteschleifen, aber hier geht es ja nicht ums Warten sondern um die Ansteuerung einer 7-Segmentanzeige. Einen Timer dafür zu benutzen wäre zunächst zuviel Aufwand.
Das Testprogramm stellt nacheinander die Ziffern 0 bis 9 auf der 7-Segment-Anzeige dar. Die jeweils auszugebende Zahl steht im Register <code>count</code> 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 <i>loop</i> statt. Die bereits bekannte Codetabelle wird mittels '''.db''' Direktive ('''d'''efine '''b'''yte) in den [[Speicher#Flash-ROM | Flash-Speicher]] gelegt. Der Zugriff darauf erfolgt über den Z-Pointer und dem Befehl '''lpm'''. Zusätzlich wird vor dem Zugriff noch der Wert des Registers '''count''' und damit der aktuelle Zählerwert zum Z-Pointer addiert.
Die eigentliche Ausgabe und damit der in diesem Artikel interessante Teil findet jedoch direkt nach dem Label <code>loop</code> statt. Die bereits bekannte Codetabelle wird mittels '''<code>.db</code>'''-Direktive (''<u>d</u>efine <u>b</u>yte'') in den [[AVR-Tutorial: Speicher#Flash-ROM|Flash-Speicher]] gelegt. Der Zugriff darauf erfolgt über den Z-Pointer und den Befehl '''<code>lpm</code>'''. Zusätzlich wird vor dem Zugriff noch der Wert des Registers <code>count</code> 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 sog. '''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 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.
Beachtet werden muss nur, dass der Zählerwert verdoppelt werden muss. Dies hat folgenden Grund: Wird die Tabelle so wie hier gezeigt mittels einzelnen <code>.db</code>-Anweisungen aufgebaut, so fügt der Assembler sogenannte '''Padding-Bytes''' zwischen die einzelnen Bytes ein, damit jede <code>.db</code>-Anweisung auf einer geraden Speicheradresse liegt. Dies ist eine direkte Folge der Tatsache, dass der Flash-Speicher ''wortweise'' (16&nbsp;Bit) und nicht ''byteweise'' (8&nbsp;Bit) organisiert ist. Da aber somit von einem <code>.db</code> in der Tabelle zum nächsten <code>.db</code> eine Differenz von 2&nbsp;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 als Byteadresse angegeben haben.
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, <code>lpm</code> 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).
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 (<code>ldi</code>, <code>cpi</code> etc. funktionieren nicht damit).


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"


Zeile 68: Zeile 67:
.org 0x0000
.org 0x0000
           rjmp    main                ; Reset Handler
           rjmp    main                ; Reset Handler
;
 
main:
main:
           ldi    temp1, HIGH(RAMEND)
           ldi    temp1, HIGH(RAMEND) ; Stackpointer initialisieren
           out    SPH, temp1
           out    SPH, temp1
           ldi    temp1, LOW(RAMEND) ; Stackpointer initialisieren
           ldi    temp1, LOW(RAMEND)
           out    SPL, temp1
           out    SPL, temp1


;
           ldi    temp1, $FF        ; die Anzeige hängt am Port D
           ldi    temp1, $FF        ; die Anzeige hängt am Port D
           out    DDRD, temp1      ; alle Pins auf Ausgang
           out    DDRD, temp1      ; alle Pins auf Ausgang
;
 
           ldi    count, 0          ; und den Zähler initialisieren
           ldi    count, 0          ; und den Zähler initialisieren
           mov    zero, count
           mov    zero, count
;
 
loop:
loop:
           ldi    ZL, LOW(Codes*2)  ; die Startadresse der Tabelle in den
           ldi    ZL, LOW(Codes*2)  ; die Startadresse der Tabelle in den
Zeile 90: Zeile 88:


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


           lpm                      ; dieses Code Byte in das Register r0 laden
           lpm                      ; dieses Code-Byte in das Register r0 laden


           out    PORTD, r0        ; und an die Anzeige ausgeben
           out    PORTD, r0        ; und an die Anzeige ausgeben
;
 
           inc    count            ; den Zähler erhöhen, wobei der Zähler
           inc    count            ; den Zähler erhöhen, wobei der Zähler
           cpi    count, 10        ; immer nur von 0 bis 9 zählen soll
           cpi    count, 10        ; immer nur von 0 bis 9 zählen soll
           brne    wait
           brne    wait
           ldi    count, 0
           ldi    count, 0
;
 
wait:      ldi    r17, 10          ; und etwas warten, damit die Ziffer auf
wait:      ldi    r17, 10          ; und etwas warten, damit die Ziffer auf
wait0:    ldi    r18, 0            ; der Anzeige auch lesbar ist, bevor die
wait0:    ldi    r18, 0            ; der Anzeige auch lesbar ist, bevor die
Zeile 110: Zeile 108:
           dec    r17
           dec    r17
           brne    wait0
           brne    wait0
;
 
           rjmp    loop              ; auf zur nächsten Ausgabe
           rjmp    loop              ; auf zur nächsten Ausgabe
;
 
Codes:                              ; Die Codetabelle für die Ziffern 0 bis 9
Codes:                              ; Die Codetabelle für die Ziffern 0 bis 9:
                                     ; sie regelt, welche Segmente für eine bestimmte
                                     ; Sie regelt, welche Segmente für eine bestimmte
                                     ; Ziffer eingeschaltet werden müssen
                                     ; Ziffer eingeschaltet werden müssen.
                                     ;
                                     ;
           .db    0b11000000        ; 0: a, b, c, d, e, f
           .db    0b11000000        ; 0: a, b, c, d, e, f
Zeile 126: Zeile 124:
           .db    0b11111000        ; 7: a, b, c
           .db    0b11111000        ; 7: a, b, c
           .db    0b10000000        ; 8: a, b, c, d, e, f, g
           .db    0b10000000        ; 8: a, b, c, d, e, f, g
           .db    0b10010000        ; 9: a, b, c, d, f, g  
           .db    0b10010000        ; 9: a, b, c, d, f, g
</syntaxhighlight>


</syntaxhighlight>
== Mehrere 7-Segment-Anzeigen (Multiplexen) ==


==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 [[AVR-Tutorial: Schieberegister|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.


Mit dem bisherigen Vorwissen könnte man sich jetzt daran machen, auch einmal drei oder vier Anzeigen mit dem Mega8 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 Mega8 aber nicht. Dafür gibt es aber mehrere Auswege. Schieberegister sind bereits in einem anderen [[AVR-Tutorial:_Schieberegister|Tutorial]] beschrieben. 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 Ansteurung 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.
[[Datei:Tut_7_Seg_03.gif |thumb |right |280px |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&nbsp;C''' angesteuert werden.


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 für eine kurze Zeit 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 Mega8 am '''PortC''' 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&nbsp;Hz angesteuert werden, es also höchstens alle 10&nbsp;ms angeschaltet ist. In Ausnahmefällen können aber selbst 100&nbsp;Hz noch flimmern, z.&nbsp;B. wenn die Anzeige schnell bewegt wird oder wenn es zu Interferenzerscheinungen mit künstlichen Lichtquellen kommt, die mit Wechselstrom betrieben werden.


Ein Aspekt dieser Ansteuerungsart ist die Multiplexfrequenz, also ein kompletter Zyklus das 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, sollen jedes Segment mit mindestens 100 Hz angesteuert werden, es also mindestens alle 10ms angeschaltet ist. In Ausnahmefällen können aber selbst 100 Hz noch flimmern, z.&nbsp;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&nbsp;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'' ([[PWM|Tastverhältnis]]).


[[Bild:Tut_7_Seg_03.gif | thumb|right|280px | Ansteuerung von vier 7-Segmentanzeigen per Zeit-Multiplex]]
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&nbsp;mA (16 × 20&nbsp;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&nbsp;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 [https://www.mikrocontroller.net/topic/107941]) diesen Fall verhindern. Prinzipiell sollte man immer den Pulsstrom und die Multiplexfrequenz einmal überschlagen, bevor der Lötkolben angeworfen wird.
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 mehreren Anzeigen wird es deutlich. Um dem entgegen zu wirken lässt man pro Segment einfach mehr Strom fließen, bei LEDs dürfen dann 20mA ü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 dem Punkt '''Peak-Current''' (Spitzenstrom) und '''Duty-Cycle''' ([[PWM|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 fliesst aber schon ein Pulsstrom von 320mA (16 x 20mA), was nicht mehr ganz ungefährlich ist. '''Strom 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 320mA werden die meisten LEDs innerhalb von Sekunden zerstört. Hier muss sichergestellt werden, dass sowohl Programm (Breakpoint im Debugger) als auch Schaltung (Reset, Power-On, [http://www.mikrocontroller.net/topic/107941]) 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 so können, wie bereits beschrieben, 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. Auch muss berücksichtigt werden, dass der Mega8 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).
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&nbsp;Ω 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===
=== Programm ===


Das folgende Programm zeigt eine Möglichkeit zum Multiplexen. Dazu wird ein Timer benutzt, der in regelmässigen Zeitabständen einen Overflow [[Interrupt]] auslöst. Innerhalb der Overflow Interrupt Routine wird
Das folgende Programm zeigt eine Möglichkeit zum Multiplexen. Dazu wird ein [[AVR-Tutorial: Timer|Timer]] benutzt, der in regelmäßigen Zeitabständen einen Overflow-[[Interrupt]] auslöst. Innerhalb der Overflow-Interrupt-Routine wird
* die momentan erleuchtete Anzeige abgeschaltet
* die momentan erleuchtete Anzeige abgeschaltet,
* das Muster für die nächste Anzeige am Port D ausgegeben
* 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
* 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:
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 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.
* 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.
Die Funktion <code>out_number</code> 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.
'''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.
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 <code>out_number</code> 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!).
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!).


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"


Zeile 181: Zeile 178:
;
;
; Aufgabe dieser Funktion ist es, bei jedem Durchlauf eine andere Stelle
; Aufgabe dieser Funktion ist es, bei jedem Durchlauf eine andere Stelle
; der 7-Segmentanzeige zu aktivieren und das dort vorgesehene Muster
; der 7-Segment-Anzeige zu aktivieren und das dort vorgesehene Muster
; auszugeben
; auszugeben.
; Die Funktion wird regelmässig in einem Timer Interrupt aufgerufen
; Die Funktion wird regelmäßig in einem Timer-Interrupt aufgerufen.
;
;
; verwendet werden 6 Bytes im SRAM (siehe .DSEG weiter unten im Programm)
; Verwendet werden 6 Bytes im SRAM (siehe .DSEG weiter unten im Programm)
;    NextDigit    Bitmuster für die Aktivierung des nächsten Segments
;    NextDigit    Bitmuster für die Aktivierung des nächsten Segments
;    NextSegment  Nummer des nächsten aktiven Segments
;    NextSegment  Nummer des nächsten aktiven Segments
Zeile 195: Zeile 192:
; NextSegment ist einfach nur ein Zähler, der bei jedem Aufruf der Funktion
; 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.
;  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
Er wird benutzt, um ausgehend von der Adresse von Segment0 auf das
;  jeweils als nächstes auszugebende Muster aus Segement0, Segment1,
;  jeweils als nächstes auszugebende Muster aus Segement0, Segment1,
;  Segment2 oder Segment3 zuzugreifen. Die Adresse von Segment0 wird
;  Segment2 oder Segment3 zuzugreifen. Die Adresse von Segment0 wird
Zeile 205: Zeile 202:
;  bei jedem Aufruf um 1 Stelle nach links verschoben wird. Beim nächsten
;  bei jedem Aufruf um 1 Stelle nach links verschoben wird. Beim nächsten
;  Aufruf findet sich dann 0b11111101 in NextDigit, dann 0b11111011 und
;  Aufruf findet sich dann 0b11111101 in NextDigit, dann 0b11111011 und
;  zu guter letzt 0b11110111. Wird beim nächsten Schiebevorgang 0b11101111
;  zu guter Letzt 0b11110111. Wird beim nächsten Schiebevorgang 0b11101111
;  erkannt, dann wird NextDigit auf 0b11111110 zurückgesetzt (und NextSegment
;  erkannt, dann wird NextDigit auf 0b11111110 zurückgesetzt (und NextSegment
;  auf 0) und das ganze Spiel beginnt beim nächsten Funktionsaufruf wieder
;  auf 0) und das ganze Spiel beginnt beim nächsten Funktionsaufruf wieder
;  von vorne.
;  von vorne.
;
;
; Segment0 .. 3  enthalten die auszugebenden Bitmuster für die Einzelleds
; Segment0 .. 3  enthalten die auszugebenden Bitmuster für die Einzel-LEDs
;  der jeweiligen 7-Segment Anzeigen. Diese Muster werden so wie sie sind
;  der jeweiligen 7-Segment-Anzeigen. Diese Muster werden so wie sie sind
;  einfach ausgegeben. Soll eine der Anzeigen etwas bestimmtes anzeigen
;  einfach ausgegeben. Soll eine der Anzeigen etwas bestimmtes anzeigen
;  (zb eine Ziffer), so obliegt es dem Code, der Werte in diese SRAM
;  (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.
;  Zellen schreibt, das dafür zuständige Bitmuster dort zu hinterlassen.
;  Die Multiplexroutine kümmert sich nicht darum, dass diese Bitmuster
;  Die Multiplexroutine kümmert sich nicht darum, dass diese Bitmuster
Zeile 234: Zeile 231:
                                       ; Das Muster für die nächste Stelle ausgeben
                                       ; Das Muster für die nächste Stelle ausgeben
                                       ; Dazu zunächst mal berechnen, welches Segment als
                                       ; Dazu zunächst mal berechnen, welches Segment als
                                       ; nächstest ausgegeben werden muss
                                       ; nächstes ausgegeben werden muss
           ldi    ZL, LOW( Segment0 )  
           ldi    ZL, LOW( Segment0 )
           ldi    ZH, HIGH( Segment0 )
           ldi    ZH, HIGH( Segment0 )
           lds    temp, NextSegment
           lds    temp, NextSegment
Zeile 269: Zeile 266:
;
;
;************************************************************************
;************************************************************************
; 16 Bit-Zahl aus dem Registerpaar temp (=low), temp1 (=high) ausgeben
; 16-Bit-Zahl aus dem Registerpaar temp (=low), temp1 (=high) ausgeben
; die Zahl muss kleiner als 10000 sein, da die Zehntausenderstelle
; Die Zahl muss kleiner als 10000 sein, da die Zehntausenderstelle
; nicht berücksichtigt wird.
; nicht berücksichtigt wird.
; Werden mehr als 4 7-Segmentanzeigen eingesetzt, dann muss dies
; Werden mehr als vier 7-Segment-Anzeigen eingesetzt, dann muss dies
; natürlich auch hier berücksichtigt werden
; natürlich auch hier berücksichtigt werden.
;
;
out_number:
out_number:
Zeile 286: Zeile 283:
           brcc    _out_tausend
           brcc    _out_tausend


           ldi    ZL, low(2*Codes)    ; fuer diese Ziffer das Codemuster fuer
           ldi    ZL, low(2*Codes)    ; für diese Ziffer das Codemuster für
           ldi    ZH, high(2*Codes)    ; die Anzeige in der Codetabelle nachschlagen
           ldi    ZH, high(2*Codes)    ; die Anzeige in der Codetabelle nachschlagen
           add    ZL, temp2
           add    ZL, temp2
Zeile 292: Zeile 289:
           lpm
           lpm
           sts    Segment3, r0        ; und dieses Muster im SRAM ablegen
           sts    Segment3, r0        ; und dieses Muster im SRAM ablegen
                                         ; die OvI Routine sorgt dann duer die Anzeige
                                         ; die OvI-Routine sorgt dann für die Anzeige
           ldi    temp2, 10
           ldi    temp2, 10


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


           lpm
           lpm
           sts    Segment0, r0        ; und ans SRAm ausgeben
           sts    Segment0, r0        ; und ans SRAM ausgeben


           pop    temp1
           pop    temp1
Zeile 351: Zeile 348:
           ldi    temp, $0F
           ldi    temp, $0F
           out    DDRC, temp
           out    DDRC, temp
;                                    initialisieren der Steuerung für die
;                                    Initialisieren der Steuerung für die
;                                    Interrupt Routine
;                                    Interrupt-Routine
           ldi    temp, 0b11111110
           ldi    temp, 0b11111110
           sts    NextDigit, temp
           sts    NextDigit, temp
Zeile 359: Zeile 356:
           sts    NextSegment, temp
           sts    NextSegment, temp


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


Zeile 377: Zeile 374:
           rcall    out_number
           rcall    out_number


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


Zeile 407: Zeile 404:
                                     ; 7: a, b, c
                                     ; 7: a, b, c
     .db  0b10000000, 0b10010000    ; 8: a, b, c, d, e, f, g
     .db  0b10000000, 0b10010000    ; 8: a, b, c, d, e, f, g
                                     ; 9: a, b, c, d, f, g  
                                     ; 9: a, b, c, d, f, g


           .DSEG
           .DSEG
Zeile 419: Zeile 416:


== Forenbeiträge ==
== Forenbeiträge ==
*[http://www.mikrocontroller.net/topic/154257 Geisterleuchten bei 7-Segment Anzeige]


*[http://www.mikrocontroller.net/topic/262337#2736074 Re: Zehn 7Segmente im Multiplexbetrieb : Hilfe !] - Ausführliche Erklärung wie und warum man entweder die '''Stellen''' oder die '''Segmente''' multiplext.
* [https://www.mikrocontroller.net/topic/154257 Geisterleuchten bei 7-Segment Anzeige]
* [https://www.mikrocontroller.net/topic/262337#2736074 Re: Zehn 7Segmente im Multiplexbetrieb : Hilfe !] Ausführliche Erklärung, wie und warum man entweder die '''Stellen''' oder die '''Segmente''' multiplext.
* [https://www.mikrocontroller.net/topic/107941 7 Segment Multiplex, Sicherheit, watchdog??] – Hardware-Watchdog mit 74HC123


----
----

Aktuelle Version vom 25. November 2022, 17:36 Uhr

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