Home ==> Inhaltsverzeichnis ==> 15-Zähler
Logo

15-Zähler



0 Seitenindex
  1. Die Aufgabe
  2. Basis-Entscheidungen
  3. Schritte zur Lösung mit CMOS
    1. Ein Impulsformer in CMOS
    2. Zählen
    3. Siebensegment-Anzeige in CMOS
  4. Schritte zur Lösung mit Mikrocontroller
    1. Auswahl des Controllers
    2. Kontakt und Controller
    3. 15-Zähler mit Controller
    4. Software für den Controller
  5. Ein selbstgemachter Zähler und Anzeiger
    1. Zählen mit Controller
    2. Übersetzung in 7-Segment
    3. Auf Tastendruck
  6. Videos vom Controller
  7. Vergleich und Fazit
Die Schaltbilder für dieses Projekt gibt es als LibreOffice-Draw-Datei hier, die Berechnungen gibt es hier als LibreOffice-Calc-Datei. Die Bauteilelisten für alle Varianten, die hier aufgezeigt werden, gibt es für Reichelt hier.

1 Die Aufgabe

So kann es gehen: man hat auf irgendeinem Flohmarkt saubillig eine Siebensegment-LED-Anzeige erstanden und will jetzt endlich mal was damit anfangen. Mit "keine Ahnung von nix" kommt man da jetzt nicht weiter, also muss man jetzt mal was Neues lernen. Warum also nicht gleich Digitaltechnik draufschaffen?

Mit einer 9V-Batterie und einem 330Ω-Widerstand hat man schnell herausgefunden, was man wo und wie rum anschließen muss, damit bei dem Ding welche Segmente leuchten. Aber wie jetzt weiter?

Warum also nicht einen Digitalzähler bauen? Und: weil es das in Aufwärts und mit Null bis Neun schon (fast) fertig gelöst auf dieser Webseite gibt, warum nicht ein wenig variieren? Der Zähler soll abwärts zählen und er soll damit nicht bei 9, sondern bei 15 anfangen. Wie krieg' ich das jetzt nur hin?

2 Basis-Entscheidungen

Nun denn, denn mal los. Aber es gibt von Anfang an ein paar Entscheidungen zu fällen, die das weitere Vorgehen zentral vorbedingen.

CMOS oder Mikrocontroller?

Mal zu allererst: So eine einfache Aufgabe kriegt man mit allen Varianten nur dann elegant gelöst, wenn man einen Mikrocontroller darauf ansetzt. Der kann so etwa alles, was man sich nur ausdenken kann.

Wer das überkandidelt findet, kann es aber auch in CMOS (oder in TTL) lösen. Nur ist man damit nicht so arg flexibel und man muss sich schon vorher darüber klar werden, was man nun genau haben will. Was der Mikrocontroller mit ein paar geänderten Programmzeilen erledigt, will beim CMOS jedes Mal neu zusammengelötet (oder auf dem Breadboard zusammengesteckt) sein. Und man braucht dazu auch andere ICs dafür, beim Mikrocontroller ist es hingegen immer derselbe.

Eine oder zwei Digitalanzeigen?

So eine 15 hat zwei dezimale Ziffern: eine 1 und eine 5. Also warum nicht zwei Digitalanzeigen hintereinander? Nun, wer nicht gerne lötet, der meidet gerne eine zweite Anzeige. Und: man kriegt die 10 bis zur 15 auch ganz anders angezeigt, so wie hier:

Die Hexadezimalziffern auf einer 7-Segment-Anzeige Alles recht deutlich erkennbar, bis auf das "b": wenn man die "6" ohne Oberstrich malt (wie das übliche 7-Segment-Decoder meist machen), kann man sie nicht vom "b" unterscheiden. Also, wenn hexadezimal, dann immer alle Ziffern schön einheitlich beieinander halten.

Gemeinsame Kathode oder Gemeinsame Anode?

7-Segment-LED mit gemeinsamer Anode bzw. Kathode Je nachdem, was da vom Flohmarkt kam, kriegt man entweder eine Anzeige mit gemeinsamer Anode oder eine mit gemeinsamer Kathode. Das macht einen himmelweiten Unterschied, zumindest bei der CMOS-Version (bei der Mikrocontroller-Version stellt man da nur ein paar Zeilen Programmcode um).

Bei der Version mit Gemeinsamer Anode sind alle LED-Anoden der sieben Segmente zusammengeschaltet. Soll ein Segment leuchten, dann muss das Treiber-IC diese Leitung vom Segment (über den Begrenzungswiderstand R) auf logisch Null ziehen, d. h. der Ausgangstransistor im IC verbindet diesen Pin mit seiner VSS-Leitung zum Minuspol. Das machen meistens die TTL-Siebensegment- Dekoder-ICs (allerdings gehen die auch nur mit 5V Betriebsspannung).

Umgekehrt die Anzeigen mit Gemeinsamer Kathode: hier muss das IC seinen Ausgang mit VDD verbinden, um das Segment zum Leuchten zu bringen. Die Polarität des Ausgangs ist hier invertiert, eine Eins macht das Segment an (und nicht aus). Das machen die CMOS-Siebensegment-Dekoder-ICs fast alle so.

Wenn man also eine Anzeige mit der falschen Polarität hat: vergiss es und bring sie besser zur Elektroschrott-Sammlung. Oder nimm einen Mikrocontroller, der kann einfach alles.

Bei zwei: führende Null unterdrücken oder nicht?

Je nachdem, was man vorhat, kann man bei zwei Ziffern die vordere dunkel lassen, wenn sie Null ist, oder man zeigt die Null tatsächlich an. Auch das macht einen Unterschied.

Unterdrueckung der fuehrenden Null Will man die führende Null mit einer dazu geschalteten zweite Anzeige unterdrücken, dann schaltet man die Segmente b und c dieser Anzeige mit einem achten Bit aus oder an. Bei allen Zusänden oberhalb von Neun wird dieser Ausgang eingeschaltet. Alle anderen Segmente werden gar nicht erst verdrahtet.

Will man führende Nullen anzeigen, dann muss man die beiden Segmente b und c beide mittels Widerstand dauerhaft anmachen, denn diese beiden sind immer an. Die Segmente a, d, e und f kriegen dann jeweils einen Widerstand und die gesammelte Leitung geht an Q7. Die wird immer dann Eins, wenn der Zähler kleiner als zehn ist.

3 Schritte zur Lösung mit CMOS

Die Aufgabe wird erst mal in Teilschritte zerlegt:
  1. Alle mechanischen Kontakte prellen. Damit die ganze Impulszählerei sauber funktioniert, muss erst mal ein Impulsformer her, der nur Impulse mit einer festlegbaren Mindestdauer zulässt und alles Prellen wirksam unterdrückt.
  2. Dann brauchen wir den Zähler. Das kann entweder
  3. Dann brauchen wir entweder
Alle drei Teilschritte sind im Folgenden detaillierter beschrieben.

3.1 Ein Impulsformer in CMOS

Damit der Impuls erst dann zum Zähler gelangt, wenn er eine bestimmte Dauer erreicht hat, brauchen wir eine Kondensatormimik, die erst nach einer gewissen Zeit den Impuls durchlässt.

Erfahrungsgemäß dauert diese Prellzeit etwa 10 bis maximal 20 Millisekunden. Der Schalter, der beim Schließen des Kontakts einen Eingang auf Null zieht, darf dies also erst nach ca. 25 ms tun.

Kontakt-Entpreller Damit er das tut, kommt er in die nebenstehende Schaltungsmühle. Der im offenen Zustand mit dem 10k-Widerstand R1 auf Plus gezogene Kontakt lädt über R2 mit 15 kΩ den Kondensator C1 mit 2,2 µF auf die Betriebsspannung auf, parallel zum R3 mit 47 kΩ.

Der NAND-Schmitt-Trigger 4093-1 hat dann an beiden Eingängen positive Betriebsspannung, sein Ausgang geht auf Low.

Schließt der Kontakt, dann wird sowohl der Widerstand R1 als auch R2 auf Low gezogen. Das entlädt den Kondensator C1 bis herab auf eine Restspannung von Ub * R2 / (R2 + R3), bei 12 V sind das 2,9 V.

Unterschreitet die Spannung am Kondensator die untere Schwellspannung des 4093-NANDs, ca. 5,9 Volt, dann geht sein Ausgang auf Eins. Das dauert ein wenig, da die RC-Kombination eine Verzögerung bewirkt.

Spannung am Kondensator bei geschlossenem Kontakt Wie lange das dauert, kriegt man mit einer Tabellenkalkulation heraus: ein Kondensator beginnt bei 12 V und wird mit einem Strom von I = U / R = UC / 15 kΩ = 0,8 mA entladen. Nimmt die Spannung am Kondensator ab, dann nimmt auch der Entladestrom ab und es kommt der Strom über die 47 kΩ hinzu. Die Spannung des Kondensators errechnet sich zu ΔU = I * Δ t / C, was man in der Tabellenkalkulation in kleinen Schritten (hier waren es 0,5 ms) zu der Entladekurve hingerechnet kriegt.

Spannung am Kondensator bei geoeffnetem Kontakt Wenn man nun den geschlossenen Schalterkontakt wieder öffnet, dann lädt sich der Kondensator von seinen 2,9 V ab wieder auf, und zwar sowohl über die Reihenschaltung von R1 und R2 als auch parallel dazu über R3. Erreicht die Spannung die obere Schwellspannung des 4093, bei 12 V Betriebsspannung sind das ca. 7,8 V, dann geht der Ausgang wieder auf Null.

Hat man einen sehr groben Schalter mit längerer Prellzeit, macht man einfach C1 größer, z. B. mit 3,3 oder 4,7 µF.

Einfach, aber wirkungsvoll. Man sieht auch, dass die drei Widerstände so dimensioniert sind, dass sowohl beim Schließen als auch beim Öffnen dieselbe Verzögerung herauskommt, nämlich ca. 27 ms. Das sollte auch für Sähmaschinen-Kontakte ausreichen.

3.2 Zähler-Mimiken

Zählen ist recht einfach, da gibt es fertige ICs in CMOS dafür. Aber alle Zählertypen können was anderes. Daher im Folgenden zwei verschiedene Typen zur Auswahl.

3.2.1 Ein 4-Bit-Zähler von 15 abwärts in CMOS

Binaerzaehler 4516 beim Abwaertszaehlen Impulsdiagramm des Binaerzaehlers Das ist so ein einfacher Abwärtszähler bei der Arbeit. Er besteht überwiegend aus unnützen Eingängen, die alle auf 0 Volt zu bringen sind. Dabei ist der U/D-Eingang für die Zählrichtung zuständig: ist er Null, geht es abwärts, ist er Eins, geht es aufwärts.

Wie man am Impulsdiagramm sieht, zählt er immer bei positiven Flanken am In-Eingang: wechselt deren Polarität von Null auf Eins, dann geht es Eins weiter abwärts. Bei Eins auf Null passiert gar nix, da ist er taub.

Der Eingang CARRY-IN und der Ausgang CARRY-OUT können zum Kaskadieren verwendet werden: CARRY-OUT wird beim Abwärtszählen Low, wenn der Zählerstand Null erreicht hat (beim Aufwärtszählen, wenn 15 erreicht ist). Schließt man CARRY-OUT an das CARRY-IN des nächst-höheren Zählers an, dann kann man alle Clock-Eingänge der Zähler zusammenschließen und die Carrys sorgen dafür, dass sich der richtige Zähler zur richtigen Zeit auch angesprochen fühlen wird.

Der Zählerstand steht dabei immer an der Ausgängen Q4:Q3:Q2:Q1 zur Verfügung. Wollen wir den anzeigen, dann brauchen wir noch einen Hexadezimal-Decoder, wie er hier oder auch hier beschrieben ist.

3.2.2 Ein 4-Bit-Dezimal- und ein 1-Bit-Zähler abwärts in CMOS

Das kriegen wir hier, wenn wir auch noch die Siebensegment-Anzeige im Griff haben.

3.3 Siebensegment-Anzeige in CMOS

Klar, da gibt es doch den 4511 dafür. Aber: angeschmiert! Der kann zwar Null bis Neun, aber bei A bis F macht er einfach mal rein gar nix und schaltet alles auf Durchzug. Da kommen jetzt jede Menge Vorschläge für andere ICs um die Ecke. Nur: die kriegt man alle gar nicht gekauft oder muss sie in Einzelstücken mühsam bei Ebay suchen.

Also ist hier Selbermachen angesagt.

3.3.1 Siebensegment mit Gattern in CMOS

Siebensegment-Decoder fuer Hexadezimalzahlen Das wäre dann mal so ein Hexadezimaldecoder und Siebensegment-Treiber, nur zur Abschreckung. Und auch nur ein Teilausschnitt von dieser Diodenorgie.

Interner Aufbau des 4028 Und so sieht der 4028 intern aus (©2023 by Texas Instruments). Man sieht schon, dass man ihn mit Invertern und ganz vielen ODER- und UND-Gattern nachbauen könnte, aber eine ganze Euro-Platine wäre da voll mit Chips. Hier ist alles in einem.

Die beiden 4028 in der Schaltung machen aus den jeweils drei unteren Bits A, B und C vom 4-Bit-Zähler acht Ausgänge. Eigentlich machen sie zehn, weil der 4028 eigentlich ein Dezimaldecoder ist. Die beiden Ausgänge 8 und 9 sind aber nur an, wenn der Eingang D Eins ist.

Die beiden benutzen wir hier gar nicht, sondern füttern das vierte Bit D invertiert - in einem 4049-Inverter - an den zweiten 4028. Auf diese Weise kriegen wir zwei mal acht = 16 Ausgangsleitungen, die den 16 verschiedenen Zuständen an A bis D entsprechen.

7-Segment-Decoder mit Dioden Wie kommen wir jetzt von 16 Einzelleitungen zu unseren sieben für die Anzeige? Wir könnten jetzt mit jeder Menge UND- und ODER-Gattern alle Zustände herausfiltern, bei denen das Segment "a" an ist. Laut nebenstehender Tabelle ist das bei zwölf der 16 Zustände der Fall (wenn die letzte Ziffer in Spalte "hgfedcba" eine Eins ist). Wir bräuchten daher ein 12-Eingangs-ODER-Gatter, was es aber gar nicht zu kaufen gibt. Und mit mehreren Chips so was zu basteln wäre bei Weitem überkandidelt.

Intellenter ist es, alle die auszusortieren, bei denen das Segment a Aus ist. Das sind nur vier. Und: wenn wir die vier über Dioden miteinander verknüpfen, dann sparen wir uns jede Menge UNDs und ODERs. Alle vier Zustände, bei denen das Segment "a" aus ist, ziehen in der Schaltung im Schaltbild oben die Sammelleitung auf Low (die ohne diese Nullen über den 47k-Widerstand auf High liegen würde). Das Low am Eingang macht beim Hex-Gatter 4050 ein Low am Ausgang und das Segment aus. Außer, wenn keiner dieser vier Zustände vorliegt, dann ist es angeschaltet.

Wie die Tabelle zeigt, brauchen wir für "a" bis "g" insgesamt 34 Dioden.

Wer so was baut, hat entweder zu viel Zeit, zu viel Lötzinn oder zu viele 1N4148-Dioden in der Bastelkiste. Oder halt doch eher einen Sprung in der Schüssel.

3.3.2 Siebensegment mit zwei Ziffern

Impulsdiagramm des Doppeldigit-Zaehlers Haben wir zur Anzeige zwei separate Ziffern, dann müssen wir umdenken: jetzt geht die hintere Ziffer immer abwechselnd von Fünf bis Null und von Neun bis Null und die vordere Ziffer zeigt ab zehn aufwärts eine Eins an.

Das bedingt aber, dass die hintere Ziffer erst mal mit fünf anfängt, solange die vordere noch Eins ist. Erst wenn die vordere Ziffer Null ist und die hintere Ziffer ebenfalls, dann muss der Zirkus neu mit 15 gestartet werden.

Das kriegt man nur mit etwas Jonglieren hin.

Zwei-Ziffern-Zaehler Das hier umgeht die Sache mit den für Hexadezimal ungeeigneten Siebensegment-Dekodern, indem es die 16 Zustände auf zwei Ziffern verteilt. Die vordere Ziffer zeigt bei 10 bis 15 eine Eins an und bei 0 bis 9 entweder gar nix (Nullunterdrückung) oder eine Null.

Der Neun-bis-Null-Zähler ist jetzt ein Dezimalzähler, der beim Abwärtszählen nach Null wieder einfach auf Neun springt. Darf er aber nur dann, wenn die vordere Ziffer noch Eins ist. Darf er aber gar nicht, wenn die vordere Ziffer schon Null ist, da er dann bei 15 wieder mit 5 anfangen muss und nicht mit 9. Das macht die Schaltung, indem sie beim nächsten Impuls nach Null für einen kurzen Moment lang den Preset-Enable-Eingang des Zehner-Zählers einschaltet. Das setzt den Zehnerzähler auf Fünf.

Jetzt ist das oberste Bit, das 10 bis 15 anmacht, kein Zähler, sondern ein D-Flip-Flop. Davon sind im 4013 zwei Stück,
  1. die mit dem CLK-Eingang bei steigenden Flanken das Bit am Data-Eingang in den Flip-Flop schreiben, und
  2. die mit dem Reset-Eingang den Flip-Flop auf Null setzen, und
  3. die mit dem Set-Eingang den Flip-Flop auf Eins setzen, und
  4. dessen Ausgänge Q und /Q den Zustand nach außen hin zugänglich machen.
(Wer mehr über den 4013 wissen will, geht einfach hierher.)

Der obere Flip-Flop 2 ist hier nun das Zehner-Bit, der untere Flip-Flop 1 dient dazu, den nötigen Neustart zu erkennen und sowohl den Preset des 4510 als auch den Set des oberen Flip-Flops vorzunehmen. Bei der Erkennung, dass jetzt ein Neustart ab 15 ansteht, kommen zwei Signale zusammen: sowohl
  1. der CARRY-OUT-Ausgang des Zählers 4510 muss Null sein, als auch
  2. das oberste Zähl-Bit muss Null sein.
Das alles macht ein Zweifach-Nicht-Oder vom Typ 4001. Sein Ausgang wird nur dann High, wenn beide Eingänge Low sind. Beim nächsten Impuls schiebt das Clock-Signal dann diese Eins in den unteren Flip-Flop. Der setzt den 4510 auf Fünf und den oberen Flip-Flop auf Eins. Nachdem der 47k-Widerstand den 220pF-Kondensator bis zur Schaltschwelle genügend lang aufgeladen hat, schaltet sich der untere Flip-Flop mit seinem Reset-Eingang selbst wieder aus (was man als einen Monoflop bezeichnet). Mit t = R * C = 47.000 * 0,000.000.000.220 = 10 µs dauert das nicht sehr lang und wir sehen nur das Resultat: eine 5 hinten und eine 1 vorne.

3.3.3 Siebensegment mit EPROM

Abwaertszaehler mit EPROM Das hier ist jetzt nur was für Leute, die in den Achtzigern schon gelötet haben und die irgendwo in ihren Bastelkisten noch ein EPROM-Programmiergerät haben und das auch noch bedienen können.

Hier steckt die Intelligenz im EPROM. Die vier Hex-Bits D bis A aus dem Binärzähler kommen an die Adressen A3 bis A0 des EPROMs. Und in dem stehen die sieben Bits herum, die bei 15 bis 0 an sein sollen. Da so ein EPROM-Ausgang nur 2 mA liefern kann, kann man damit alleine noch keine Siebensegment-Anzeige beglücken. Deswegen haben wir hier noch einen Treiber dazwischen gebaut: der ULN2001 (2002 und 2003 gehen auch) ist sieben Darlington-Transistoren und macht für alle Eingänge, die auf Eins sind, seine Ausgänge auf Durchzug. Das bedeutet, dass unsere Anzeige diesmal eine Gemeinsame-Anode haben muss.

In das EEPROM kommt die Reihe 63, 6, 91, 79, 102, 109, 125, 7, 127, 111, 119, 124, 57, 94, 121 und 113 (in Dezimal). Dann klappt's auch mit den Hex-Ziffern. Geeignet ist vom kleinsten (2716) bis zum größten 27C512 jedes EPROM, wir brauchen ja nur 16 Bytes.

Wer lieber zwei Ziffern haben will, kann das auch im EPROM haben. Das hier nicht verwendete D7-Bit kann dann die vordere Anzeige (mit einem zusätzlichen Transistor als Treiber oder mit einem 8-Bit-Darlington wie dem ULN2801) steuern, während die hintere wie gehabt die 0 bis 9 der zweiten Anzeige macht. Das kriegt man hin, wenn man den Inhalt des EPROMs ein wenig anpasst: die Ziffern 0 bis 9 bleiben wie oben, aber bei 10 bis 15 werden die Ziffern 0 bis 5 wiederholt und einfach mit 128 addiert.

Alles in allem schon viel einfacher als mit dem Diodenverhau (mit drei ICs und 32 Dioden) oder dem Zwei-Digit-Zähler mit vier ICs. Aber es geht noch viel einfacher, nämlich mit nur einem einzigen 14-poligen IC, und das zeigen die nächsten Kapitel.

4 Schritte zur Lösung mit Mikrocontroller

Sorry, jetzt wird es frustig: vergiss den ganzen Kram mit den vermaledeiten Diodengräbern, den D-Flip-Flops und den Gemeinsamen Kathoden und Anoden. Einfacher geht es einfach nicht mehr, aber Du kannst von jetzt an die Suche nach irgendwelchen ICs, die dann doch nicht das machen, was Du brauchst, endgültig einstellen.

Frustig aber deswegen, weil Du dazu ganz schön viel Neues lernen müsstest. Nämlich, wie man ein Assembler-Programm schreibt, wie man es assembliert und wie man den Hexcode in den Chip hineinbringt. Dafür gibt es dann aber maßgeschneiderte Chips, die genau das - und die genau nur das - machen, was Du genau haben willst. Aber, vor den Erfolg hat der Herr den Schweiß gestellt, also los mit dem ganzen Übel und dem ganzen komplizierten Kram.

4.1 Auswahl des Controllers

Wenn Du einen Arduino hast, kannst Du Dir dieses Kapitel sparen, denn Deine Antwort heißt immer "ATmega328", egal was auch immer nötig sein sollte. Der hat zwar doppelt so viele Pins wie nötig, aber im Pinverschwenden biste ja Weltmeister.

Alle anderen, normalen Menschen beginnen so: lade Dir avr_sim herunter, von dieser Webseite und für dein Betriebssystem. Ganz Mutige laden den Quellcode dafür herunter und machen sich die ausführbare Datei mit Lazarus selber.

Nach dem Starten wählen wir im Menu Projekt "New" aus und im sich öffnenden Fenster klicken wir auf "Device selector". Das nachfolgende Fenster öffnet sich:

Auswahl des Chips In dem Fenster stellen wir mit dem Ausklappfenster "Ext ints" eine Eins (für unsere Zähltaste) und mit dem Ausklappfenster "8-bit I/O" ebenfalls eine Eins ein (für unsere Siebensegment-Anzeige(n).

Jetzt erscheinen im mittleren Fenster 37 verschiedene AVR-Controller (aber nicht den ATmega328, den gibt es in dieser Kombination mit INTn und 8-Bit-I/O nicht - Arduino schon mal ausgespielt).

Klicken wir im mittleren Fenster auf einen Typen (A oder nicht A ist dabei egal), dann kriegen wir im rechten Fenster seine Anschlussbelegung. Im Falle des ATtiny24 ist die Siebensegment-Anzeige an den Port A mit den Pins 13 bis 6 und die Taste an Pin 5. An VCC kommen +3 bis +5 Volt, an GND 0 Volt. Die Pins 2 und 3 sind noch frei.

4.2 Kontakt und Controller

In der CMOS-Version mussten wir noch jede Menge zusätzlich dazu bauen, um mit prellenden Kontakten umzugehen. Brauchen wir bei den AVRs nicht, die haben alles Nötige schon von Haus aus dabei. Der Zusatzaufwand verringert sich auf einen Widerstand und einen Elko.

Eingangsstufe mit Prellunterdruckung Mit nur einem Widerstand und einem Elko sowie dem eingebauten Pull-Up und der eingebauten Hysterese im AVR ist das Eingangssignal am INT0 schnell mal Hardware-entprellt.

Ladekurve des Elkos mit dem internen Pull-Up-Widerstand So lädt der interne Pull-Up-Widerstand den externen Elko auf. Nach etwa 50 Millisekunden ist die obere Schaltspannung des Eingangstreibers erreicht und der INT0-Eingang ergibt beim Lesen eine Eins.

Entladekurve des Elkos mit externem Widerstand Schließt man den In-Eingang an die Masse an, dann wird der geladene Elko langsam entladen. Der Entladestrom wird teilweise vom Ladestrom durch den Pull-Up-Widerstand kompensiert, daher entlädt er sich nur bis herab zu 1 Volt.

Nach ca. 0,02 Sekunden ist die untere Schaltschwelle des Eingangstreibers unterschritten und INT0 würde beim Lesen Null ergeben.

Im Zusammenspiel des Elkos, der beiden Widerstände und der Hysterese am Eingangspin hat man einen hochqualitativen Schutz gegen Störnmanöver durch elektromagnetische Wellen. Der Aufwand dafür ist bei einem AVR minimal.

4.3 15-Zähler mit Controller

4.3.1 15-Zähler mit einer Ziffer und mit Controller

Abwaertszaehler mit einer Ziffer und Controller Das ist das Schaltbild von unserem controller-getriebenen Zähler und Anzeiger für eine einzelne Siebensegment-Anzeige. Sie zeigt die Hexadezimal-Ziffern 0 bis 9 und A bis F korrekt an.

Die Ausgänge PA0 bis PA6 des Controllers treiben die Anzeige über Strombegrenzungswiderstände. Ob es sich um eine Anzeige mit gemeinsamer Kathode oder Anode handelt, kann im Assembler-Programm mit der Konstanten "cAnode" eingestellt werden, gehen tut beides. Ach ja: obwohl CMOS kann so ein AVR-Ausgang 40 und mehr mA Strom liefern und ziehen, was auch für Monsteranzeigen genug sein sollte und so eine kleine Anzeige ohne den Widerstand schnell kaputt kriegen würde.

Die drei Pins PA4, PA5 und PA6 haben noch eine zweite Funktion: wenn der Reset-Eingang, der normalerweise über 10k an Plus liegt, auf Null gezogen wird, dienen sie als Programmierpins. Der Reset, die drei Programmierpins und die Stromversorgung liegen an der ISP6-Schnittstelle an, sodass der ATtiny24 im laufenden Betrieb der Schaltung bequem umprogrammiert werden kann.

Am INT0/PB2-Eingang liegt die schon oben besprochene Eingangsschaltung, die im Betrieb den Elko auflädt und den Eingang auf Eins legt. Jeder Tastendruck an diesem Pin (auf 0V) bewirkt das Zählen und Erneuern der Anzeige.

Am Eingang PB0 liegt der Lampentest-Taster. Wird er auf Null gezogen, dann gehen alle Lampen an. Das erfolgt über einen PCINT, der beim Loslassen der Taste auch wieder den alten Zählerstand anzeigt.

An den Pin PB1 kann man einen Schalter anschließen. Wird der geschlossen, dann zählt der Zähler aufwärts.

4.3.2 15-Zähler mit zwei Ziffern und mit Controller

Abwaertszaehler mit einer Ziffer und Controller Dasselbe hier mit zwei Anzeigeziffern. In dieser Schaltung wird die vordere Ziffer ausgeschaltet (Nullunterdrückung), man kann das aber auch anders haben, wenn man weitere vier Widerstände an die Segmente a, d, e und f anschließt und die an PA7 führt. Die beiden Widerstände an den Segmenten b und c gehen dann auf +5V (gemeinsame Kathode) oder auf Null Volt (gemeinsame Anode). Im Assemblerprogramm wird dann die Konstante cMitNull auf Eins gesetzt.

Alle anderen Anschlüsse bleiben so wie in der vorigen Schaltung.

4.4 Software für den Controller

Hier kann man sich die fertige Software 15-zaehler_tn24_v1.asm im Assembler-Format herunter laden. Im Kopf der Software kann man alles wie gewünscht einstellen, nämlich
  1. ob es eine Anzeige mit Common-Kathode (.equ cAnode = 0) oder Anode (.equ cAnode = 1) ist,
  2. ob die Anzeige eine (.equ cDisplays = 1) oder zwei (.equ cDisplays = 2) Ziffern haben soll,
  3. ob die führende Null angezeigt werden soll (.equ cMitNull = 1) oder lieber nicht (.equ cMitNull = 0).
Wer nur mal testen will, ob es richtig zählt, macht .equ cAutoAbw = 1 und kriegt das Zählen und Anzeigen automatisch und ohne Taster.

Dann assemblieren wir die Datei (Anleitung für den Simulator avr_sim gibt es hier, für gavrasm für Windows hier und für Linux hier). Dann brennen wir die Hex-Datei in den fertig aufgebauten Chip und sehen, ob es alles so macht wie wir es haben wollen.

Wer die Software selber machen will: in den folgenden Kapiteln gibt es eine bebilderte Anleitung für eine etwas abgespeckte Einfachst-Version.

5 Ein selbstgemachter Zähler und Anzeiger

Im Simulator ein neues Projekt anlegen Um so einen 15-Abwärtszähler selber zu machen, starten wir mit avr_sim (woher man das kriegt siehe hier) ein neues Projekt und füllen in dem Fenster alle rot markierten Felder entsprechend aus (Auswahl des AVR wie oben gezeigt). Wenn wir jetzt "Ok" drücken, dann kriegen wir ein Fenster, mit dem wir die Verpackung des ausgewählten AVR auswählen können.

Der Editor im Simulator Haben wir auch das mit Bravour und "Ok" absolviert, dann begrüßt uns dieses Fenster. Die vielen Semikolons sagen uns, dass das Programm im Wesentlichen aus Kommentaren besteht. Das macht der anständige Assembler-Programmierer immer so, dass die Hälfte seines Quelltextes nur für ihn selbst (und seine Leser) gemacht ist, weil er in vier Wochen sonst nicht mehr weiß, was er da gemacht hat.

In diesem Fenster tragen wir im Kopf schon mal die richtige Beschreibung des Projekts ein. Dann stellen wir den Cursor in die Zeile, in der mal " (F2 adds ASCII pin-out for device here)" geschrieben stand, löschen diese Zeile (markieren und Entf) und drücken dann die Taste F2.

Kopfeintraege Und so wissen wir auch noch in vier Wochen, wozu das alles gut war.

Die Interruptvektoren des ATtiny24 Gehen wir mit dem Cursor oder mit dem rechten Schieberegler ein Stück nach unten, dann fallen uns diese Zeilen hier ins Auge. Hier hat avr_sim schon mal die ersten Befehle hingeschrieben: den Reset- und die Interruptvektoren im ATtiny24. Was Interruptvektoren sind und wozu man sie braucht, kommt weiter unten. In der ersten ausführbaren Zeile (ausführbare Zeilen haben in der linkesten Eintragsspalte ein "E", das hat der Editor da hin gemacht), die "rjmp Main ; Reset vector" lautet, steht der Befehl für einen relativen Sprung (RJMP für Relative JuMP) und das Sprungziel, in diesem Fall "Main". Wenn wir weiter nach unten blättern, dann sehen wir eine Zeile mit "Main" und einem ":". Das ist also hier das Sprungziel.

So ein Controller fängt immer bei Null an zu arbeiten, wenn man ihm Strom gibt. In diesem Fall findet er an der Adresse 0 diesen Sprungbefehl und hüpft genau dort hin.

Der Programmloop Da, wo er hinspringt, bei Main:, stehen diese Zeilen herum. Die ersten beiden Befehle kriegen wir später, das "sei" auch. Dann folgt aber wieder ein Sprungziel (Label), diesmal "Loop:", und dahinter "rjmp Loop". Das macht jetzt mit dem Herrn Prozessor, dass der immer im Kreis rennt und immer nur wieder rjmp Loop ausführt und dann wieder selbiges.

5.1 Zählen mit Controller

Den Zaehler mit 15 laden Das ist das Schicksal des Prozessors: immer nur im Kreis rennen und nichts anderes tun. Damit er was Sinnvolles zu tun kriegt, fügen wir die beiden rot umrandeten Zeilen ein. Die sagen, er solle bitte schön in sein Register R17 (haben täte er 32 verschiedene Register oder 8-Bit-Speicherplätze, aber nur die ab 16 gehen mit LDI) die Dezimalzahl 15 schreiben. Wenn wir jetzt im Menue von avr_sim auf "Assemble" drücken, kriegen wir das Ganze in Maschinensprache übersetzt.

Der Assembler uebersetzt in Maschinensprache Und das sollte jetzt erscheinen: keine Fehler. Die Warnung des Assemblers gavrasm können wir ignorieren, sie wäre nur in Spezialfällen wichtig, die wir hier aber gar nicht haben.

Das Assembler-Listing Nachdem wir "Ok" gedrückt haben, beglückt uns der Assembler mit einem weiteren Eintrag im Tabulator: er hat seine wertvolle Tätigkeit verrichtet und möchte uns von seinen Erfahrungen dabei berichten. Das macht er in seinem Listing, das immer mit .lst endet und eine hundsordinäre Textdatei auf der Festplatte ist.

Der uebersetzte Befehl Aus dem "ldi R17,15", unserer ersten selbstgemachten Assembler-Codezeile, hat der Assembler nun das hexadezimale 0xE01F an der Adresse 0x0013 gezaubert. Was das nun heißen soll und wie er drauf kommt, sollte uns erst mal egal sein (wir wollen ja nicht unbedingt in die Untiefen der Assembler-Programmierung abtauchen).

Die Simulation Immer, wenn wir fehlerfrei assembliert haben, gibt es im avr_sim-Menue den Eintrag "Simulate". Drücken wir den, dann öffnet sich das Simulationsfenster und der blaue Pfeil im Listing zeigt uns den näschten Befehl, der bei schrittweiser Ausführung ausgeführt werden wird.

Schrittweise bis zum LDI Drücken wir auf den Eintrag "Step" im Simulatorfenstermenue, dann wird der Befehl ausgeführt. Beim ersten Drücken landet der blaue Cursor in der Zeile hinter "Main:", nach noch zwei mal "Step" ist dann unser Ladebefehl am dransten. (Wie wir den Pfeil rot statt blau kriegen, davon im Fortgeschrittenen-Kurs im Handbuch vom avr_sim.)

Nach dem Ausfuehren des LDI Und wenn wir den ausführen lassen, dann sehen wir im R17 das Ergebnis. Stellen wir die Maus auf R17, dann sagt uns das kleine Pop-Up-Fenster, dass R17 (0x0F) dezimal 15 ist.

Dekrementieren und zurueck Nun, das Abwärtszählen geht in Assembler ganz einfach. Einfach nur "dec R17", dann dekrementiert der Controller den Wert in R17. Wollten wir aufwärts zählen, dann wäre es "inc R17" für Inkrement (unser U/D-Pin am CMOS-IC 4510 oder 4516 ist hier einfach nur ein anderer Befehl).

Aber, das ist jetzt etwas verzwickt, denn der Controller liefe nach dem dec einfach wieder in seine Endlosschleife und zählt nur bis 14, aber nicht weiter. Damit er weiter abwärts zählt, müssen wir ihn nach dem DEC wieder nach oben bugsieren. Und das kennen wir schon: mit einem Label/Sprungziel und einem RJMP.

Wenn wir jetzt mit jedem zweiten Schritt den Wert um Eins abwärts zählen, dann kriegen wir nach 30 Einzelschritten Null in das Register R17. Und was kommt nach Null? Natürlich Minus 1. Und das ist hexadezimal 0xFF oder dezimal 255: das Register fängt einfach von alles mit Eins (255) wieder an abwärts zu zählen.

Dekrementieren Damit das korrekt abläuft und der Zähler bei unter Null wieder bei 15 beginnt, müssen wir feststellen, ob R17 gleich 255 geworden ist. Das machen wir mit dem Befehl "cpi R17,0xFF". Das steht für ComPare Immediate und vergleicht das Register mit der Konstante dahinter.

Aber, wo steht nun das Ergebnis des Vergleichs? Dafür hat jeder AVR das Statusregister, das im oberen Teil des Simulationsfensters abgebildet und mit SREG abgekürzt ist. In diesem SREG-Register gibt es das Z-Bit (für Zero): es sagt, ob beim Vergleich Gleichheit herauskam (Z=1) oder nicht (Z=0). Jetzt brauchen wir nur noch einen Befehl, der diese Z-Flagge abfragt und bei Gleichheit um einen Befehl weiter nach oben springt (und dann das R17 wieder auf 15 stellt) oder halt nicht. Der Befehl heißt "BREQ Label" (BRanch on EQual) und ist ein bedingter Sprung.

Wenn jetzt aber der bedingte Sprung nicht ausgeführt wurde, weil 0xFF noch nicht erreicht wurde, kommt wieder unser Sprung zum Abwärtszählen zur Ausführung. Das Gegenteil von BREQ heißt übrigens BRNE (BRanch on Not Equal). Alle anderen Sprungbefehle beim AVR gehen mit BRcC und BRcS (mit c als dem Namen des Bits im SREG), nur beim Z haben die beiden Norweger sich was anderes ausgedacht, so dass es BRZC und BRZS im AVR-Befehlssatz nicht gibt. Wäre ja auch langweilig ...

Man kann das aber auch anders lösen. Man macht das Abziehen von Eins nicht mit DEC, sondern mit SUBI R17,1. Das subtrahiert die Konstante 1 vom Registerinhalt in R17. Wenn beim Abziehen ein Unterlauf stattfindet, was der Fall ist, wenn man von Null eine Eins abzieht, dann setzt die Recheneinheit das Übetrags-Bit Carry C im Statusregister auf Eins. Der Abfrage- und Sprungbefehl heißt jetzt BRCS (BRanch if Carry Set). Das spart uns den Vergleichsbefehl CPI, der Sprung erfolgt dann sofort nach dem SUBI. Weil es aber keinen ADDI-Befehl gibt, geht das nur beim Abwärtszählen, mit dem wir es hier zu tun haben. Wer dem Controller ein Befehlswort und eine Mikrosekunde Arbeit ersparen will, nimmt SUBI und BRCS ...

Der AVR bietet aber auch noch mehr Möglichkeiten als nur das CPI R17,0xFF oder das SUBI R17,1. Wenn man sich die Flaggen anschaut, die ein DEC r17 so setzt (z. B. im Instruction Set Manual von Microchip), dann sieht man, dass bei dieser Instruktion die S-Flagge gesetzt wird, wenn das Register beim Dekrementieren von Null auf 255 unterläuft. Wir brauchen also gar nicht das CPI R17,0xFF, weil die S-Flagge das auch schon anzeigt. Die S-Flagge heißt ausgeschrieben "Signed-flag" und wird immer dann gesetzt, wenn das Bit 7 des Ergebnisses Eins wird. Das ist der Fall, wenn von Null auf Minus Eins dekrementiert wird. Auch für die Auswertung des S-Bits gibt es einen bedingten Sprungbefehl: er lautet BRMI für BRanch on MInus (im Gegensatz zu BRPL für Plus). Geht dann genauso wie beim SUBI, aber mit BRMI statt mit BRCS. Viele Wege führen nach ... wohin bloß?

Der Zaehler bei der Arbeit Man assembliere das und klicke das "Step" immerzu an. Oder man trägt ganz einfach in das Eingabefeld "Update ..." eine 1 und in das Feld "Delay ..." eine 1000 ein und drückt im Menue auf "Run/Go". Dann läuft es wie von selbst und man kann die Z-Flagge im Statusregister beim CPI dabei beobachten, wie sie manchmal, aber ganz selten, angeht. Ebenso, wenn wir SUBI mit der Carry-Flagge oder DEC mit der S-Flagge beobachten.

Aber aufpassen! Die nebenstehende Animation zeigt nicht alle Einzelschritte an, sodass man bei SUBI die C-Flagge und bei DEC die S-Flagge hier nicht sieht. Beim Ausführen im avr_sim im Einzelschrittverfahren sieht man die schon. Ich wollte hier dem animierten GIF ein paar kB sparen.

Man merke, dass unser 4510-/4516-Zähler nun aus ganzen fünf oder sechs Zeilen Programmcode besteht. Wie uns das Assemblieren mitteilt, sind das ganze 2,5% des im ATtiny24 vorhandenen Flash-Speichers. Da ist also noch genug Platz übrig für die nächste Aufgabe: aus dem Zählerstand eine Hexadezimalausgabe an die Anzeigen-Portpins zu basteln.

5.2 ... und die Übersetzung in 7-Segment

Die 7-Segment-Ausgabe soll an den Portbits PA0 bis PA6 erfolgen. Die sollen also Ausgänge sein und die Pins sollen die Segmente a bis g mit Strom beglücken, wenn sie beim Zählerstand an sein sollten.

Dafür haben alle Ports in den AVRs ein Datenrichtungsregister namens DDRp. Das DDR steht für Data Direction Register, das p steht für den Portnamen, also in unserem Fall A. Schreibt man eine Eins in ein Bit dieses Registers, dann ist der Pin ein Ausgang. Ob er Eins oder Null ist, steht im Portregister PORTp: eine Eins macht den Ausgang high, eine Null low.

Port A als Ausgang und Low Mit diesen ganzen großartigen vier Zeilen Code bringen wir den Port A mit seinen Bits PA0 bis PA6 dazu, Ausgang zu sein und wir setzen diese Pins auf Null, so dass eine extern angeschlossene Siebensegment-Anzeige mit gemeinsamer Kathode aus ist. Was müssten wir daran ändern, wenn es eine mit gemeinsamer Anode wäre?

Port A nach dem Init Wenn wir das nun assemblieren und simulieren und im Simulationsfenster das Auswahlfeld "Ports" anmachen, sehen wir dieses hier links. Der Port A ist jetzt laut DDRA Ausgang (bis auf PA7) und seine Pins in PORTA sind alles Nullen. Wenn wir nun die Pin-Eingänge mit "IN R16,PINA" in das Register R16 einlesen würden, dann kriegten wir lauter Nullen. Bis auf PINA7, das würde in der Realität klappern, weil es ein hochohmiger Eingangspin ist. Im Simulator hier klappert nix, aber wir könnten es im Simulator klappern lassen, wenn wir mit der Maus auf PINA7 klicken würden. Das machen wir aber erst nachher, wenn uns auch einer zuhört und daraus Action macht.

In der Schleife, in der unser Zähler in R17 immerzu abwärts gezählt wird, müssten wir nun für jeden der Zustände von R17 die Bits 0 bis 6 so an PORTA ausgeben, dass die Segmente a bis g entsprechend an oder aus sind. Das könnten wir natürlich mit CPI und BREQ erledigen, indem wir bei CPI R17,15 und BREQ an die Stelle springen, an der LDI R16,113 und OUT PORTA,R16 steht. Danach müsste das Ganze mit RJMP wieder an die Stelle hinter der ganzen Vergleicherei hinspringen. Nur der Horror, das 16 mal wiederholt, aber mit verschiedenen Zählerständen und mit verschiedenen Zahlen für Port A, hinschreiben zu müssen, hält uns davon ab und lässt uns über eine einfachere Alternative sinnieren.

Code zur Ausgabe Das ist nun alles an Um- und Zubau, was wir für ein ordentliches und kurzes Programm brauchen. Zu allererst machen wir hinter Loop: eine Tabelle in das Flash. Die Tabelle besteht aus einzelnen Bytes, die hintereinander, mit Kommas getrennt, die dezimalen Ausgaben angeben, die beim Zählerstand 0, 1, ... bis 15 die Anzeige ausgeben soll.

Die Tabelle im Flash Solche Tabellen legt der Assembler dann im Flashspeicher an der aktuellen Stelle an (hier war es die Adresse 0x0025). Im Assembler-Listing ist an dieser Stelle 0x063F, 0x4F5B, usw. abgelegt. Das bedeutet, dass
  1. der Programmspeicher in AVRs immer 16 Bits breit ist, jede Adresse immer ein Wort mit 16 Bits bezeichnet (ein LSB und ein MSB),
  2. das erste angegebene Byte (63) in das LSB kommt, das zweite angegebene Byte in das MSB,
  3. beim Lesen aus dem Programmspeicher mit LPM (Load from Program Memory) immer angegeben werden muss, ob das LSB oder das MSB gelesen werden soll.
Weil für diese Angabe ein zusätzliches Bit gebraucht wird, wird beim LPM die 16-Bit-Zeigeradresse in dem Doppelregister Z (= R31:R30) verdoppelt (2 *). Die Funktionen High und Low liefern dabei das MSB und das LSB der verdoppelten Adresse zurück, um diese in die Register R31 (High) und R30 (Low) zu schreiben.

Damit nun jedes Zählerbyte in R17 auf den richtigen Tabelleneintrag zeigt, wird R17 zu dieser Tabellenadresse hinzu addiert, was mit ADD ZL,R17 geht. Weil aber Z auch an einer Adresse liegen kann, die beim Addieren auch das MSB erhöht, wird ein weiteres Bit im Statusregister SREG dazu benutzt, um solche Überträge zu handhaben: das Carry- oder Übetrags-Bit C. Zum ZH wird das Carry hinzugezählt, wenn mit ADC eine Null addiert wird.

Der Befehl LPM R16,Z holt nun das Byte, das an der Adresse Z, geteilt durch zwei, steht in das Register R16. Ist das Bit 0 im ZL (=R30) Eins, dann holt er das MSB, wenn nicht dann das LSB. Das R16 brauchen wir jetzt nur noch mit OUT PORTA,R16 in das Ausgangsregister von Port A zu schreiben und fertig ist unsere Übersetzungsmaschine von Binär- in Siebensegment-Code.

Hexadezimal 0F nach 7-Segment Ist die Übersetzungsmaschine beim ersten Schreiben angekommen und hat es ausgeführt, dann steht im Port A diese Kombination herum. Und sie ist korrekt: das 0x0F macht die Segmente a, e, f und g an, was ein "F" auf die Anzeige zaubert.

Simulieren wir jetzt weiter, kriegen wir nacheinander 14 ("E"), 13 ("d"), usw., in Siebensegment-Kodierung gezeigt.

Aber damit sind wir noch nicht fertig: der Controller soll ja nur auf Tastendrücke reagieren und nicht mit vielen 10 kHz nacheinander über die Anzeige flitschen lassen. Das würden wir nämlich jetzt kriegen, wenn wir die Hex-Datei, die der Assembler nebenbei auch produziert hat, in das Flash schreiben würden und wenn wir, wie im Schaltbild oben, eine Anzeige anschließen würden.

5.3 ... und auf Tastendruck

Den Pull-Up-Widerstand einschalten Die Taste für den Zähleingang ist über das RC-Glied an den Pin PB2 des Controllers angeschlossen (siehe Schaltbild oben). Damit der Pin seinen internen Pull-Up-Widerstand einschaltet und damit den Elko lädt, kriegt er den PORTB2 auf Eins und das Richtungsregister DDB2 auf Null gestellt.

Das hier mit CBI und SBI ist die Alternative zum OUT-Befehl: man kann einzelne Bits in einem Port mit CBI auf Null (Clear-Bit-I/o) und mit SBI (Set-Bit-I/o) auf Eins setzen.

Den Pull-Up-Widerstand einschalten Und das ist der Effekt, wie ihn uns die Port-Anzeige zeigt. Wenn wir den Hexcode jetzt in den Chip brennen, würden wir am Pin PB2 +5V messen (wenn wir die Schaltung mit 5 Volt betrieben). Physikalisch aber über einen hohen Widerstand, so dass eine angeschlossene Taste den Eingang locker auf Null herunter kriegt.

Und seine große Stunde kam, als er den Interruptvektor nahm. Wir laufen jetzt nicht immer nur im Kreis herum und warten mit IN PINB und CPI darauf, bis der User mal endlich die Taste drückt. Können täten wie das schon, aber das wäre unter unserer Würde. Für so was haben wir die oben schon gezeigten Interruptvektoren.

So ein AVR kann nämlich auf Tastendruck seine sonstigen Arbeiten unterbrechen (Interrupt), führt dann zeitweise mal was anderes (die Interrupt-Service-Routine ISR) aus und kehrt danach wieder an genau die gleiche Stelle zurück, an der die Ausführung unterbrochen wurde. Dieses Können macht AVRs, obwohl sie nur mit wenigen MHz getaktet werden, zu schnelleren Rechnern als es ein PC mit einem lahmarschigen Betriebssystem und einigen GHz an Takt kann: so schnell kann keiner, auch kein PC, reagieren.

Möglich macht das der folgende Mechanismus:
  1. In einem Portregister (MCUCR) stellt man ein, ob der INT0-Eingangspin nur auf fallende oder nur auf steigende oder gar auf beide Flanken reagieren soll.
  2. In einem anderen Portregister (GIMSK) stellt man den INT0-Interrupt scharf, indem man sein Enable-Bit auf Eins setzt.
  3. Irgendwann setzt man mit dem Befehl SEI die I-Flagge im SREG auf Eins. Solange die auf Null steht, vollführt der Controller keine Interrupts, die verhungern einfach alle.
  4. Jetzt checkt der Controller, welche Interrupts eigentlich anstehen, da er ganz viele davon kann. Dazu haben alle Interrupts ein Flaggenbit, das auf Anforderung gesetzt wird. Der so aktivierte Interrupt, der am höchsten in der oben gezeigten Tabelle steht, ist jetzt am dransten, alle anderen mögen solange warten, bis der hier fertig ist. Dafür, dass kein anderer dazwischen kommen kann, wird jetzt die I-Flagge ausgeschaltet. Und auch gleich die Aktivierungsflagge des Interrupts, der jetzt gerade behandelt wird.
  5. Zu Beginn der Behandlung legt der Prozessor die aktuelle Ausführungsadresse in seinem statischen RAM (SRAM) ab, denn er muss ja nach dem Ereignis dorthin auch wieder zurück. Der Adressbereich, an dem er die Adresse ablegt, ist ganz am Ende des SRAM. Die Adresse wird irgendwann in das Portregister SPL (bei mehr als 256 Bytes SRAM in SPH:SPL) geschrieben, die Stapelzeiger heißen. Womit auch die beiden Instruktionen hinter Main: ihren Sinn kriegen: sie setzen den Stapelzeiger auf das Ende des SRAMs.
  6. Dann springt die Programmausführung an die für den betreffenden Interrupt festgelegte Adresse (ab 0x0001 aufwärts). Dort muss ein Sprungbefehl stehen, der zur Interrupt-Service-Routine führt. Dort wird dann weiter gearbeitet.
  7. Ist die Bearbeitung dort abgeschlossen, wird sie mit dem Befehl RETI beendet. Das setzt die I-Flagge im SREG wieder auf Eins, holt die Rücksprungadresse vom Stapel und macht dort wieder weiter, als ob zwischendurch gar nix anderes passiert wäre.
Interruptvektoren und ISR Der Code rechts ist schon fast alles, was man so braucht: der RETI beim INT0 wird durch einen RJMP zur ISR ersetzt, unser Code für das Zählen und die Ausgabe wird, ein wenig modifiziert, zur Interrupt-Service-Routine für den INT0-Interrupt. Neu sind die beiden RETI, mit denen die ISR abgeschlossen wird: sie sorgen dafür, dass die I-Flagge im SREG am Ende wieder gesetzt wird.

Das Aktivieren des INT0-Interrupts Mit dem Code links wird der INT0 auf fallende Flanken eingestellt und dessen Enable-Bit gesetzt. Ferner ist noch der Schlafmodus auf Idle eingestellt (mit 1<<SE für Sleep-Enable). Die beiden < heißen nicht, wie im normalen Leben "sehr viel kleiner als ...", sondern teilen dem Assembler mit, er solle die 1 davor bitte SE-mal nach links schieben. Das | zwischen dem ISC-Bit und dem SE-Bit macht ein ODER mit beiden Zahlen (die nun mal beide im MCUCR herumstehen und deswegen auch gleichzeitig beschrieben werden wollen).

Wenn die Feierlichkeiten zu Beginn abgeschlossen sind, wird der Prozessor abschließend in sein Bett gelegt (mit SLEEP) und kann ratzen, bis er wieder mal gebraucht wird und dann vom Interrupt aufgeweckt wird. Danach geht es wieder ins Bett. Das spart ein paar mA an Stromverbrauch, weil der Controller nur arbeitet, wenn es auch was zum Arbeiten gibt. Die Herumrennerei in elendigen Schleifen mit immer dem gleichen (negativen) Ergebnis ist damit zu Ende.

Der INT0-Interrupt ist aktiviert Wenn wir das Programm ausführen, bis der SLEEP eintritt, dann sieht der Port B nun so aus. Der INT0-Pin ist jetzt rot auf grau, d.h. er ist scharf geschaltet. Wenn wir jetzt mit der Maus auf das entsprechende Bit mit dem Pull-Up-Symbol klicken, wird dieses Bit Null und der INT0 damit ausgelöst.

Das Ausloesen des INT0-Interrupts Das macht sich durch einen erneuten Farbwechsel des INT0-Bits bemerkbar: die INT0-Interruptflagge ist gesetzt. Nach ein, zwei oder auch drei Einzelschritten springt der Ausführungscursor dann vom SLEEP zum Interruptvektor und von da aus in die ISR.

Der INT0-Interrupt wird bearbeitet Jetzt ist er aber dran und wird rot. Das bleibt er auch solange, bis ein RETI ausgeführt wird.

Das war es schon mit unserer Aufgabe. Das fertige Programm braucht ungefähr ein Achtel des verfügbaren Speichers im ATtiny24 und macht alles so wie es soll.

Brennen wir den Hexcode in den Chip, dann haben wir uns einen 4-Bit- Hex-Zähler mit Siebensegment-Decoder selbstgestrickt, den es so gar nicht zu kaufen gibt. Der Chip spricht damit genau unsere Sprache und macht genau nur das, was wir ihm in Assembler beigebracht haben. Optimaler geht nicht, und das in einem einzigen popeligen 14-poligen IC.

So, viel Neues gelernt bis hier. Die Chips können aber noch viel, viel mehr, weil sie noch ganz viel weitere spannende und vielseitige Innereien haben, die man mit ein paar wenigen Zeilen Programmcode aktivieren und einsetzen kann. Wer noch mehr wissen will, geht nach hier oder schaut sich mal hier um, da gibt es noch mehr Stoff zum Lernen, Programmieren und zum Basteln.

Und, apropos Assembler lernen: hier gibt es noch mehr kommentierte Links zu Assembler-Tutorials.

6 Bilder und Videos vom Controller

6.1 Bilder

15-Zaehler auf dem Breadboard 15-Zaehler mit Lampentest So sieht der 15-Zähler auf dem Breadboard aus. Ich habe zwei gemeinsame Anoden-Anzeigen mit 470Ω verwendet. Rechts ist PB0 auf GND gelegt und der Lampentest macht alle Lampen an.

6.2 Videos

So zählt der Zähler mit zwei Anzeigen abwärts.

Und so sieht es bei einer Anzeige aus.



Hier ist der Chip auf Aufwärtszählen umgestellt.


7 Fazit und Schlussfolgerungen

Mach doch, was Du willst, alles geht. Wenn Du lieber vier statt einem einzigen IC das Zählen und Anzeigen beibringen willst, nur los. Beschwer' Dich nicht, wenn Du beim Löten von 68 Bauteilen Verdrahtungsfehler machst und die mühsam suchen und beseitigen musst, mit dem ATtiny24 wäre das alles nicht passiert (der braucht nur 17 Bauteile für genau den denselben Effekt).

Wem das Selberprogrammieren zu mühsam ist, hat selbst Schuld. Wem nicht, dem steht eine ganze neue Welt offen. Und nach dem vierten selbstgestrickten Schächtelchen bleibt die CMOS-Bastelkiste meistens zu, weil alles viel mühsamer ist als es schnell mal zu programmieren, im avr_sim auszuprobieren und mit dem AVR-Brenner in den 6-/8-/14-/20-/28- oder 40-poligen Chip einzuprogrammieren.

Wer jetzt einfach nur den fertig programmierten Chip haben will, der schickt mir per Post einfach nur einen an sich selbst adressierten und frankierten gepolsterten Umschlag zu, legt für ca. 3 Euro frische Briefmarken bei und teilt mir auf einem beigelegten Zettel die drei Konstanten mit, die im Quellcode unter "Adjustable constants" stehen. Dann klappt's auch mit dem Programmieren und dem Versand.

©2023 by Gerhard Schmidt