Dieses Beispiel zeigt die Verwendung der 1-Wire-Bauteile DS1994 (RTC) und DS1820 (Temperatursensor). Dabei ist durch den ROM-Search-Algorithmus die Anzahl der Temperatursensoren beliebig erweiterbar. Als AVR wird der ATTINY12 verwendet, da durch die serielle Datenübertragung nur wenige Leitungen benötigt werden. Es ist sogar noch ein Portpin (PB5) unbenutzt. Dort könnte man z.B. einen 2. Taster anschließen um die Anzeige zwischen mehr als 2 Temperatursensoren umzuschalten.
Dieses Programmbeispiel soll gleichzeitig die Grundzüge der modularen Programmierung verdeutlichen. Gerade Anfänger schreiben häufig seitenlange Programme (sogenannter Spaghetticode), wo fast identische Aktionen mehrmals wiederholt hingeschrieben werden, statt sie in einer Schleife auszuführen oder in Unterprogrammen zusammen zu fassen.
Eine einfache Methode der Modularisierung ist das Schreiben der einzelnen Funktionen in separate Dateien. Für den Assemblierungsvorgang können diese einzelnen Dateien mit dem ".include"-Befehl bequem zusammengefaßt werden, d.h. sie werden genau so wie eine einzige große Datei assembliert.
Für den Programmierer ist es aber wesentlich leichter solche einzelnen Dateien in andere Programme mit einzufügen, wenn sie dort benötigt werden. Bei einer einzigen riesenlangen Datei ist dagegen die Gefahr groß, daß man zuviel oder zuwenig löscht bzw. reinkopiert und dann umständlich den Fehler suchen muß.
Das Schreiben der Funktionen in separate Dateien verhilft auch zu einer besseren Strukturierung des Programms, da man sich überlegen muß, wie der Datenaustausch und die Zusammenarbeit mit anderen Funktionen erfolgen soll. Auch bleiben die Funktionen klarer abgegrenzt, da man nicht zwischen den Funktionen kreuz und quer springen kann. Dadurch sind dann die einzelnen Module universeller, d.h. sie lassen sich leichter in anderen Programmen weiter verwenden.
Spätestens dann, wenn man einige Zeit etwas anderes gemacht hat und nun doch wieder an einem alten Programm etwas ändern will oder eine Funktion aus diesem Programm woanders benötigt, merkt man schnell, daß man in einem ellenlangen Spaghettikode einfach nicht mehr durchsieht.
In ein modulares Programm braucht man aber nie mehr komplett einzusteigen. Man muß sich nur mit dem Programmteil beschäftigen, den man ändern oder nachnutzen will.
Ein guter Programmierstil ist es, die Programme auch immer in der chronologischen Reihenfolge zu schreiben, d.h. eine Programmroutine wird immer oben angesprungen und unten mit einem RET verlassen. Auch wenn z.B. in einem Fehlerfall eine Routine früher beendet werden muß, sollte das nicht mit einem RET mittendrin passieren sondern mit einem Sprung zu dem RET am Ende.
Dann kann man nämlich diese Funktion bequem erweitern und muß sich nicht mehr wundern, warum diese Erweiterung gar nicht ausgeführt wird, nur weil die Funktion durch ein RET in der Mitte des Kodes schon längst verlassen wurde.
Eine Besonderheit des ATTINY12 ist es, daß er nur einen begrenzten Hardwarestack hat. Das muß einen aber nicht am modularen Programmieren hindern. Deshalb habe ich das Macro "INCALL" definiert, welches durch einen Sprung die einzelne Routine aufruft, die dann wieder durch einen Sprung zurückkehrt. Somit hat man den Vorteil der modularen Programmierung aber es wurde kein Stack-Level verbraucht. Der Nachteil ist aber, daß solche Funktionen nur einmal aufgerufen werden können, da sie ja nur an eine Stelle zurückkehren können. Das ist jedoch typischer Weise bei den Funktionen, die im Main aufgerufen werden, immer der Fall.
Die Verwendung des Macros macht es auch leicht, auf anderen AVRs mit richtigem Stack das dann durch einen CALL zu ersetzen. Der ATTINY12 soll ja zukünftig durch den ATTINY13 abgelöst werden, wo dann die Einschränkungen durch den Hardwarestack wegfallen.
Damit die Unterprogramme auch wirklich universell sind, sollte man so oft wie möglich mit Definitionen arbeiten. Z.B. werden sämtliche Anschlüsse des ATTINY12 über Definitionen verwendet. Dadurch ist es jederzeit möglich, die Anschlüsse umzulegen, wenn sich z.B. dadurch ein günstigeres Platinenlayout ergibt. Oder auch, wenn in einem anderen Programm die Zuordnung eine andere ist (anderer AVR, Konflikte mit den Sonderfunktionen bestimmter Pins).
Auch Konstanten innerhalb des Programms sollten nicht als Zahl eingegeben werden, wenn sie sich z.B. bei anderer Quarzfrequenz mit ändern. Auch kann der AVR-Assembler Berechnungen bis 32Bit für die Konstanten selber durchführen. Z.B. sind die benötigten Verzögerungen beim LCD oder 1-Wire-Bus über Berechnungen aus der Quarzfrequenz definiert. Man muß also nur z.B. 1.2MHz beim ATTINY12, 1.6MHz beim ATTINY15 oder 2MHz beim ATTINY26 angeben und schon werden die Verzögerungszeiten korrekt neu berechnet.
Da jedoch die Zeiten der Einfachheit halber nur über 8-Bit Register realisiert wurden, kann es bei weit abweichenden Quarzfrequenzen zu Fehlern kommen. Z.B. für einen ATTINY26 bei 16MHz muß man diese Verzögerungsschleifen neu dimensionieren.
Diese Programmiertips entstanden rein aus meiner praktischen Erfahrung. Fast
jeder fängt nach dem Motto an "Das Genie beherrscht das Chaos".
Aber wenn man nicht nur ein einziges Programm schreiben will, merkt jeder
bald: "Mensch, das hast Du doch 1000-mal schon so ähnlich gemacht, muß
man wirklich jedesmal das Rad neu erfinden ?".
Mit anderen Worten, je früher man mit der modularen Programmierung
anfängt, umso schneller und umso mehr zahlt es sich aus.
Das Kernstück der Schaltung ist wie gesagt der ATTINY12. Als Display
dient das EA DIP162 von Conrad. Zur Ansteuerung des Displays wird ein 74HC164
Schieberegister verwendet. Da die Daten nur übernommen werden, wenn
der E-Pin des LCD auf high ist, kann man die übrigen beiden Leitungen
auch noch anderweitig verwenden. Die CLK-Leitung des Schieberegisters wird
daher mit als 1-Wire Leitung zum RTC-Chip DS1994 verwendet.
Die Leitung zu den Temperatursensoren kann ja länger sein, bzw. da
können auch mal Störungen einkoppeln oder Kurzschlüsse auftreten.
Damit dann wenigstens die Zeit richtig angezeigt wird, liegt die RTC an einer
separaten Leitung.
Das Programm ist in einzelne Module aufgeteilt:
Das ist das Hauptprogramm, in dem alle Module per ".include" zusammengefaßt werden. Das Hauptprogramm besteht im Wesentlichen nur aus Funktionsaufrufen. Dadurch ist sehr leicht die grundsätzliche Funktion und der Ablauf des ganzen Programms erkennbar.
Am Anfang stehen die Definitionen für die Startzeit und Datum. Diese werden dann später in einen 32-Bit Wert umgewandelt und damit der RTC beim allerersten Einschalten gestartet. Soll später ein Neustart mit einer anderen Zeit erfolgen, muß außerdem der Wert "rtc_magic" geändert werden.
Danach werden die entspechenden Definitionsfiles eingebunden, in denen die ATTINY12 Hardware, die Registerverwendung und die Anschlußzuweisung erfolgt. Außerdem werden noch einige Macro-Definitionen eingebunden (das oben erwähnte "INCALL"-Macro, sowie Macros zur LCD-Ausgabe). Es sind die folgenden Dateien:
Danach erfolgt als erster Programmcode der Sprung zur Initialisierung und die Interrupthandler. Da nur ein Interrupt verwendet wird, kann man den Interrupthandler direkt an die Stelle des Interruptvektors einfügen. Muß jedoch hinter dem Interruptvektor ein weiterer Interrupt behandelt werden ist der eigentliche Interrupthandler durch einen RJMP anzuspringen.
Nach der Sprungmarke INIT: stehen dann alle Aktionen, die nach jedem Reset einmalig auszuführen sind. Als erstes wird das Calibrationsbyte gelesen und damit der interne RC-Oszillator auf die Sollfrequenz gesetzt. Dann werden alle benötigten I/O-Register gesetzt und die Variablen initialisiert.
Dann wird das erste mal der 32-Bit Wert aus dem RTC ausgelesen und es erfolgt
die Initialisierung des LCD. Durch die Verwendung einer Leitung gemeinsam
für das LCD und die RTC ergibt sich eine Besonderheit:
Ein Takten dieser gemeinsamen Leitung von der LCD-Routine würde auch
die RTC empfangen und da diese Leitung bidirektional ist, den Takt des LCD
stören. Damit dies nicht erfolgt, wird nach jedem Zugriff auf die RTC
an diese das Write-Scratchpad Kommando gesendet. Damit ist die RTC der
Datenempfänger, d.h. sie ist quasi hochohmig. Das dann durch das LCD
wirre Daten in das Scratchpad geschrieben werden ist egal.
Deshalb also die Reihenfolge, das zuerst die RTC ausgelesen wird und danach
das LCD initialisiert wird.
Als nächstes erfolgt dann die Umwandlung des 32-BIT RTC Wertes in die aktuelle Zeit. In der Hauptschleife MAIN wird dann nur noch das niederwertige Sekundenbit ausgewertet und jedesmal, wenn es wechselt, wird die Uhren- und Datumsroutine weitergeschaltet.
Weiterhin werden in der Hauptschleife Uhrzeit und Wochentag auf dem LCD angezeigt. Mit dem Taster kann in der 2. Zeile des LCD zwischen Anzeige des Datums oder Anzeige der beiden Temperaturen umgeschaltet werden. Zusätzlich zum Weiterschalten der Uhrzeit wird jede Sekunde das Ergebnis der vorhergehenden Temperaturmessung ausgelesen und eine neue Temperaturmessung gestartet. Während der Temperaturmessung wird der 1-Wire-Anschluß auf High gesetzt und als Ausgang geschaltet. Damit wird dann die "Parasite Power" für die Sensoren bereit gestellt.
Das ist der Timer0-Overflow-Handler und dient zum Entprellen der Taste. Dazu werden 4 Bits verwendet, wovon 2 Bits eine Zähler bilden, der solange aufwärts zählt, wie der Tastenzustand ungleich dem Bit "key_state" ist. Das Bit "key_state" entspricht dann dem entprellten Tastenwert und beim Wechsel von Losgelassen nach gedrückt wird zusätzlich das Bit "key_press" gesetzt. In der Hauptschleife wird dann zwischen Datums- und Temperaturanzeige umgeschaltet und das Bit "key_press" gelöscht.
Das ist ein vollständiger universeller 1-Wire Treiber, der es gestattet mehrere 1-Wire-ICs zu verwenden. Dazu ist der ROM-Search Algorithmus implementiert. Damit Fehler auf dem 1-Wire-Bus erkannt werden können, wird bei jedem Bus-Reset Kommando auf Kurzschluß und ob überhaupt mindestens ein 1-Wire-IC present ist getestet. Z.B. wird im Fehlerfall erst gar nicht die Parasite Power wärend der Temperaturmessung zugeschaltet
Um beim ROM-Search auch alle angesclossenen ICs zu finden, muß die Adresse des letzen gefundenen ICs gemerkt werden und auch die Nummer des Bits, welches als nächstes aufgelöst werden muß. Zum Start der Suche muß dieser Wert auf SEARCH_FIRST gesetzt werden. Wurden alle Bits aufgelöst, d.h. das letzte IC gefunden, dann steht dieser Wert auf LAST_DEVICE und das ROM-Search kann abgebrochen werden.
Da die RTC als einziges 1-Wire-IC an einem separaten Draht hängt, wurde ein 2. Treiber geschrieben. Ein Presence Detect ist, wie auch der Test auf Kurzschluß, nicht nötig, die RTC ist ja direkt auf die Platine gelötet. Deshalb wird auch nach jedem Rest das SKIP-ROM Kommando gesendet, um auf die RTC zuzugreifen. Weiterhin werden nur noch die Funktionen Byte lesen bzw. schreiben benötigt.
Da ist nur das Kalibrationsbyte drin. Da ich 5 Stück ATTINY12 habe, binde ich dementsprechend die Calc12_1.inc bis Calc12_5.inc in meine Programme mit ein. Bequemer ist natürlich einen Programmer zu verwenden, der das automatisch kann.
Diese Routine enthält alle für die RTC notwendigen Funktionen.
Im allgemeinen wird immer nur der 32-Bit Zeitwert ausgelesen. Außerdem
wird noch ein 16-Bit Magic kontrolliert, um sicher zu sein, daß kein
Fehler aufgetreten ist.
Da bei einem fabrikneuen DS1994 dieser Magic Wert nicht stimmt, wird nach
32 Fehlversuchen die RTC mit der definierten Startzeit geladen und gestartet.
Das Laden der Startzeit und das Ausführen des Copy Scratchpad Kommandos
erfolgt mittels einer Look-Up Tabelle, d.h es werden die zu ladendenden Bytes
als ".DB" Anweisung definiert und dann mittels LPM aus dieser Tabelle der
Reihe nach ausgelesen und zur RTC gesendet. Da es sich um längere Sequenzen
verschiedener Bytes handelt, kann man auf diese Weise eine Menge Kode einsparen,
bzw. es ist äußerst einfach diese ".DB" Anweisungen mit anderen
Daten zu füttern.
Außerdem wird nach jedem Zugriff auf die RTC das Kommando Write-Scratchpad
gesendet, damit nachfolgend Zugriffe auf das LCD nicht gestört werden.
Das LCD benutzt ja eine Leitung gemeinsam mit der RTC.
Diese Routine enthält eine Uhr und einen Kalender in Software. Da es
immer sehr aufwendig ist, eine 32Bit Zahl in Datum und Uhrzeit umzurechnen,
wird einfach sobald sich die Sekunde geändert hat, eine Sekunde zur
aktuellen Zeit addiert. Jeden Sonntag erfolgt zusätzlich auch der Test
auf die Sommer-/Winterzeitumstellung.
Des weiteren wird diese Routine genutzt, um nach jedem Reset die aktuelle
Zeit beginnend vom 1.1.2000 um 00:00Uhr zu ermitteln. Dazu wird die Uhr in
16h-Schritten weitergezählt, dann in 1min-Schritten und die verbleibenden
Sekunden übernommen.
Ich habs mal für 2099 getestet, da dauert es etwa 2s. So schnell ist
kaum ein MC-gesteuertes Gerät oben, meine Waschmaschine oder HiFi-Anlage
brauchen jedenfalls wesentlich länger.
Diese Routine wird jede Sekunde einmal aufgerufen und liest die Temperatursensoren aus. Da die ROM-Search Methode benutzt wird können theoretisch 248=2E14 Sensoren ausgelesen werden. Im Beispielkode werden aber nur die letzten beiden Sensorenwerte abgespeichert. Danach wird für alle Sensoren gleichzeitig die nächste Temperaturwandlung gestartet.
Da digitale ICs nur 2 Zustände kennen, wird auch in CPUs üblicher
Weise im Dualsystem gerechnet. Der Mensch ist aber das Dezimalsystem
gewöhnt. Daher ist eine Konvertierung nötig. Im Beispielprogramm
werden nur Byte-Variablen verwendet, daher ist die Umwandlung von 8 Dualstellen
(= 1Byte) in bis zu 3 Dezimalstellen (0..255) implementiert. Die DS1820
können ja nur bis 125°C umwandeln. Der besseren Ablesbarkeit halber
werden führende Nullen ausgeblendet.
Um bei der Zeit- und Datumsanzeige aber immer ein festes Format zu haben,
werden dort die Zahlen immer 2-stellig dargestellt. Zur Feststellung einer
führenden Null wird da T-Flag verwendet. Um also bei Zeitwerten <10
trotzdem die führende Null zu sehen, wird beim Ansprung von "dec_out00"
das T-Flag gesetzt, bei "dec_out" jedoch gelöscht.
Diese Routine enthält die Initialisierung des LCD, sowie die Ausgaberoutine
für Daten und Kommandos seriell über das Schieberegister 74HC164.
Dadurch sind nur 3 Leitungen für die LCD-Ansteuerung notwendig.
Die Taktleitung des Schieberegisters wird außerdem noch gemeinsam mit
der RTC verwendet. Da diese Leitung als Open-Kollektor arbeitet, wird, wenn
immer der Takt =1 ist, das DDR-Bit=0 gesetzt und dadurch über den externen
Pull-Up (2,2kOhm) high Pegel angelegt. Für Takt =0 wird demzufolge das
DDR-Bit=1 und damit der Ausgang aktiv, der ja ständig auf Low ist.
Das ist die übliche Methode, wie man bei einem AVR einen Open-Kollektor
Ausgang implementiert.
Das sind die einzelnen Ausgaberoutinen für Zeit, Wochentag, Datum bzw. Temperatur. Für die Anzeige des Wochentags findet wieder eine Look-Up Tabelle Anwendung.
Nachfolgend nun das komplette Listing und das erzeugte Hex-File: