Bauen und Programmieren von AVR-Mikrocontrollern am Beispiel von ATMEL ATTiny 2313 und ATMega 8
last updated: 03.2009 / 08.2010
Wenn die Register nicht mehr ausreichen - SRAM benutzen
Die 32 allgemeinen Register im Mega 8 scheinen ausreichend für alle Fälle. Irgendwann kommt allerdings der Moment, in dem man die Register zu umfangreichen Berechnungen benötigt und die Datenspeicherung auslagern muss - in den SRAM -Bereich. Am Ende dieses 1KByte großen Bereichs im Mega 8 liegt, wie wir bereits wissen, der Stackbereich. Der Stack wird dynamisch vom AVR selbst verwaltet nachdem er durch Belegen des Stackpointers mit dem Wert RAMEND initialisiert wurde. Auch das haben wir bereits etliche Male gemacht.
Es erscheint sinnvoll, für die Datenspeicherung den SRAM-Bereich vom Anfang her, also ab Adresse 0x60, zu benutzen. Beide Bereiche wachsen dann aufeinander zu. Natürlich dürfen sich die Obergrenze des Datenbereichs und die Untergrenze des Stacks niemals überlappen, Datenchaos und wohl auch Systemabstürze wären die Folge. Bei 1024 Byte SRAM im Mega 8 ist diese Gefahr trotz intensiver Nutzung von Prozeduren und dem Sichern von Registern eher gering.
Natürlich kann man SRAM-Speicherstellen ganz einfach mit Zahlen adressieren. Besser, weil aussagekräftiger, ist es aber auch hier, die Speicherzellen mit Namen vor Gebrauch festzulegen. Das funktioniert ähnlich wie im Flashspeicher mit den Programmlabels. Dem Assembler teilt man das Vorhaben, Speicherstellen im SRAM-Bereich definieren zu wollen, durch die Direktive ".dseg" mit, ".dseg" steht für Datensegment. Nach der Angabe des Variablennamens mit abschließendem Doppelpunkt folgt die Reservierung des gewünschten Speichers durch die Direktive ".byte".
Für das Ansprechen des SRAM gibt es spezielle Befehle. Die Tabelle gibt einen ersten Überblick. Die beiden ersten benutzen wir in diesem Projekt direkt, der ST-Befehl mit Z-Pointer wird im Arithmetikmodul eingesetzt.
LDS R, <adresse> | Lies den Speicher an der Stelle <adress> aus und fülle damit Register R auf. |
STS <adresse>, R | Schreibe den Inhalt von Register R in die Speicherstelle <adresse> |
LD, LDD |
Lade indirekt mit X,Y oder Z-Pointer - Lade indirekt mit displacement |
ST, STD | Speichere indirekt mit X,Y oder Z-Pointer - Speichere indirekt mit displacement |
Der SRAM hat zwei Nachteile. Eingeschriebene Daten halten nur so lange, wie eine Betriebsspannung anliegt. Das kennen wir schon von den Registern. Anders als die Register verhält sich der SRAM in Hinsicht auf logische und arithmetische Operationen. Diese sind nur auf Register beschränkt, mit SRAM-Zellen kann man also nicht rechnen, sie dienen nur dem Vorhalten von Daten. Für Operationen müssen die Inhalte vom SRAM in ein Register transferiert werden und nach Abschluss der Operationen wieder in das SRAM zurückgeschrieben werden.
Der EEPROM-Speicherbereich (Electrical Eraseable Programable Read Only Memory) verhält sich was Operationen angeht ähnlich wie das SRAM. Allerdings werden die Daten im EEPROM auch noch nach dem Abschalten der Betriebsspannung gehalten. Das heißt, man kann im EEPROM z.B. Parametereinstellungen speichern und beim nächsten Einschalten als Vorgabewerte wieder abrufen. Genau das machen wir im Projekt "Datenlogger". Darüber hinaus werden wir im EEPROM Temperaturmessdaten speichern, die über einen Zeitraum von bis zu 2 Wochen auflaufen und die jederzeit über die RS232 zu einem PC übertragen werden können, wo sie etwa in einem Kalkulationsprogramm ausgewertet werden. Die Daten werden dabei "roh" übermittelt. Die Feinarbeit unter Berücksichtigung der Eichkurve des Sensors muss der PC erledigen.
Ähnlich wie im SRAM-Bereich erfolgt die Festlegung der Variablen auch im EEPROM-Bereich. Durch die Direktive ".eseg" sagen wir dem Assembler, dass die Variablen im EEPROM-Bereich liegen sollen.
Das Ansprechen des EEPROMs ist im Gegensatz zum SRAM- oder FLASH-Bereich ungleich komplexer, denn es gibt keine darauf abgestimmten (= dedicierten) Befehle. Der Schreib- und Lesezugriff erfolgt über die SFR-Register EEARH/EEARL (Adressregister), EEDR (Datenregister) und EECR (Kontrollregister). Ferner muss ein bestimmtes Vorgehen eingehalten werden, das im Datenblatt zum Mega8 ab Seite 19 beschrieben ist. Die Bibliotheksproceduren "eep_read" und "eep_write" halten sich genau daran. Deren praktischer Einsatz wird später beschrieben.
Der Analog-Digital-Converter - Aufbau
Die Experimentierplatine des Mega8 wird an den Portleitungen PORTB.0 bis PORTB.3 mit den Datenleitungen D4 dis D7 des LC-Displays verbunden. Der RS-Anschluss des LCD kommt an PORTD.6, die Enable-Leitung E an PORTD.7. Eine LED an PORTD.5 soll blinken, während Messungen laufen. Am Analogeingang PORTC.0 wird der Abgriff des Temperatursensors aus dem Kapitel 15 - Spannungen vergleichen angeschlossen.
Nach dem bischen Hardware nun zur umfangreicheren Software
Zum Programmstart wird die Variable mega8 definiert. Sie ist wichtig für die Realisierung der RS232-Schnittstelle. Beim Mega8 ist eine Registeradresse doppelt belegt, was den Zugriff etwas kompliziert gestaltet. Diesem Umstand trägt eine etwas andere Behanhdlungsroutine im Bibliotheksmodul "rs232inc.asm" Rechnung. Die Existenz der Variable mega8 bewirkt nun einfach durch bedingte Assemblierung, dass besagter Teil anders übersetzt wird wie beim Tiny2313. Die Einzelheiten entnehme man der Kommentierung des RS232-Moduls.
Die Flags fblinken und fgetvalue steuern über die ISR (Interrupt-Service-Routine) von Timer1 und die Jobschleife das Abrufen und Speichern der Messwerte. Die Messwerte werden "roh" gespeichert und zum PC übertragen. Dieser hat neben dem Empfang der Daten und deren Darstellung die Aufgabe die Zahlen gemäß der Eichkurve anzupassen.
Die bisherigen Register zur Zeitsteuerung wurden in das SRAM ausgelagert, weil sie anderweitig gebraucht werden. Das macht die Zeitsteuerung in der ISR "Timer1_oc1a: " etwas komplizierter und schwerfälliger.
Der Schwingquarz hat eine Frequenz von 16 MHz. In der Zeit, in welcher beim Tiny2313 ein Ein-Takt-Befehl ausgeführt wurde, werden jetzt zwei abgearbeitet. Für die Zeitsteuerung ist die Frequenz zu hoch und deshalb wird der Takt für den Timer 1 durch 8 vorgeteilt. Lassen wir den 16-Bittimer 1 mit der Timerfrequenz von 2 MHz bis 40000 zählen, dann kann er das pro Sekunde 50 mal. 1 Jiffie ist demnach gleich 1/50 Sekunde.
Neu ist beim Mega8, dass der Stackpointer 1KByte Speicher ansprechen muss. Deshalb ist die Aufteilung in ein Low- und ein High-Byte nötig, wie man im Listing sehen kann. Wie alle anderen Peripherieeinheiten muss auch der ADC, der Analog-Digital-Converter eingestellt - initialisiert - werden. Das macht die Procedur "adc_init". Dann wird auch gleich eine erste Messung angestoßen, deren Ergebnis in den Mülleimer kommt. Notwendig ist das deshalb, weil der ADC für die erste Messung diverse Takte mehr benötigt wie sonst. Das Programm kann das zum Startzeitpunkt am besten verkraften.
Wie beim Tiny2313 triggert das Lesen und Schreiben des Lowbytes der 16-Bitregister von Timer1 den jeweiligen Vorgang. Beim Schreiben muss also das Lowbyte als zweites, beim Auslesen eines 16-Bitregisters muss es als erstes angesprochen werden. Für Timer1 wird der Modus 4 (CTC mit OCRA als Vergleichsregister eingestellt, den Vorteiler für den Timertakt habe ich bereits angesprochen. Im Datenblatt findet man die Beschreibung der Register zu Timer 1 ab Seite 96.
Timer 1 löst beim Gleichstand mit dem Vergleichswert 40000-1 = 39999 einen IRQ aus, der in der ISR "Timer1_oc1a:" behandelt wird.
Die Adressverwaltung zum Schreiben und Lesen der Werteliste im EEPROM dient das Wortregister Y = YL:YH= R28:R29. Dass dieses Registerpaar als 16-Bit-Zeiger angesprochen werden kann, macht das Hoch- und Herunterzählen durch die Befehle ADIW und SBIW einfacher.
Die Steuerung des Programms geschieht durch die zwei Flags "fblinken" und "fgetvalue". Die zeitkritischen Prozesse (zeitgesteuert und von geringem zeitlichen Umfang) finden in der ISR von Timer 1 statt. Prozesse, die länger dauern, erledigt die Jobschleife, damit dadurch der Zeitablauf nicht gestört wird.
Durch Setzen von "fblinken" weiß die ISR zu Timer 1, dass es was zu tun gibt. Sie lässt die LED an PORTD.5 blinken und zählt das Zeitintervall bis zum nächsten Erfassen des Analogwerts an PORTC.0 herunter. Jetzt setzt die ISR das Flag "fgetvalue", falls noch weitere Werte zu holen sind, wird der Countdown-Zähler neu gesetzt. Nach dem letzten Messwert schaltet die ISR die Blink-LED aus. Das beendet auch die Messwerterfassung.
Die Jobschleife prüft einerseits auf das Erscheinen einer 1 in Flag "fgetvalue", in diesem Fall wird der Messwert ermittelt und abgespeichert. Andererseits muss die Jobschleife auf das Eintreffen von Tastatureingaben vom PC, die über die RS232 ankommen, reagieren. Mit dem Setzen des Bits RXC im SFR-Register UCSRA des USART-Blocks signalisiert der USART, dass ein Zeichen im UDR (Datenregister) bereit steht. Liegt dieses Bit vor, dann wird der RJMP-Befehl zum Beginn der Jobschleife (LOOP:) übersprungen, das Zeichen mit Echo eingelesen und decodiert.
Am Ende der Jobschleife wird ein LF-CR (Zeilenvorschub und Wagenrücklauf) an den PC gesendet, damit im Terminalprogramm die nächste Ausgabe in eine neue Zeile stattfindet.
Folgende wichtigen Befehle gibt es:
Befehl (Parameter) | Beschreibung |
s XX | Zweistellige Sekundenangabe von 00 bis 99; die zweite Ziffer löst die Übergabe des Werts an das Programm aus. (Kein "Enter" am Schluss!) |
m XX | Zweistellige Minutenangabe von 00 bis 99; die zweite Ziffer löst die Übergabe des Werts an das Programm aus. (Kein "Enter" am Schluss!) |
e | Errechnet das Zeitintervall für die ISR von Timer 1. Der Wert wird ins EEPROM und in das SRAM geschrieben. Von dort wird der Countdownzähler aufgefrischt. |
a |
Zeigt die Anzahl von Messwerten an, wie sie im EEPROM und im SRAM gespeichert sind. |
t | Leitet den Transfer der Daten zum PC ein. Zuerst wird das Zeitintervall in Sekunden gesendet, es folgt die Anzahl von Messwerten. Dann schließen sich die Messwerte an. Das Auswertungsprogramm am PC kann so komplette Messreihen rekonstruieren. |
g |
Go-Befehl startet die Messwerterfassung. |
b | Break-Befehl beendet eine Messreihe von Hand |
o XXX | Obergrenze (1 bis 240) für die Anzahl von Messwerten bis zu dreistellig mit Plausibilitätskontrolle. Die Eingabe muss mit "Enter" abgeschlossen werden. |
l | Löscht die Messwerte (schreibt die Anzahl 0 ins EEPROM) |
Der "e"-Befehl wird durch eine eigene Procedur umgesetzt.
Der "m"- und der "s"- Befehl demonstrieren das Einschreiben eines Byte-Werts in das EEPROM. Nach dem Decodieren des Befehls durch den Konstantenvergleich holt die Procedur "v24_in_number" (Bibliothek: "rs232inc.asm") eine zweistellige Zahl von der RS232. Dieser Wert kommt in Register temp1 zurück. Das ist gleichzeitig das Register zur Wertübergabe an die Routine "eep_write" (Bibliothek: eeprom_512_inc.asm). Meine Proceduren in den Bibliotheken sind auf einander abgestimmt und so geschrieben, dass Bytedaten stets in Temp1 (=R16) ankommen oder abgehen. Das erspart aufwendiges Umlagern. Die Adresse der EEPROM-Zelle wird in temp2 (Lowbyte) und temp3 (Highbyte) übergeben. Die Procedur "eep_write" schreibt den Wert ins EEPROM. Weil die Routine "v24_in_number" die Ziffern ohne Echo einliest, wird die Zahl zum Schluss noch als Echo zum PC gesendet (Procedur "v24_number" VORSICHT nicht verwechseln mit "v24_in_number").
Der "g"-Befehl setzt nur das "fblinken"-Flag, der "b"-Befehl löscht es.
"t"- "o"- und "l"-Befehl erledigen ihre Aufgaben in eigenen Proceduren. Diesen Weg wähle ich gerne, wenn die Befehlssequenzen recht lang werden. Die Jobschleife wird dann schnell unübersichtlich. Es kann auch vorkommen, dass die Sprungbereiche der Branch-Befehle überschritten werden. Die Jobproceduren stehen am Programmende und werden dort besprochen.
Den Beginn der Timer1-ISR kennen wir schon von früher. Jetzt wird die Zeit allerdings nicht in Registern sondern im SRAM verwaltet. Das erfordert zusätzliche LDS- und STS-Befehle. Ist "fblinken" gesetzt, dann wird "RJMP loeschen" übersprungen, und die Sequenz "led_blinken" wird durchlaufen. Diese typische "wenn-dann-sonst"-Konstruktion hatten wir schon einige Male.
Ist "fblinken" gesetzt, wird "rjmp getvalue_done" übersprungen. Der Countdownzähler wird verringert und auf 0 geprüft. Kein Handlungsbedarf besteht, wenn der Zähler noch nicht Null ist - "rjmp getvalue_done".
Ist der Zähler = 0, dann wird erst einmal eine Messung angefordert, das Flag "fgetvalue" wird gesetzt und die Anzahl Messwerte erhöht. Es folgt der Vergleich mit der Maximalzahl, die auch im SRAM steht. Deshalb muss sie erst in das Register temp2 eingelesen werden. "BRLO" (=Branch if LOwer) verzweigt zum erneuten Setzen des Countdownwerts aus dem SRAM, falls "anzahlwerte" kleiner als der Wert "maxzahl" ist. Ist "maxzahl" erreicht, wird "fblinken" zurückgesetzt, das war's.
Die Verwaltung der Messwertaufnahme geschieht also durch diese wenigen Befehle. Der schwergewichtige Overhead der tatsächlichen Ermittlung und Umrechnung des Ergebnisses geschieht in der Jobschleife.
Der "e"-Befehl liest die Sekunden- und Minutenwerte aus dem EEPROM und berechnet daraus das Zeitintervall in Sekunden (= m mal 60 + s) als 2-Byte-Wert. Die zugehörige Jobroutine heißt "timebase_set" und ist nachfolgend dargestellt. Der Wert wird im SRAM abgelegt und auch gleich in den Countdown-Zähler übertragen. "v24_binout" gibt den Wert via RS232 an den PC zurück, so dass er im Terminalfenster angezeigt werden kann. Die Verwendungweise der eingesetzten Bibliotheksroutinen kann aus den Headerdateien entnommen werden. Es lohnt sicher auch ein Blick in die Routinen selbst. Beim Studieren von (gut dokumentierten) Programmen lernt man eine Menge für eigene Projekte hinzu.
Die Routine "werte_sichern" ruft über "rcall adc_get" einen über acht Einzelmessungen gemittelten Wert der Spannung an PORTC.0 ab, dessen Lowbyte sofort an die aktuelle (=nächste freie) EEPROM-Adresse (in YL und YH geführt) ausgelagert wird. Der Adresspointer wird um 1 erhöht, dann erfolgt die Speicherung des Highbytes vom Messwert. Wieder wird der Adresszeiger erhöht, damit beim nächsten Proceduraufruf gleich wieder die nächste freie EEPROM-Adresse eingestellt ist. Der Wert für die Anzahl der Messwerte im EEPROM muss noch erhöht werden, dann wird "fgetvalue" rückgesetzt. Nummer und Wert der letzten Messung werden im LCD angezeigt.
Die Routine zum Datentransfer erklärt sich durch ihre umfassenden Kommentare selbst. In der neuen Bibliotheksroutine "v24_lfcr" wird eine Sequenz aus Zeilenvorschub und Wagenrücklauf gesendet.
Die Procedur, welche die Maximalanzahl von Messungen einliest, ist ein Beispiel für eine Terminalroutine, die mit Prompt eine Eingabe anfordert und die Eingabe auf Plausibilität überprüft. Die zugehörigen Meldungstexte stehen am Ende des Programms. Der "TST"-Befehl in der Sequenz testet den Inhalt von Register R3 auf 0. Ist R3 = 0, dann wird das Z-Flag im Statusregister SREG gesetzt und der nachfolgende BRNE-Befehl wird nicht nicht nach ms00a verzweigen. Sonst ist die Routine selbsterklärend.
Auch dazu muss man wohl kaum noch etwas sagen.
Hier noch die Meldungstexte zur Routine "maxzahl_setzen".
Als neue Peripherie betrachten wir jetzt noch den Analogwandlerteil. Die relevanten SFR-Register mit der Bedeutung der Bits werden im Headerbereich vorgestellt, der vom Procedurteil nicht getrennt ist, weil keine Declarationen vorkommen.
Zu den beiden Datenregistern ADCL und ADCH gibt es nicht viel zu sagen. Sie enthalten nach der Wandlung den Messwert als 10-Bit breiten Wert. Im Normalfall (rechtsjustiert) ist dieser Wert mit den unteren 8 Bit im ADCL-Register enthalten, die restlichen beiden hochwertigsten Bits des Wandlerergebnisses stehen als LSBs im ADCH-Register.
Weil die Taktfrequenz für den Wandler zwischen 50 und 200 kHz liegen soll, muss der Vorteiler auf 128 eingestellt werden. Damit wird die ADC-Frequenz von 16 MHz auf 125 kHz gesenkt.
Die Bibliothek enthält zwei Proceduren: adc_init und adc_get. "adc_init" erwartet keine Parameter. "adc_get" erwartet in temp1 (=R16) den Exponenten einer 2er-Potenz, deren Wert die Anzahl der Einzelmessungen ergibt. Aus den Einzelmessungen wird schließlich ein Mittelwert als wahrscheinlichster Messwert berechnet. Das ist nötig, weil die einzelnen Messungen um einige Digits von einander abweichen können. Da nur Zweierpotenzwerte als Anzahl der Einzelmessungen vorkommen können, ist die abschließende Division der Messwertsumme einfach durch Rechtsschieben in einer Schleife möglich.
Grundlage für die Analog-Digital-Wandlung ist ein Comparator, der die zu messende Spannung mit einer Referenzspannung vergleicht. Welche Referenzspannung ihren Weg zum Wandler machen darf, das legen die Bits REFS1:0 fest. Während REFS1 entscheidet, ob die Spannung an AVcc oder die interne Referenzspannung von 2,56 V ausgewählt wird, legt REFS0 fest, ob überhaupt eine von beiden an den AREF-Anschluss gelangt.
Die Referenzspannung (AVcc, AREF oder interne Referenz 2,56V) wird nun einem Digital-Analogwandler (DAC) zugeführt, der als weiteres Eingangssignal die digitale Information (10 Bit) der Converterlogik bekommt. DAC-Wandler setzen eine digitale Information unter Verwendung einer konstanten Vergleichsspannung in eine variable Spannung um. Die 10 Bit von der Converterlogik teilen die Referenzspannung im AVR quasi in 2 hoch 10 = 1024 Scheibchen. In der folgenden Tabelle wird die Wandlung durch fortschreitende Annäherung (sukzessive Aproximation), wie sie im AVR abläuft, anhand von Münzen erklärt.
Ich vergleiche die Referenzspannung mit Stapeln von Geldstücken. Beide Stapel sollen die gleiche Anzahl Geldstücke enthalten. Soll der Stapel niedriger ausfallen, müssen die Geldstücke dünner sein, denn die Anzahl muss ja in beiden Stapeln die gleiche sein, ich wähle für das Beispiel 16 Münzen. Ich wähle meinen Münzstapel stets so aus, dass seine Höhe die maximale Höhe eines zu messenden fremden Stapels nur wenig übersteigt. |
Nun habe ich mich für eine Referenzspannung entschieden, die Stapelhöhe der 16 Münzen liegt fest. Wenn ich mich für eine Sorte Geldstücke entschieden habe, kann ich die Stapelhöhe aber immer noch durch die Anzahl der Geldstücke verändern. Ich muss die Anzahl der Münzen in meinem Stapel mitzählen. |
Jetzt kommen wir zur eigentlichen Aufgabe: Bestimme
die Anzahl von Münzen, die den rechten Stapel ergeben.
Der einfache Vergleich der neben einander liegenden Stapel sagt uns, wann wir den Vorgang abbrechen können. Weil wir stets mitgezählt haben, kennen wir die nötige Anzahl Münzen. |
Die Höhe einer Münze entspricht beim AVR einem von den 1024 Spannungsscheibchen. Die Controllerlogik stellt über den DAC einen variablen Stapel von Spannungsscheibchen zur Verfügung. Der Comparator vergleicht diesen Stapel mit dem vom Messeingang und meldet der Controllerlogik, wann Gleichstand erreicht ist. Darauf bricht die Logikschaltung den Vorgang ab und schreibt den Zählerstand in die beiden Ergebnisregister ADCL und ADCH.
Nicht alltäglich ist in diesem Zusammenhang der Begriff "Multiplexer". Darunter versteht man eine Schaltung, die in der Lage ist, die Spannung, die an einem ihrer Eingänge anliegt, auf den einzigen Ausgang durchzuschalten. Als Vergleich möge folgende Zeichnung dienen, in der das Rohr mit der blauen Brühe durch Öffnen des Tores 2 = 0b10 den Anschluss an das Abflussrohr bekommt.
Der eindeutige Vorteil von Multiplexern ist die Einsparung aufwendiger und komplexer Schaltungsteile durch die Bereitstellung billiger "Tore", der ebenso eindeutige Nachteil ist, dass immer nur die Information von einem Eingang verarbeitet werden kann. Die Timer im AVR sind nicht gemultiplext, sie arbeiten alle in unabhängiger Weise.
Durch den Wert 0b01000000, welcher in ADMUX geschriebenwird, wird als Referenzspannung der Pegel an AVcc eingestellt, der in der Regel 5 V beträgt. Zur Beseitigung von Störsignalen muss dieser Pin mit einem 0µ1 Kondensator gegen Masse geblockt werden. Durch die 4 Nullen im unteren Nibble von ADMUX wird der Analogeingang 0 an Pin 23 des Mega8 auf den Comparator durchgeschaltet.
Im ADCSRA-Register (AD-converter Control und Status Register A) wird der ADC eingeschaltet und der Vorteiler für den Takt auf 128 festgelegt. Das Ausschalten des nicht verwendeten Analogcomparators spart etwas Energie. Übrigens, der Analogcomparator, den wir in Kapitel 15 verwendet hatten und der Comparator der Wandlereinheit sind komplett unterschiedliche Baugruppen im AVR, die miteinander nichts zu tun haben.
Nun zur wichtigsten Routine des Projekts Datenlogger. Anfang und Ende der Procedur sind durch das obligatorische Sichern und Restaurieren der veränderten Register sehr aufgebläht. Aus dem Arithmetikmodul bediene ich mich der Registergruppe "akkuB" (R5:3), um darin die Einzelergebnisse aufzusummieren. Deshalb wird dieser Bereich anfangs gelöscht.
Die Ergebnisse der Einzelmessungen weichen entwas von einander ab, was auf Leitungsrauschen und andere Nebeneffekte im AVR zurückzuführen ist. Um einen verlässlichen Messwert zu erhalten werden deshalb mehrere Einzelmessungen angestoßen und deren Ergebnisse in akkuB aufaddiert. Wie viele Messungen gemacht werden, steht im Register temp1 (=r16) beim Aufruf der Procedur "adc_get". Als Anzahl sind durch das vorliegende Verfahren nur Zweierpotenzwerte möglich. Wie dadurch die Ermittlung des Gesamtergebnisses vereinfacht und beschleunigt wird, sehen wir weiter unten.
Die kleinste Zweierpotenz in 2 hoch 0 mit dem Wert 1. Dieser wird auch als nächste Anweisung als Konstante in R20 geladen. Der Zweierexponent wird von R16 nach R18 als Zähler für die Potenzierung und als Zähler für die abschließende Division durch die Anzahl von Messungen kopiert. Wir sind fertig, wenn R18 = R16 = 0 ist. Der AND-Befehl des Registers R18 mit sich selbst setzt in diesem Fall das Z-Flag im Statusregister SREG und der folgende BREQ (BRanch on EQual, verzweige, falls gleich) überspringt die Potenzierung.
Innerhalb der Schleife "adc_get0" wird die 1 in R20 so oft nach links geschoben, wie in R16 angegeben war. Jedes Linksschieben entspricht einer Multiplikation des vorherigen Werts mit 2. Deimaliges Linksschieben macht aus der ursprünglichen 1 eine 2, dann eine 4 und schließlich eine 8; 2 hoch 3 = 8.
Und schon startet durch Setzen des Bits ADSC im Register ADCSRA die erste Wandlung. Deren Ende teilt der AD-Wandler mit, indem das eben von uns gesetzte Bit ADSC auf den Wert 0 setzt. Wir warten so lange - "wait_adc". In Anwendungen, in denen andere wichtige oder zeitkritische Dinge anstehen, kann man natürlich nicht einfach warten und nix tun, dann müssten wir uns durch die Interruptsteuerung des ADC im laufenden Programm das Ende der Wandlung mitteilen lassen. Zu diesem Zweck müsste das Flag ADIE in der Vorbereitung gesetzt und eine entsprechende ISR zur Verfügung gestellt werden. In diesem Projekt habe ich das bereits mit der ISR von Timer1 und der Jobschleife so gemacht.
Wir holen den Wert der letzten Messung in die Register R16 (Lowbyte) und R17 (Highbyte) ab. Wichtig ist hier, wie beim Timer1, dass beim Lesen stets das Lowbyte zuerst gelesen wird. Das triggert das Einfrieren des Highbytes und sorgt so für ein "unfallfreies" Auslesen der beiden Bytes. Register R18 ist seit dem Potenzieren am Anfang sicher = 0. So steht der 3 Byte breiten Addition - das erste Mal ohne Carry dann zwei mal mit Übertragsbit - nix mehr im Weg. Das mit 0 besetzte Register R18 ist nötig, weil die Addition der Konstante 0 mangels zuständigem Befehl nicht möglich ist.
Der Durchlaufzähler wird verringert und die Schleife abgebrochen, falls R20 den Wert 0 erreicht hat. Sonst springt das Programm an den Beginn der Schleife "sample_adc" zurück.
Nun wird für das Teilen durch 8 der anfangs in R19 gemerkte Wert 3 benötigt. Wir stellen zuerst fest, ob überhaupt geteilt werden muss. Wenn R19 = 0 ist, dann ist der Teiler gleich 1 und die Division nicht nötig. Andernfalls wird der Inhalt aller drei Bytes in der Variable akkuB drei mal kollektiv nach rechts geschoben. Die Schleife ist so oft zu durchlaufen bis R19 den Wert 0 erreicht.
Das Rotieren erfolgt nicht ins Nirwana, sondern über R18. Alles, was vom LSB von akkuB rechts hinausgeschoben wird, wird von links in R18 als Divisionsrest hineingestopft. Ist nach Abschluss der Division das MSB in R18 gesetzt, dann beträgt der Rest mehr als die Hälfte des Divisors und das Ergebnis in akkuB ist aufzurunden. Das geschieht, indem zum Inhalt von akkuB 1 addiert wird.
Zum Abrufen eines ADC-Werts haben - wer hat mitgezählt - 5 Befehle genügt, derRest war zum Ausschalten von Zufällen und Fehlern nötig. Wem das nicht zusagt, der kann, wenn er mit 8 Bit statt 10 Bit im Ergebnis zufrieden ist, das Wandlerergbnis links justiert einstellen und nur das Highbyte abrufen. Die beiden untersten (unsicheren = wackelnden) Bits fallen dann in den Mülleimer.
Die Quelldateien sind in diesem Fall etwas umfangreicher, weshalb ich sie in ein Archiv gepackt habe. Zur eigenen Verwendung das Archiv in ein beliebiges Verzeichnis downloaden, entpacken und im AVR-Studio als Quelle angeben. Als Hauptdatei verwendet man die Datei "adc.asm". Die Hex-Datei einzeln gibt es hier.
Appendix (05.03.2009)
In den letzten Wochen ist der kleine Einkanal-Temperaturlogger zum ausgewachsenen 4-Kanal Datenlogger geworden, den man in der TQFP-Version des Mega 8 auch ohne Probleme mit der Software zum 8-Kanal-Gerät ausbauen kann. Allerdings ist dann im EEPROM nur noch für 60 Messpunkte Platz. Mit der Hardwareweiterentwicklung einher ging natürlich die Änderung der Software. Bei dieser Gelegenheit wurden einige Bugs der Einkanalversion beseitigt, die Steuerung und Werteabfrage an die Bedienung von MS-Excel aus via RS232 angepasst und neben einer Dreitasten-Menüsteuerung über das LCD der Funktionsumfang erweitert. Die ganze Einheit befindet sich jetzt als SMD-Hauptplatine nebst Sensorplatine, Spannungskonstanter und RS232-Modul in einem Kunststoffgehäuse mit 9V-Block und zusätzlicher externer Spannungsversorgung.
Layout spiegelverkehrt in 400%. Verkleinern, ausdrucken und auf die Platine
bügeln
Die letzte Version der Firmware gibt es hier. Das Hexfile alleine in neuer Fassung kann man hier runterladen. Und wenn es gleich ein Import nach MS-EXCEL sein soll, dann sind diese XLS-Dateien sicher von Interesse, die zeigen, wie man den AVR über VBA steuert und abfragt. Für den 2-Kanal-Logger, wie er in der Abbildung gezeigt ist, geschieht das noch über das Öffnen eines Files über die serielle Schnittstelle. Für die Datenausgabe, also für das Absetzen des Steuercodes an den Mikro, muss ein File zum Schreiben geöffnet und nach der Datenübermittlung wieder geschlossen werden. Das sieht so aus:
Open ("com3:9600, N, 8 , 1") for Output as #1
print #1, "t";
close #1
Mit #1 wird der Ausgabekanal numeriert, die Zahl dient nach dem Öffnen der Referenzierung des Ausgabekanals. Die Parameter des Openbefehls sprechen für sich:
com3 ist die Nummer der Hardwareschnittstelle.
9600 legt die Baudrate fest
N steht für No Parity also keine Paritätsprüfung
(= Fehlerprüfung des ubermittelten Bytes)
8 Datenbits und 1 Stopbit werden gesendet
Damit in einem anschließenden Lesevorgang von der Schnittstelle Daten empfangen werden können, muss zunächst der Ausgabekanal geschlossen werden, das macht der Close #1 - Befehl.
Nach dem erneuten Öffnen der Schnittstelle zum Lesen (For Input) kann eine Variable wert, die natürlich vorab zu declarieren ist, mit den empfangenen Daten belegt werden, das macht der Input #1 - Befehl im folgenden Listing.
Open ("com3:9600, N, 8 , 1") for Input as #1
Input #1, wert
close #1
Ein Problem stellt die Tatsache dar, dass VBA eine interpretierte Programmiersprache und daher naturgemäß langsam ist. Diesem Umstand muss bei der Programmierung des Mikrocontrollers durch den Einbau zusätzlicher Warteschleifen Rechnung getragen werden. Der Controller muss sich also gedulden, bis der PC nach dem Senden des Befehls empfangsbereit ist. Zu verbessern wäre diese Situation, indem man zwei weitere Portbits als Steuerleitungen für die serielle Schnittstelle sponsort und das Hardwarehandshaking RTS/CTS am PC einschaltet. Neben den beiden Leitungen muss der Mikro natürlich dann auch zusätzliche Software bekommen, welche das Abfragen und Setzen der Steuerleitungen übernimmt.
Einen anderen Ansatz verfolgt der VBA-Code für den großen Bruder des oben beschriebenen Loggers. Der 8-Kanallogger hört auf die gleichen Befehle vom PC und sendet selbst genau so wie der 2-Kanal-Logger. Allerdings erfolgt das Senden und Empfangen vom PC aus nicht mehr über ein File, sondern über ein MSCOMM-Objekt. Dieses Objekt wird durch VB zur Verfügung gestellt und durch seine vielfältigen Eigenschaften (Properties) und Befehle (Methoden) gesteuert und abgefragt. Dadurch kann man simultan senden und empfangen. Die entsprechende EXCEL-Datei kann man hier herunterladen. Allerdings hatte ich in diesem Zusammenhang erst kürzlich die Begenung der 5. Art mit der Willkür von Onkel Bill. Der hatte nämlich durch eines der zahllosen Updates die Funktion meines MSCOMM-Objekts durch Setzen des Killbits einfach abgeschaltet, aus Sicherheitsgründen, wie mir die Fehlermeldung mitteilte. Nachdem ich mir einen Wolf gesucht hatte, (hab ich was falsch gemacht, das ging doch schon alles...) fand ich in etlichen Foren genau die Beschreibung meines Fehlers und gleichzeitig die Gewissheit, dass nicht ich den Fehler verursacht hatte sondern ein gewisser Herr Gates und seine Crew. Zwei Möglichkeiten der Abhilfe boten sich an.
Auf der Site von Nir Sofer gibt es ein kleines Freewaretool (ActiveX Compatibility Manager v1.00), mit dem man das von MS gesetzte Killbit wieder rücksetzen und somit das MSCOMM-Objekt wieder aktivieren kann. Man sucht nach dem MSCOMM32.OCX-File in der Liste und klickt dessen Statusfeld rechts und schaltet im Kontextmenü das ActiveX-Element wieder aktiv, der rote Fleck in der CLSID-Spalte schaltet auf grün, das war's. Allerdings kann es sein, dass beim nächsten Update der gleiche Schlamassel wieder passiert. (Nebenbei bemerkt, findet man auf dieser Site eine Menge sehr nützlicher Tools, schaut selber!)
Die andere Lösung ist dauerhaft, weil nicht durch Onkel Bill vertrieben, kostet ein paar Euro (10 US$ = 7,50 Euronen) und erfordert eine ganz geringfügige Anpassung in der Programmierung auf der VBA-Seite. Hier wird anstelle des MSCOMM-Objekts das von Richard Grier verfasste XMCOMMCRC-Objekt verwendet, das man sich von dessen Site herunterladen kann. Die Freewareversion meldet sich beim Initialisieren mit einer Messagebox, die man durch Lizensieren des Produkts beseitigen kann.
Sowohl mit dem MSCOMM32 als auch mit Dick's XMCOMMCRC muss nun keine Pause mehr zwischen dem Empfangen, Auswerten und darauf folgendem Senden einer Antwort seitens des Mikrocontrollers mehr gewartet werden. Das folgende Bildschirmfoto meines DSO (Digitales Speicheroszilloskop) macht den Zeitablauf deutlich.
In einem Testprogramm im ATMega8 reagiert der Mikro auf das vom PC gesendete "u" durch zurücksenden der Zeichenfolge "U3"+EOF. EOF steht für "End of File" und wird im ACSII-Code durch das Steuerzeichen mit der Nummer 26 = 0x1A vertreten. Das DSO schreibt von links nach rechts. Das Startbit wird von den Schnittstellen als erstes ausgegeben und steht im Kurvenverlauf daher ganz links. Nach dem V24-Standard wird das LSB (= least significant bit) des Datenbytes als erstes herausgeschoben, es folgt daher direkt auf das Startbit. Das HSB (highest significant bit) steht am Schluss der Sequenz kurz vor dem Stopbit, das stets den Pegel 1 aufweist. Im Oszillogramm kann man sehen, dass zwischen empfangenem Stopbit und gesendetem Startbit quasi keine Pause vorhanden ist.
Für alle, die das Protokoll eines Frames nicht mehr im Kopf haben, dient die folgende Grafik.
Hier folgen noch die Listings der Programme für Mikro und PC (EXCEL mit VBA und MSCOMM32.OCX.
Testprogramm im Mega8:
Das Gegenstück im PC:
Zum VBA-Programm noch ein paar Anmerkungen:
In der Senderoutine wird das MSCOMM-Objekt vorbereitet. Nachdem das Ausgabefeld in der EXCEL Tabelle1 gelöscht ist, wird, falls offen, der COMPort geschlossen. Mach man das nicht und war das Programm vorher nicht beendet worden, führt das nochmalige Öffnen zur Fehlermeldung 8005 (Port already open). Nach der Übergabe der Parameter für die Schnittstelle wird diese geöffnet und danach der Status festgestellt. Zum Abschluss wird das auslösende "u" gesendet und die Variable Wert geleert.
Der Rest geht im Gegensatz zum Steuerprogramm für den Datenlogger ganz vollautomatisch über das Ereignis OnComm des MSCOMM32-Objekts. Falls das Ereignis die Kennung ComEvReceive (= 2) hat, wird der Empfangspuffer der COM3 so lange ausgelesen, bis das letzte Zeichen des in Wert aufgebauten Empfangsstrings das EOF-Zeichen (= 0x1A) ist. Der Schleifendurchlauf ist damit beendet, der empfangene String wird ohne das EOF ausgegeben und der Port geschlossen. Die Statusanzeige schließt die Procedur ab.
Das ActiveX-Element von Dick Grier arbeitet ähnlich, unterscheidet sich aber im Eingabebefehl. Anstelle von
H = MSCOMM1.Input
muss es dann lauten
H = XMCOMMCRC1.InputData
(Außerdem habe ich den Verdacht, dass Dick's Version trotzdem das MS-Teil voraussetzt. Nachdem ich letzteres nämlich versteckt (=umbenannt hatte) hat's nicht mehr funktioniert (Folge: Fehlermeldung!). Das muss aber noch näher auf einem jungfräulichen PC (d.h. ohne installiertes VB) ausprobiert werden.