Pfad:AVR-DE =>
Anwendungen =>
Signalgenerator M16 => Software
 |
AVR-Anwendungen
Sinus/Dreieck/Rechteck/Sägezahn-Signalgenerator mit ATmega16
Software für den Signalgenerator |
 |
Signalgenerator ATmega16 Software-Beschreibung
Auf dieser Seite wird erläutert, wie die Software aufgebaut ist und wie sie
funktioniert.
Im Überblick funktioniert die Software so:
- Die Stellung des Potentiometers zur Frequenzeinstellung wird durch den Timer TC0
angestoßen und alle 15,6 ms gemessen. 16 dieser Messwerte werden
aufsummiert (ca. 4 mal pro Sekunde) und dann zusammen mit den Stellungen der
Schalter ausgewertet.
- Die Auswertung dieser Eingangsgrößen erfolgt folgendermaßen.
Aus dem Potistand wird durch Multiplikation die einzustellende Frequenz
berechnet sowie daraus durch Division der frequenzbestimmende CTC-Wert für
den Ausgabetimer TC1. Abhängig von der Wellenform und der Frequenz wird
ein Wert für die Kondensatorzuschaltung im RC-Filternetzwerk zur
Ausgabe vorbereitet. Nach Abschluss der Berechnungen wird die bUeber-Flagge
gesetzt, um die neuen Einstellungen beim nächsten Nulldurchgang der
Schwingung in den Timer zu schreiben. Aus dem tatsächlich im Timer TC1
stehenden Compare-Wert wird durch Division die anzuzeigende Frequenz errechnet
und, zusammen mit einem Zeichen für die Kurvenform, sechs- bzw. siebenstellig
auf der LCD ausgegeben.
- Der 16-Bit-Timer TC1 erzeugt mit einem Vorteilerwert von 1 über ein
R/2R-Netzwerk aus in Tabellen gespeicherten Digitalwerten die auszugebenden
Analog- und Digitalsignale. Er wird dazu im CTC-Modus betrieben, der
Vergleicherwert Compare A bestimmt über die Frequenz. Mit jedem
Erreichen des Vergleicherwerts wird ein Interrupt ausgelöt;st und der
nächste von 256 Tabellenwerten an das R/2R-Netzwerk ausgegeben.
Nachfolgend werden alle Software-Bestandteile im Detail beschrieben.
- Erzeugung der Kurvenformen mit dem TC1-CTC-Interrupt
- Frequenzeinstellung
- Die CTC-Berechnung
- Die RC-Filter-Einstellungen
- Die Transfer-Zusammenstellung zum Umschalten
- Die LCD-Anzeige
- Die Kurvenformerzeugung
- Simulation des Programms, Debug
- Listing der Quellcodes
1.1 Anforderung bei der Erzeugung der Kurvenform
Das Besondere an der Software, und die zentrale Anforderung dabei ist, dass über
den gesamten Frequenzbereich von 2 Hz bis 20 kHz hinweg der DA-Wandler mit
dem R/2R-Netzwerk regelmäßig und zeitlich exakt mit den Tabellenwerten der
betreffenden Kurvenform versorgt wird. Das wird erreicht, indem der 16-Bit-Counter/Timer
TC1 im CTC-Modus (Clear Timer on Compare) betrieben wird. Der Compare-Wert bestimmt
dabei über den Zeitraum, der bis zur nächsten Byte-Ausgabe an den R/2R-Port
vergeht, und damit über die erzeugte Frequenz.
1.2 Verfahren der Erzeugung der Kurvenform
Dazu wurde folgendes Verfahren ausgewählt.
- Eine Tabelle, die im Flash-Speicher des ATmega16 abgelegt ist, enthält
für jede der vier Kurvenformen jeweils 256 Einzelwerte, die die
Kurvenform und deren Amplitude (Aussteuerung) angeben. Die 256 Einzelwerte
bilden den Kurvenzug in einem Durchgang ab.
- Wechselt die Kurvenform, dann wird die jeweilige neue Flash-Tabelle in einen
256 Byte umfassenden Speicher im SRAM kopiert und von dort aus schrittweise
mit dem CTC-Interrupt an den Digital-Analog-Wandler ausgegeben.
- Liegt die gewählte Frequenzeinstellung im Bereich 2 kHz - 20 kHz
und die mit dem Potentiometer eingestellte Frequenz bei 2.500 Hz und
darüber, dann wird die Frequenz dadurch verdoppelt, indem in den 256 Bytes
der auszugebenden SRAM-Tabelle zwei Kurvenzüge kopiert werden, d. h.
es wird nur jeder zweite Tabelleneintrag der Flash-Tabelle in die SRAM-Tabelle
übernommen und bei den restlichen 128 Werten mit der Flash-Tabelle neu
begonnen.
- Entsprechend wird ab 5.000 Hz eine vervierfachte Kopie der Flash-Tabelle
abgelegt und nur jeder vierte Wert aus der Flash-Tabelle verwendet. Analog erfolgt
ab 10.000 Hz eine Verachtfachung des Wellenzugs.
1.3 CTC-Compare-A-Werte
Bei den beiden Extremen, 2 Hz und 20 kHz, ergeben sich für den CTC-Wert
von TC1 folgende Beziehungen. In beiden Fällen wird mit einem TC1-Vorteiler von 1
gearbeitet, so dass TC1 mit 16 MHz getaktet wird. Bei 2 Hz wird in den 256
Einzelstufen ein Kurvenzug ausgegeben, bei 20 kHz in den 256 Stufen aber acht
Kurvenzüge. Die CTC-Vergleicherwerte für TC1 ergeben sich dann zu
Frequenz | CTC-Compare-A-Berechnung | CTC-Wert | Anmerkung |
1 Hz | 16.000.000 Hz / 256 - 1 | 62.499 | beim Einschalten |
2 Hz | 16.000.000 Hz / 256 / 2 Hz - 1 | 31.249 | niedrigste Frequenz |
20 Hz | 16.000.000 Hz / 256 / 20 Hz - 1 | 3.124 | Beginn zweiter Bereich |
200 Hz | 16.000.000 Hz / 256 / 200 Hz - 1 | 311 | Beginn dritter Bereich |
2 kHz | 16.000.000 Hz / 256 / 2000 Hz - 1 | 30 | Beginn vierter Bereich |
2,5 kHz | 16.000.000 Hz / 256 * 2 / 2500 Hz - 1 | 49 | Beginn Verdoppelung |
5 kHz | 16.000.000 Hz / 256 * 4 / 5000 Hz - 1 | 49 | Beginn Vervierfachung |
10 kHz | 16.000.000 Hz / 256 * 8 / 10000 Hz - 1 | 49 | Beginn Verachtfachung |
20 kHz | 16.000.000 Hz / 256 * 8 / 20000 Hz - 1 | 24 | Ende Einstellbereich |
1.4 Anforderung bei der Erzeugung der Kurvenform mit dem CTC-Interrupt
Bei 16 MHz Takt und der maximal zu erzeugenden Frequenz von 20 kHz tritt daher
alle (24 + 1) Takte ein Interrupt auf. Maximal darf daher die CTC-Interrupt-Routine bis
zu 24 Takte benötigen, von der Auslösung der Interrupt-Anforderung durch
den Timer bis zu und einschließlich der Rückkehr vom Interrupt. Würde
sie länger benötigen, dann würde der CTC-Interrupt die Ausführung
jeden anderen Codes blockieren und käme außer Tritt (unbehandelte
Mehrfachanforderungen). Diese zentrale Anforderung ist auf jeden Fall von der Software
einzuhalten, wenn sie über den gesamten Frequenzbereich bis 20 kHz korrekt
funktionieren soll.
Wechsel der Kurvenformen treten daher immer dann auf, wenn
- sich Schaltereinstellungen zur Kurvenform ändern, oder wenn
- die eingestellte Frequenz von <2.500 auf 2.500 Hz und mehr
wechselt, oder wenn
- die eingestellte Frequenz von <10 kHz auf 10 kHz wechselt, oder wenn
- die eingestellte Frequenz von >=2,5 kHz auf <2,5 kHz, oder wenn
- die eingestellte Frequenz von >=10 kHz auf <10 kHz wechselt.
In allen diesen Fällen muss die Ausgabetabelle im SRAM neu geschrieben werden.
Damit es in allen diesen Fällen des Neuschreibens nicht zu abrupten
Pegeländerungen kommt (z. B. beim Umschalten von Sinus auf Rechteck oder bei
den genannten Änderungen der Frequenzeinstellung),
- darf das Schreiben der neuen Tabelle erst dann erfolgen, wenn der komplette
Kurvenzug ausgegeben ist (wenn das Ende des SRAM-Bereiches der 256 Werte
erreicht ist, was bei 2 Hz schon mal eine halbe Sekunde dauern kann), und
- alle Kurvenzüge mit dem gleichen Wert beginnen (bei Null).
Das erstgenannte Kriterium wird dadurch erfüllt indem in der CTC-Interrupt-Routine
eine Flagge bStart gesetzt wird. bStart signalisiert dabei den
Nulldurchgang des Signals. Falls die Auswertung der AD-Wandlerwerte vom Potentiometer
oder der Schaltereinstellungen ergab, dass Änderungen erfolgt sind, werden dann
- die neuen SRAM-Tabellenwerte aus den drei Flash-Tabellen für
die ausgewählte Kurvenform erzeugt,
- die neuen Werte für den TC1-Vergleicher Compare A gesetzt, und
- beim Sinus die ermittelten Kondensatoren eingeschaltet, bei
den drei anderen Kurvenformen jedoch ausgeschaltet.
Das zweite Kriterium, dass alle Kurvenzüge mit einer Null beginnen sollen, hat
nur Konsequenzen bei der Sinustabelle: sie wurde um 270° phasenverschoben in die
Tabelle geschrieben, damit sie bei Null losgeht.
1.5 Kurvenform-Tabellen
Die vier Flash-Tabellen für die Kurvenformen sind in der
Include-Datei Tabellen.inc abgelegt. Die
Tabelle Sine: erzeugt den Sinus, die Tabelle Triangle:
die Dreieckspannung, die Tabelle Sawtooth: den Sägezahn
und Rectangle: die Rechteckspannung.
Alle Tabellen sind so angelegt, dass sie nach der Analogisierung im
R/2R-Netzwerk die nachfolgende Elektronik (RC-Filter-Schalter,
Operationsverstärker) mit maximal 3 V Amplitude aussteuern
(dezimal 153 von 255 bei maximal möglicher Aussteuerung), damit
insbesondere der CMOS-Operationsverstärker-Eingang nicht
übersteuert wird. Sie wurden mit einem
Open-Office Spreadsheet
generiert und können daher für andere Kurvenformen abgewandelt
werden. Der Export der Tabellenwerte in die Assembler-Include-Datei
erfolgt durch Markieren der Tabelle in Spalte H, kopieren mit Strg-C
und Pasten in die Include-Datei mit Strg-V.
1.6 CTC-Interrupt-Service-Routine
Die Zeitbeziehungen in der Interrupt-Service-Routine (Anzahl benötigte Takte)
sind also exakt zu erfassen und dürfen 24 Takte nicht
überschreiten. Das ergibt folgende Zeitbeziehungen:
; CTC-compareA-Interrupt: Ausgabe naechster DAC-Wert
TC1CAIsr: ; 6 Taktzyklen fuer Int und Vektor-Jump
in rSreg,SREG ; Status retten, +1 = 7
ld rimp,Y+ ; Naechster Tabellenwert, +2 = 9
out pDacOut,rimp ; in DAC schreiben, +1 = 10
cpi YL,Low(sR2REnd) ; Ende Puffer? +1 = 11
brne TC1CAIsrRet ; +1 = 12, +2 = 13
ldi YH,High(sR2R) ; Neustart ; +1 = 13
sbr rFlag,1<<bStart ; +1 = 14
TC1CAIsrRet: ; 13/14 Takte
out SREG,rSreg ; Status wieder herstellen, +1 = 14/15
reti ; + 4 = 18/19
In allen Fällen bleiben bei der höchsten Frequenz zwischen
6 und 7 Takte pro CTC-Interrupt für andere Aufgaben übrig.
Das ist bei 20 kHz nicht gerade üppig, reicht aber für die
anstehenden Aufgaben aus, insbesondere für das zügige Schreiben
der Ausgabetabelle in das SRAM.
Damit die Software die Frequenz einstellen kann, ist regelmäßig
der Potentiometerstand am Eingang ADC0 zu messen und in die korrekten CTC-Werte
für den TC1-CTC-Interrupt umzurechnen.
2.1 Die Interrupt-Service-Routine der ADC-Messungen
Damit durch die ADC-Messungen keine relevante Störung der DAC-Ausgabe
erfolgt, muss die ADC-Interrupt-Routine möglichst ganz kurz gehalten
werden.
Das gelingt nur, wenn die ADC-Routine mit den folgenden Instruktionen auskommt:
; ADC Interrupt-Service-Routine
AdcInt: ; 6 Takte fuer Int und RJMP
set ; Setzen der T-Flagge im SREG, +1 Takt = 7
reti ; +4 Takte = 11
Die dadurch verursachten 11 Takte Verzögerung machen sich in einer zeitlichen
Verschiebung einer einzigen DAC-Ausgabe um 0,69 Mikrosekunden bemerkbar. Bei 2 Hz
entspricht das einer zeitlichen Verschiebung einer einzigen Ausgabe um 0,035%, bei
20 kHz um 44%. Da die 11 Takte Verzögerung bei 20 kHz schon nach
zwei Einzelausgaben wieder aufgeholt sind (zwischen zwei CTC-Int bleiben in der Regel
7 Takte übrig), handelt es sich nicht um einen merklichen Einfluss.
Außerdem spielen diese Störungen nur beim Dreieck und beim Sägezahn
eine Rolle:
- Beim Sinus mittelt das RC-Netzwerk solche Verschiebungen weitgehend aus.
- Beim Rechteck betrifft die Verzögerung nur zwei der 256 Stufen (nur an
den Pegelwechseln).
2.2 Taktung des ADC
Um die Störungen durch ADC-Interrupts weiter zu minimieren, werden nicht mehr
als unbedingt nötige ADC-Wandlungen durchgeführt. Das wird erreicht, indem
zum Anstoßen des Messvorganges der Timer TC0 verwendet wird:
- Dessen Vorteiler wird auf den höchsten Wert von 1.024 eingestellt,
- Mit einem Compare-A-Wert von 243 wird dieser so eingestellt, dass eine Messung
erst mit jedem 249.856ten Takt begonnen wird.
- Die Messungen werden daher mit ca. 64 Hz oder ca. alle
15,6 Millisekunden angestoßen. Für die hohen Frequenzen ab
2.499 Hz, bei denen niedrige TC1-CTC-Werte vorkommen, bedeutet das jeden
39sten Kurvenzug, der durch den ADC-Int um die minimale Verzögerung von
weniger als 1 µs gestört werden könnte.
Um den Messvorgang anzustoßen, wird
- im Special Function I/O Register (SFIOR) das Bit ADTS1 und das Bit ADTS0
gesetzt, und
- im ADC Control Register A (ADCSRA) das ADATE-Bit gesetzt, und
- der ADC sowie eine erstmalige Wandlung mit gesetztem Interrupt-Enable-Bit
ADIE gestartet.
Alle weiteren Wandlungen erfolgen dann über den Timer, der damit auch über
die weitere Auswertung zeitlich bestimmend ist.
2.3 Weitere Auswertung der ADC-Ergebnisse
Da pro ADC-Messung nur ca. 15,6 ms vergehen, werden sechzehn dieser Messergebnisse
des ADC außerhalb der Interrupt-Service-Routine aufsummiert. Liegen acht
Ergebnisse vor, werden diese wie im nächsten Kapitel beschrieben ausgewertet.
Die Berechnung erfolgt in zwei Stufen:
- der Berechnung der Soll-Frequenz aus dem Potentiometer-Mittelwert,
- der Berechnung des CTC-Wertes aus der Soll-Frequenz.
3.1 Berechnung der Soll-Frequenz aus der Potentiometerstellung
Damit die erzeugte Frequenz linear mit der Potentiometerstellung ansteigt, wird
aus der Stellung des Potentiometers die Sollfrequenz errechnet. In den Grobbereichen
zwischen 2 und 200 Hz wird diese als das 65.536-fache der Sollfrequenz berechnet,
ab 200 Hz als das 256-fache. Dazu werden mit den folgenden Parametern a (Steigung)
und b (Ordinatenabschnitt) in f = Multiplikator * (a * Poti + b) folgende Berechnungen
ausgefürt:
Einstellbereich | Multiplikator | a | b | Min(Poti=0) | Max(Poti=255) |
Dezimal | Hex | Dezimal | Hex |
2 - 20 Hz | 65.536 | (20 - 2) / 255 | 2 | 131.072 | 02.00.00 | 1.310.720 | 14.00.00 |
20 - 200 Hz | 65.536 | (200 - 20) / 255 | 20 | 1.310.720 | 14.00.00 | 13.107.200 | C8.00.00 |
200 - 2.000 Hz | 256 | (2.000 - 200) / 255 | 200 | 51.200 | 00.C8.00 | 512.000 | 07.D0.00 |
2.000 - 20.000 Hz | 256 | (20.000 - 2.000) / 255 | 2.000 | 512.000 | 07.D0.00 | 5.120.000 | 4E.20.00 |
Die Berechnung erfolgt durch Multiplikation des 16-Bitwertes 65.536 bzw.
256 * a mit dem 8-bittigen ADC-Durchschnittswert zum 24-Bit-Ergebnis.
Der Addierer 65.536 bzw. 256 * b wird vor der Multiplikation
in die Ergebnisregister geladen und zusammen mit den einzelnen
Multiplikationswerten aufaddiert (das spart das anschließende
Hinzu-Addieren).
Das nachfolgende Schema zeigt den Ablauf der Berechnung der Sollfrequnz in den
betreffenden Registern.
Das Register rN2 ist zu Beginn der Berechnungen immer Null, es wird bei der
8-Bit-Multiplikation dazu verwendet, um den Multiplikator in jedem Schritt mit
Zwei zu multiplizieren und, falls das aus rmp herausgeschobene Bit eine Eins
ist, zum Ergebnis zu addieren.
Das Ergebnis in rN5:rN4:rN3 ist das 65.536- bzw. 256-fache der Sollfrequenz. Für die
nachfolgende CTC-Berechnung wird rN3 zum Runden verwendet und mit dem 256-fachen bzw.
dem 1-fachen Frequenzwert in rN5:rN4 weitergerechnet.
3.2 Berechnung der CTC-Werte aus der Sollfrequenz
Die Berechnung der CTC-Werte aus den Sollfrequenzen ist mit
nCTC = Takt / 256 / Sollfrequenz - 1
relativ einfach (Division 24 Bit / 16-Bit = 16-Bit. Da in den Bereichen
von 2 bis 200 Hz das 256-fache der Sollfrequenz vorliegt, muss es für diese
Frequenzen heißen:
nCTC = Takt / (256*Sollfrequenz) - 1
Bei der Berechnung der CTC-Werte aus den Sollfrequenzen spielen ab 2.500 Hz aber
zusätzlich noch die Anzahl der in der DAC-Tabelle gespeicherten Kurvenzüge
(2, 4 oder 8) eine Rolle, so dass sich folgende Beziehungen ergeben:
Fein- bereich # | Frequenzbereich (Hz) | DAC- Vielfache | Soll-Frequenz Formel | Dividend Formel | Dividend Dezimal | Dividend Hex | CTCmax bei fmin | CTCmin bei fmax |
0 | 2 - 20 | 1 | (65536*f)/256 | (256*clock)/256 | 16.000.000 | F42400 | 31.249 | 3.124 |
1 | 20 - 200 | 3.124 | 312 |
2 | 200 - 2.000 | (256*f)/256 | clock/256 | 62.500 | 00F424 | 312 | 30 |
3 | 2.000 - 2.500 | 30 | 24 |
4 | 2.500 - 5.000 | 2 | clock/128 | 125.000 | 01E848 | 49 | 24 |
5 | 5.000 - 10.000 | 4 | clock/64 | 250.000 | 03D090 |
6 | 10.000 - 20.000 | 8 | clock/32 | 500.000 | 07A120 |
Der Dividend kommt mit vier Bytes (=32 Bits) aus. Die zu dividierenden Frequenzen
benötigen zwei Bytes (max. 200 * 256 = 51.200). Das Ergebnis passt in
zwei Bytes.
Es ist noch zu beachten, dass das CTC-Ergebnis aus dem Dividenden und dem Divisor um
Eins zu vermindern ist, da im CTC-Betrieb ein Takt mehr ausgeführt wird (das
Rücksetzen des Zählers erfolgt erst nach dem Erreichen des Compare-A-Wertes
und dem Eintreffen des nächsten Zählerimpulses, synchronisiert mit dem
Zählertakt) und nicht bereits mit dem Eintreten der Gleichheit.
Als Besonderheit der Division wurde nicht mit dem Linksschieben des Dividenden begonnen,
damit auch der Fall, dass der Divisor bereits kleiner ist als die beiden oberen Bytes
des Dividenden. Dafür werden 17 und nicht 16 Divisionsschritte durchgeführt.
Das folgende Bild zeigt die Registerverwendung beim Teilen der Taktfrequenz durch die
Sollfrequenz.
3.3 Zeitbedarf der Berechnungen
Die in der folgenden Tabelle angegebenen Zeiten wurden mit
avr_sim
gemessen und beziehen sich auf den Betrieb ohne die Frequenzausgabe durch TC1.
Feinbereich | Frequenz | Zeitbedarf µs | Frequenz | Zeitbedarf µs |
0 | 2 | 26,7 | 19 | 32,4 |
1 | 20 | 26,5 | 199 | 31,9 |
2 | 200 | 26,0 | 1.999 | 32,4 |
3 | 2.000 | 26,4 | 2.499 | 28,3 |
4 | 2.500 | 28,3 | 4.999 | 30,2 |
5 | 5.000 | 30,2 | 9.999 | 31,0 |
6 | 10.000 | 31,0 | 19.999 | 32,0 |
Alle Zeiten liegen um die 30 µs, was in Anbetracht einer
8*24-Bit-Multiplikation und einer 32/24-Bit-Division relativ kurz ist.
Bei der schnellsten Frequenzausgabe-Variante verlängert sich dies
um das 25 / 6 -fache (19 Takte Ausgabe, 6 Takte zur
freien Verfügung), also auf ca. 125 µs, da zwischendurch
immer der TC1-Interrupt zuschlägt.
Die Hardware ermöglicht das Zuschalten von Kondensatoren, um die
Spannungsstufen des DAC-Rechtecksignals auszugleichen. Das macht aber nur beim
Erzeugen von Sinussignalen Sinn, bei allen anderen Kurvenformen nicht.
Im Einzelnen können mit dem digitalen Schalter vier Kondensatoren
von 470 pF, 1 nF, 10 nF und 100 nF einzeln oder
parallel zugeschaltet werden. Für die Auswahl der Kondensatoren
kann die hier beschriebene Software
verwendet werden.
In der Tabelle sind die einzustellenden Kondensatoren in den
verschiedenen Frequenzbereichen zusammengestellt.
Feinbereich | Frequenzbereich | Potentiometer | Kondensator | Kapazität | Schalter |
von | bis | von | bis | 100nF | 10nF | 1nF | n47F | nF | PB3:PB0 |
0 | 2 | 20 | 0 | 255 | X | X | X | X | 111n47 | 1111 |
1 | 20 | 200 | 0 | 255 | | X | X | X | 11n47 | 0111 |
2 | 200 | 500 | 0 | 42 | | X | | | 10n | 0100 |
500 | 2.000 | 43 | 255 | | | X | X | 1n47 | 0011 |
3 | 2.000 | 2.500 | 0 | 255 | | | X | | 1n | 0010 |
4..6 | 2.500 | 20.000 | 0 | 255 | | | | X | n47 | 0001 |
Die Eigenschaften der verschiedenen Filter sind im Folgenden näher
beschrieben.
4.1 Frequenzbereich 2 bis 20 Hz
Im Frequenzbereich 2 bis 20 Hz werden alle vier Kondensatoren
parallel eingeschaltet. Dadurch ergeben sich bei 2 Hz folgende
Kurven:
Wie zu erkennen ist, ergeben sich dadurch saubere Sinussignale.
Bei 20 Hz weist das so geschaltete Filter bereits eine geringe
Abschwächung der Amplitude auf:
Die Abschwächung ist aber gering und hinnehmbar, so dass eine
weitere Differenzierung der zugeschalteten Kondensatoren nicht nötig
ist.
Über den gesamten Frequenzbereich ergibt sich der hier gezeigte
Verlauf.
Die Filterkurve mit n(RC)=2 verläuft über den gesamten
Frequenzbereich oberhalb des 0,9-fachen.
Bei 2 Hz und einem 8-Bit-DAC beträgt die digitale Frequenz
256 Hz. Die dritte und weitere ungeradzahligen Oberwellen, aus
der sich das Rechtecksignal zusammensetzt, liegt daher bei über
750 Hz. Bei diesen Frequenzen weist das RC-Filter mit R=10k und
C=111n47 die nebenstehenden Eigenschaften auf. Die Unterdrückung
dieser Oberwellen ist sehr hoch (Amplitude kleiner 0,04).
Die Filterkurven wurden mit dem Software-Tool rc_sweep erstellt. Es ist
Freeware und in der Windows-64-Version hier
und als Lazarus-Win-Source-Code hier
verfügbar. Achtung! Bei niedrigen Frequenzen, kleinen Widerständen
und kleinen Kondensatoren kann die Ausführung länger dauern.
4.2 Frequenzbereich 20 bis 200 Hz
In diesem Frequenzbereich wird mit dem Kondensator 11n47 (10n+1n+0,47n)
gearbeitet. So sieht die Filterkurve bei der niedrigsten Frequenz,
20 Hz, aus. Der Filtereffekt ist bei dieser niedrigen Frequenz
noch recht bescheiden und wird am oberen Ende des Bereichs noch
deutlich besser.
Und so sieht es bei 200 Hz aus.
Die Abschwächung im gesamten Frequenzbereich liegt bei weniger als
10%.
4.3 Frequenzbereich 200 bis 500 Hz
Bei diesem Frequenzbereich kommt noch der 10 :nF-Kondensator zum Einsatz.
Die Abschwächung bei 500 Hz hält sich in Grenzen. Der
1 nF-Kondensator hätte bei 200 Hz eine zu geringe Filterwirkung:
4.4 Frequenzbereich 500 bis 2.000 Hz
In diesem Bereich werden die beiden Kondensatoren 1 nF und n47F
eingeschaltet. Die Filterkurve sieht so aus.
Im untersten Frequenzbereich bei 500 Hz ergibt sich folgende
Kurve:
4.5 Frequenzbereich 2.000 bis 2.500 Hz
Hier wird nur der 1 nF-Kondensator eingeschaltet. Er sorgt für
eine gute Filterwirkung in diesem Bereich.
4.6 Frequenzbereich 2.500 bis 20.000 Hz
In diesem gesamten Bereich kommt nur der 470 pF-Kondensator zum
Einsatz. Er bringt über den gesamten Bereich gute Filterwirkung.
Bei zweistufigem Betrieb wie in der vorliegenden Schaltung ist zwar
die Abschwächung auf die halbe Ausgangsspannung schon merklich,
aber für einen weiteren kleineren Kondensator von 100 pF
steht kein weiterer Analogschalter zur Verfügung.
An der untersten Frequenzbereichsgrenze von 2.500 Hz und beim
7-Bit-DAC sieht die Kurve so aus:
Die Filterung bei dieser Frequenz zeigt einen sauberen Sinus ohne
Oberwellen. Bei der höchsten Frequenz ist die Abschwächung
noch hinnehmbar:
5.1 Umschaltung bei Frequenzänderung und Kurvenformwechsel
Beim Wechsel des Frequenzbereiches, der Potistellung oder der Schalter für
die Kurvenform ist es nötig, die Zählerhardware mit den neuen Werten
zu versorgen. Das erfolgt erst dann, wenn die ausgegebene Kurve ihren
Nulldurchgang hat und das Bit bStart im Register rFlag durch die TC1-CTC
Interruptserviceroutine gesetzt ist. Bis zum Erreichen dieses Zustands sind die
berechneten Werte in einem SRAM-Bereich abgelegt und werden noch nicht direkt in
die Hardware geschrieben.
Es sind zwei Fälle zu unterscheiden:
- Ändert sich bei der Neuberechnung die Kurvenart (Rechteck, Sägezahn,
Dreieck oder Sinus) oder die Anzahl der Durchgänge einer Kurve (1-fach,
2-fach, 4-fach oder 8-fach), dann müssen die SRAM-Inhalte aus der
Flash-Tabelle neu erzeugt werden.
- Ist das nicht der Fall, dann ist nur der CTC-Wert neu einzustellen.
5.2 Wechsel der Kurvenform
Der erste Fall wird nach der Neuberechnung der Sollwerte durch eine gesetzte
bSetR2R-Flagge signalisiert. Damit die Übernahme der neuen Werte auch in den
Fällen mit kurzen CTC-Werten schneller erfolgt als die Ausgabe über das
R/2R-Netzwerk, muss dies sofort nach dem Nulldurchgang beginnen und mit hoher
Geschwindigkeit erfolgen. Ist das Schreiben der neuen Tabellenwerte langsamer als
deren Ausgabe über den CTC-Interrupt, dann wird die neue Kurve um einen
Kurvenduchgang verzögert ausgegeben. Ist das nicht der Fall, wird sofort die
neue Kurvenform ausgegeben.
Um zu ermitteln, bei welchen Frequenzen diese Umschaltung sofort oder erst mit
Verzögerung erfolgt, ist die Schreibroutine im Folgenden nach der Anzahl
Takte analysiert:
Convert2Sram1: ; 12/22 Takte Instruktionen pro Byte
lpm rmp,Z ; Lese Wert aus Sinustabelle, +2 = 2
st X+,rmp ; Kopiere in SRAM-Puffer, +2 = 4
add ZL,rAdd ; Addieren Tabellenwert, +1 = 5
ldi rmp,0 ; Ueberlauf addieren ; +1 = 6
adc ZH,rmp ; zu MSB ; +1 = 7
inc rCnt ; Anzahl Bytes zaehlen, +1 = 8
breq Convert2SramEnd ; Null, fertig, +1 = 9 (Sprung irrelevant)
mov rmp,rCnt ; Kopiere Byte-Zaehler, +1 = 10
and rmp,rAnd ; Isoliere Bits, +1 = 11
brne Convert2Sram1 ; +2 = 13, +1 = 12
pop ZL ; Anfangsadresse setzen, LSB, +2 = 14
pop ZH ; dto., MSB, +2 = 16
push ZH ; Z wieder sichern, MSB, +2 = 18
push ZL ; dto., LSB, +2 = 20
rjmp Convert2Sram1 ; +2 = 22
Im Falle der einfachen Kurvenform erfolgt kein Durchlauf des 22-Takte-Teils und
pro Byte werden 12 Takte benötigt. Sind zwei Kurventeile zu übertragen
erhöht sich diese Taktzahl auf (12*255+22)/256 = 12,04 Takte pro Byte. Bei
vier Kurven sind es 12,16 und bei acht 12,27 Takte. Der Übergang erfolgt
also dann, wenn der CTC-Durchgang (12+18) = 30 Takte dauert, was im Frequenzbereich
von 10 bis 20 kHz bei etwa 16,7 kHz läge. Da diese Frequenz aber
wesentlich höher liegt als diejenige, bei der ein Neuschreiben der Tabelle
erfolgen muss (9,999 nach 10 kHz), tritt dieser Fall praktisch nicht ein.
Daher ist auch der Fall ausgeschlossen, dass Einzelwerte aus der neuen Tabelle sich
mit Einzelwerten der alten Tabelle mischen und dadurch Störstellen entstehen
könnten: entweder ist beim Neuschreiben der Tabelle die Kopierroutine langsamer
als die Ausgaberoutine (z. B. beim Übergang von 9,999 kHz auf
10 kHz, alter CTC-Wert = 24) oder wesentlich schneller (beim umgekehrten
Übergang, alter CTC-Wert = 49).
Nur beim Umschalten über einen Schalterwechsel kann dieser Fall bei der
oben genannten Frequenz eintreten, dauert dann aber nur für einen einzigen
Durchlauf lang an (1, 2, 4 oder 8 Kurvendurchgänge lang).
5.3 Wechsel der Frequenz
Der zweite Fall, dass nur der CTC-Wert sich ändert, kommt relativ häufiger
vor, da der Poti-Wert um +/- 1 Bit klappern kann. In dem Fall, dass sich dadurch auch
der CTC-Wert um eine Einheit nach unten ändern kann (insbesondere im Bereich
zwischen 2 und 20 Hz) muss vermieden werden, dass der CTC-Wert um Eins kleiner
wird und dadurch ein Compare-Match verpasst werden kann. Dies wäre der Fall,
wenn der Timer in einem der beiden CTC-Modes betrieben würde (CTC durch Compare
Match A im Timer Mode 4 oder durch ICR-Compare-Match im Timer Mode 12), da der neu
geschriebene Compare-Wert sofort angewendet wird. Das könnte dazu führen,
dass der Timer erst 65.535 Takte später wieder den Compare-Match erreicht,
was bei dem hier verwendeten Takt vier Millisekunden lang dauert und bei hohen
Frequenzen eine halbe Ewigkeit lang den Ablauf verzögern würde.
Um das zu vermeiden wird hier von einem Feature der AVR-Timer Gebrauch gemacht: der
neu geschriebene Compare-Wert wird in den PMW-Modi erst dann angewendet, wenn der
Timer Null erreicht (Update of OCR on Bottom). Dadurch kann es nicht zum Verpassen
eines Compare-Match kommen, wenn häufig neue Werte in den Compare A geschrieben
werden. Der Timer TC1 wird daher im Fast-PWM-Modus betrieben (Mode 15), obwohl gar
keine anderen PWM-Features benötigt werden.
5.4 Datenzusammenstellung im SRAM
Um feststellen zu können, ob
- ein Wechsel der Kurvenform, und/oder
- ein Neuschreiben des CTC-Wertes
erforderlich ist, enthält die Software einen umfangreichen Soll/Ist-Speicher.
In diesem Speicherbereich im SRAM sind beide Bereiche in identischer Abfolge
gehalten, damit der Speicherbereich, in dem die Parameter der Neueinstellung
zusammengestellt werden, nach der erfolgten Umschaltung über die alten
Werte kopiert werden können und bei der nächsten Auswertung aller
Einstellungen eindeutig festgestellt werden kann, welche der Parameter zu
ändern sind.
Die Datenzusammenstellung ist im SRAM wie folgt gegliedert:
; Ablagepeicher fuer die aktuellen Einstellwerte
sR2RCurrL: ; 0x0060
.Byte 1 ; Aktuelle R2R-Tabellen-Adresse, LSB
sR2RCurrH: ; 0x0061
.Byte 1 ; dto., MSB
sR2RCurrAdd: ; 0x0062
.Byte 1 ; Aktueller R2R-Tabellen-Addierer
sR2RCurrAnd: ; 0x0063
.Byte 1 ; Aktueller R2R-Tabellen-Wiederholwert
sCmpACurrH: ; 0x0064
.Byte 1 ; Aktueller TC1-CTC-Wert, MSB zuerst
sCmpACurrL: ; 0x0065
.Byte 1 ; dto., LSB danach
sCondCurr: ; 0x0066
.Byte 1 ; Aktueller Kondensator fuer RC-Netzwerk (0..15)
sFineTuneCurr: ; 0x0067
.Byte 1 ; Aktueller Feinbereich Frequenzeinstellung (0..6)
sCourseTuneCurr: ; 0x0068
.Byte 1 ; Aktueller Grobbbereich Frequenzeinstellung (0..3)
sPotCurr: ; 0x0069
.Byte 1 ; Aktueller Potistand, (0..255)
sCurveCurr: ; 0x006A
.Byte 1 ; Aktueller Modus, (0..3)
sCurrEnd: ; Ende aktueller Datenbereich
Der Bereich der Neueinstellung ist identisch gegliedert und benannt, nur fehlt in
den Labeln der Teil "Curr".
5.5 Übernahme der neuen Kurvenform
Sind beide Flaggen bStart und bSetR2R gesetzt, dann erfolgt die Übernahme der
neuen Werte folgendermaßen.
Zunächst wird die neu einzustellende Kurvenform geschrieben. Dazu wird
- das Registerpaar ZH:ZL auf die Adresse der ausgewählten Kurve in
sR2RH:sR2RL gesetzt,
- das Register rAdd aus dem Wert in sR2RAdd geladen, dieses Register gibt an,
um welchen Betrag die Flash-Adresse bei jedem geschriebenen zu erhöhen ist
(1, 2, 4 oder 8), um die richtige Anzahl an Kurven in den Speicherbereich zu
kopieren,
- das Register rAnd aus dem Wert in sR2RAnd geladen, dieses Register gibt an,
bei welcher Anzahl Bytes wieder am Tabellenanfang der Flashtabelle begonnen
werden muss (0 bei einem Kurvendurchgang mit 256 Werten, 0x7F bei zwei Kurven
pro Durchgang, 0x7F bei dreien und 0x3F bei vieren).
Die Routine Convert2Sram: erledigt daraus das Kopieren der Werte aus der
Flashtabelle in den Speicherbereich ab sR2R:. Dabei wird so vorgegangen:
- Aus der Flash-Tabelle, auf die Z zeigt, wird ein Byte ausgelesen und in
den SRAM-Speicherbereich geschrieben. So sieht die Flash-Tabelle beim
Sinus auszugsweise aus:
Sine: ; Sinustabelle 8 bits, 3V max
.db 0,0,0,0,0,1,1,1 ; n=0 - 7
.db 1,2,2,3,3,4,4,5 ; n=8 - 15
; ... bis n = 255
- Zu der Flash-Adresse wird der Wert des Registers rAdd addiert. Soll eine
1:1-Kopie der Flash-Tabelle angelegt werden (eine Kurve pro Durchgang) wird
Eins addiert, sollen zur Frequenzerhöhung mehrere Kurven pro Durchgang
erzeugt werden (2, 4 oder 8), dann wird nur jeder zweite, vierte oder achte
Wert der Flash-Tabelle kopiert.
- Ein Bytezähler wird erhöht. Dieser wird je nach Anzahl der
Kurven pro Durchgang mit 0xFF, 0x7F, 0x3F oder mit 0x1F ver-UND-et. Ergibt
das Null, wird mit der Flash-Tabelle neu begonnen.
- Sind alle 256 Werte der Tabelle geschrieben, wird die Routine beendet.
Das ergibt bei acht Kurvenzügen pro Durchgang im SRAM dann folgende Belegung:
Beginnend ab Adresse sR2R: liegen acht entsprechend verkürzte
Kurvenzüge mit jeweils 32 Bytes Länge im SRAM, alle sind
identisch.
Nach der Änderung der Kurvenform wird die Übernahme mit der Einstellung
der anderen Werte und mit der Aktualisierung der LCD-Ausgabe fortgesetzt.
5.6 Übernahme der neuen CTC-Werte
Nach der Bearbeitung nach 5.5 wird noch geprüft, ob die Flagge bSetCtc
gesetzt ist. Falls ja, wird der unter sCmpAH:sCmpAL: abgelegte neue CTC-Wert
in das Vergleichsregister OCR1A des TC1 geschrieben. Danach werden die Kondensatoren
gemäß des übergebenen Wertes in sCond: eingestellt.
Man beachte, dass die Änderung des CTC-Wertes sich erst nach Beendigung des
aktuell laufenden CTC-Zyklus auswirkt (ab Zyklus 1, bei vorheriger Neueinstellung
der Kurvenform erst nach dem Abschluss des Kopiervorganges). Das kann sich durch eine
Verzögerung minimal bemerkbar machen.
5.7 Aktualisierung der LCD-Anzeige
Wenn eine der Flaggen bSetR2R oder bSetCtc gesetzt war, erfolgt nun
die Aktualisierung der LCD-Anzeige.
Dazu wird
- aus den Einstellungen in sFineTune: und der Potistellung in sPot
zunächst die Displayeinstellung in sDisplayTune: ermittelt: sie gibt
an, in welchem Format die Ausgabe der Frequenz erfolgt (0 = n,nnnn; 1 = nn,nnn;
2 = nnn,nn; 3 = n.nnn,n; 4 = nn.nnn),
- entsprechend dieser Einstellung in sDisplayTune: sowie aus der Einstellung
in sFineTune: wird ein 32 Bit umfassender Dividend geladen, der durch den
eingestellten 16-Bit-CTC-Wert (+1) geteilt wird,
- die resultierende 24-Bit-Ganzzahl in den SRAM-Pufferbereich ab
sLcdBuf: in eine Dezimalziffernfolge umgewandelt (mit Unterdrückung
führender Nullen),
- durch Einfügen von Tausender- und Dezimal-Trennzeichen an den geeigneten
Stellen formatiert,
- durch ein führendes LCD-Spezialzeichen ergänzt, das die Kurvenform
symbolisiert, und schließlich
- der Pufferinhalt in die LCD geschrieben.
Die einzelnen Schritte sind nachfolgend detaillierter beschrieben.
5.7.1 Ermittlung der Frequenz aus dem CTC-Wert
Die Frequenzberechnung aus den CTC-Werten erfolgt durch Teilen des Taktes
durch den CTC-Wert von Teiler TC1 (+1). Dabei werden die anzuzeigenden
Kommastellen als Quasi-Festkomma-Integer-Werte behandelt. So wird das
Frequenzergebnis 2,0000 Hz als binärer Wert 20.000 aus der
Division erhalten, das Einfügen des Kommas an der richtigen Stelle
(2,0000) erfolgt erst nach der Umwandlung in die Dezimalzahl. Ebenso das
Einfügen des Tausendertrenners an der richtigen Stelle (2,000.0).
5.7.2 Berechnung der Ist-Frequenz
Drei Parameter gehen in die Frequenzberechnung ein:
- der CTC-Wert aus dem Timer TC1,
- der Grobbereich (0 .. 3) und der Feinbereich (0 .. 6),
- die Anzahl der auszugebenden Dezimalstellen (4..0, fünf Displaybereiche).
Bedingt durch die Darstellungsweise der Ergebnisse ergeben sich die folgenden
Darstellungsbereiche und deren Frequenzberechnung:
f (Hz) | Grob- | Fein- | Display- |
Dezimal- | Darstellung | CTC+1 |
Dividend | Minimum | Maximum |
von | bis | bereich | stellen | 01234567 | von | bis | Formel | Dezimal | Hex | Dezimal | Hex | Dezimal | Hex |
2 | 9,9999 | 0 | 0 | 0 | 4 | M 2,0000 | 31.250 | 6.250 | Takt*10.000/256 | 625.000.000 | 25.40.BE.40 | 2,0000 | 00.4E.20 | 10,0000 | 01.86.A0 |
10 | 19,999 | 1 | 3 | M 10,000 | 6.249 | 3.125 | Takt*1.000/256 | 62.500.000 | 03.B9.AC.A0 | 10,002 | 00.27.12 | 20,000 | 00.4E.20 |
20 | 99,999 | 1 | 1 | 3.124 | 625 | 20,000 | 00.4E.20 | 100,000 | 01.86.A0 |
100 | 199,99 | 2 | 2 | M 100,00 | 624 | 313 | Takt*100/256 | 6.250.000 | 00.5F.5E.10 | 100,00 | 00.27.10 | 199,68 | 00.4E.40 |
200 | 999,99 | 2 | 2 | 312 | 63 | 199,68 | 00.4E.80 | 992,06 | 01.83.86 |
1.000 | 1.999,9 | 3 | 1 | M1.000,0 | 62 | 31 | Takt*10/256 | 625.000 | 00.09.89.68 | 992,1 | 00.26.C1 | 2.016,1 | 00.4E.C1 |
2.000 | 2.499,9 | 3 | 3 | 30 | 25 | 2.016,1 | 00.4E.C1 | 2.500,0 | 00.61.A8 |
2.500 | 4.999,9 | 4 | 4 | 50 | 25 | Takt*10/128 | 1.250.000 | 00.13.12.D0 | 2.500.0 | 00.61.A8 | 5.000,0 | 00.C3.50 |
5.000 | 9.999,9 | 5 | 5 | Takt*10/64 | 2.500.000 | 00.26.25.A0 | 5.000,0 | 00.C3.50 | 10.000,0 | 01.86.A0 |
10.000 | 20.000 | 6 | 6 | 0 | M 10.000 | Takt/32 | 500.000 | 00.07.A1.20 | 10.000 | 00.27.10 | 20.000 | 00.4E.20 |
Bei der Frequenzberechnung werden folgende Ressourcen benötigt:
- 4 Bytes (32 Bits) für den Dividenden,
- 2 Bytes (16 Bits) für den Divisor (CTC+1),
- 3 Bytes (17 Bits) für das Ergebnis (f * Dezimalstellen).
Die Division des Dividenden (4 Bytes, 32 Bit) durch den Divisor (2 Byte,
16 Bit) erfolgt wie üblich. Nur eine Besonderheit ist dabei zu
beachten: wenn die beiden oberen Bytes des Dividenden von Beginn der
Division an schon größer sind als der Divisor (dieser
Fall tritt unter bestimmten Relationen auf - in den Fällen in
der Tabelle, bei denen das Divisionsergebnis in der Spalte
Maximum hex 01.00.00 erreicht), muss die Division nicht mit dem
Linksschieben des Dividenden sondern mit dem Abziehen des Divisors
beginnen. Dafür sind dann 17 Divisionsschritte auszuführen
statt 16. Da das Divisionsergebnis in diesen Fällen tatsächlich
17 Bits hat, muss bei der Dezimalumwandlung das 17.te Bit mit verarbeitet
werden (100.000-er).
5.7.3 Umwandlung der binären Frequenz in die dezimale Anzeige
Die binäre Frequenz ist dann in eine Dezimalzahl zu wandeln. Die größte
Dezimalzahl ist dabei 100.000 (binär: 01.86.A0). Die einzelnen Dezimalziffern
werden dazu jeweils schrittweise mit den dezimalen Vielfachen von 10 verglichen (von
100.000 abwärts über 10.000, 1.000, 100, 10 bis 1) und jeweils n-fach
abgezogen, bis beim Abziehen ein Unterlauf eintritt.
Das Vorgehen ist im Folgenden am Beispiel der Zahl 234.567 ausgeführt.
Im ersten Schritt wird die Zahl mit der binären 100.000 (hex 01.86.A0) verglichen.
Diese lässt sich von der Zahl zwei Mal abziehen, bevor ein Unterlauf auftritt.
Durch das Abziehen verschwindet die erste Ziffer der Zahl (siehe die Zahl in der
nächsten Zeile.
Die "2" wird in eine ASCII-Zahl umgewandelt, indem einfach 48 addiert wird.
Sie wird ein einem Ausgabepuffer abgelegt und der Pufferzeiger um Eins erhöht.
Als Zweites sind die Zehntausender dran. Danach die Tausender, etc. Die letzte Ziffer
braucht nicht mehrfach abgezogen werden, sie stellt als solche den Einer-Rest dar.
Dies hier ist der Quellcode der Dezimalumwandlung. Im SRAM steht in der Zelle sCurve
das Zeichen für die Kurvenformausgabe der LCD (0 = Sägezahn
,
1 = Sinus
,
2 = Dreieck
,
3 = Rechteck)
,
das als Erstes an die LCD ausgegeben wird. Danach wird der
Umwandlungspuffer durch Überschreiben mit Leerzeichen geleert. Das Register rN3
dient zur Unterdrückung führender Nullen (1 = Unterdrücken, 0 = nicht
mehr unterdrücken).
; Divisionsergebnis ist in rN6:rN5:rN4
DecimalConversion:
ldi XH,High(sLcdBuf) ; X zeigt auf Puffer
ldi XL,Low(sLcdBuf)
lds rmp,sCurve
st X+,rmp ; Kurvenform-zeichen in LCD-Ausgabepuffer
ldi rmp,' '
DecConv1:
st X+,rmp ; Leerzeichen in den Puffer
cpi XL,Low(sLcdBuf+8)
brne DecConv1
ldi XH,High(sLcdBuf+2) ; Auf Anfang Ausgabe
ldi XL,Low(sLcdBuf+2)
clr rN3 ; Fuehrende Nullen unterdruecken
inc rN3 ; auf Eins
ldi ZH,High(2*Dec6Tab) ; Dezimaltabelle in Z
ldi ZL,Low(2*Dec6Tab)
DecConv2:
lpm rN0,Z+ ; Lese Dezimal-Digit-Binaerzahl
lpm rN1,Z+
lpm rN2,Z+
tst rN0 ; Ende der Tabelle?
breq DecConv6
ldi rmp,0xFF ; Ergebnis in rmp
DecConv3:
inc rmp ; Naechstes Ergebnis
sub rN4,rN0 ; Dezimalzahl abziehen, Byte 1
sbc rN5,rN1 ; dto., Byte 2
sbc rN6,rN2 ; dto., Byte 3
brcc DecConv3 ; Weiter abziehen
add rN4,rN0 ; Letzte Subtraktion rueckgaengig, Byte 1
adc rN5,rN1 ; dto., Byte 2
adc rN6,rN2 ; dto., Byte 3
tst rmp ; Ergebnis Null?
breq DecConv5 ; Ist Null
clr rN3 ; Fuehrende-Nullen-Flagge loeschen
DecConv4:
; Nicht unterdruecken
subi rmp,-'0' ; Digit in ASCII-Ziffer wandeln
st X+,rmp
rjmp DecConv2
DecConv5:
tst rN3 ; Fuehrende Nullen unterdruecken?
breq DecConv4 ; Nein, als '0' ausgeben
ldi rmp,' ' ; Fuehrende Null unterdruecken
st X+,rmp ; in LCD-Puffer
rjmp DecConv2 ; Weiter wandeln
DecConv6:
ldi rmp,'0'
add rmp,rN4 ; Letzte Ziffer
st X,rmp ; in LCD-Puffer
Dazu gehört noch die Dezimaltabelle DecTab:, die die
binärkodierten Dezimalvielfachen angibt.
;
; Dezimaltabelle 6 Ziffern = 24 Bit
Dec6Tab:
.db Byte1(100000),Byte2(100000)
.db Byte3(100000),Byte1(10000)
.db Byte2(10000),Byte3(10000)
.db Byte1(1000),Byte2(1000)
.db Byte3(1000),Byte1(100)
.db Byte2(100),Byte3(100)
.db Byte1(10),Byte2(10)
.db Byte3(10),0
.db 0,0
5.8 Finalisierung der Datenübernahme
Abschließend werden noch
- der Neueinstellungsbereich im SRAM ab sTransfStart: bis sTransfEnd:
über den Bereich ab sCurrStart: kopiert,
- die Flaggen bSetR2R: und gelöscht.
5.9 Zeitbedarf
Alle angegebenen Zeiten wurden mit
avr_sim
gemessen und beziehen sich auf den Betrieb ohne die Frequenzausgabe durch TC1.
5.9.1 Zeitbedarf beim Wechsel der Kurvenform
Wechselt bei der Neueinstellung die Kurvenform, dann dauert dies minimal
225,6 µs bei einer Kurve, bei acht zu erzeugenden Kurven
226,4 µs, also minimal länger.
5.9.2 Zeitbedarf der Übernahme
Der Zeitbedarf dieses Teilschrittes beläuft sich auf bescheidene
6,2 µs und ist unabhängig von anderen Parametern konstant.
5.9.3 Zeitbedarf der Frequenzanzeigeberechnung
Feinbereich | Frequenz | Zeitbedarf µs | Frequenz | Zeitbedarf µs |
0 | 2 | 44,6 | 19 | 51,9 |
1 | 20 | 45,1 | 199 | 51,7 |
2 | 200 | 46,4 | 1.999 | 50,2 |
3 | 2.000 | 49,6 | 2.499 | 48,8 |
4 | 2.500 | 48,8 | 4.999 | 48,3 |
5 | 5.000 | 48,3 | 9.999 | 44,1 |
6 | 10.000 | 44,1 | 19.999 | 44,5 |
Alle Zeiten liegen um die 50 µs, mit geringer Variationsbreite.
5.9.4 Zeitbedarf der LCD-Ausgabe
Am Ende ist die Position der LCD an den Anfang zu stellen und die acht
Zeichen aus dem SRAM-Puffer an die LCD auszugeben. Der Zeitbedarf für
ein einzelnes Zeichen sowie die Positionseinstellung ist mit maximal
40 µs zu veranschlagen, mit allen Programmrahmenoperationen
nicht mehr als 60 µs. Neun Schreiboperationen belaufen sich
damit auf ca. 540 µs.
5.9.5 Gesamter Zeitbedarf
Der gesamte Zeitbedarf für die Datenübernahme (6,2 µs),
für die Frequenzberechnung mit anschließender Dezimalwandlung
und Dezimal- bzw. Tausender-Punktsetzung (50 µs) und dem
Schreiben auf die LCD (540 µs) beläuft sich damit auf ca.
600 µs.
Ist zusätzlich eine neue Kurvenform einzustellen, dann sind weitere
226 µs zu veranschlagen, was zu 826 µs Gesamtdauer
führt.
Verlängert sich die Bearbeitung durch zwischenzeitlich eintretende
TC1-Interrupts zur Frequenzausgabe, dann sind bei der schnellsten
Frequenzausgabe-Variante Verlängerungen auf bis zu 3,5 ms
Ausführungsdauer zu veranschlagen.
Durch diese lange Dauer könnten Wechselwirkungen auftreten.
Allerdings treten ADC-Messzyklen nur alle 15 ms ein, so dass
die Ausgabe in jedem Fall kürzer bleibt als ein einziger Messzyklus.
Der gesamte Messzyklus für 16 Messungen hat ca. 250 ms Dauer
und endet mit der Neuberechnung aller Parameter. Auch dieser Zeitraum
wird weit unterschritten. Nebenwirkungen treten daher keine auf.
Die für die LCD-Anzeige erforderliche Assembler-Software ist in der
Include-Datei lcd.inc zusammengestellt, die
hier genauer beschrieben ist. Die in der
Include-Datei zur Verfügung gestellten Programmteile sind mit den
jeweiligen Aufrufparametern in einem Abschnitt in deren Kopf dokumentiert.
Alle zur Verfügung gestellten Routinen verwenden das Register R16
als Vielzweckregister. Alle weiteren verwendeten Register sind in
der Routinenliste benannt. Die Register R0 und R1 sind bei einigen
Routinen ebenfalls verwendet, ihre Inhalte sind aber mit Push und Pop
gesichert.
6.1 Einstellungen der LCD-Anzeige
6.1.1 Parametersatz für lcd.inc
Nachfolgend ist ist der Parametersatz dargestellt. Er enthält alle
Parameter, die die Include-Datei erwartet und die den konkreten Anwendungsfall
charakterisieren.
; Parameter-Satz der Einstellungen
.equ clock = 16000000 ; Taktfrequenz Prozessor in Hz
; LCD-Groesse:
.equ LcdLines = 1 ; Anzahl Zeilen (1, 2, 4)
.equ LcdCols = 8 ; Anzahl Zeichen pro Zeile (8..24)
; LCD-Ansteuerung
.equ LcdBits = 8 ; Busbreite (4 oder 8)
; Wenn 4-Bit-Ansteuerung:
;.equ Lcd4High = 1 ; Busnibble (1=Oberes, 0=Unteres)
.equ LcdWait = 0 ; Ansteuerung (0 mit Busy, 1 mit Warteschleifen)
; LCD-Datenports
.equ pLcdDO = PORTC ; Daten-Ausgabe-Port
.equ pLcdDD = DDRC ; Daten-Richtungs-Port
; LCD-Kontrollports und -pins
.equ pLcdCEO = PORTA ; Control E Ausgabe-Port
.equ bLcdCEO = PORTA7 ; Controll E Ausgabe-Portpin
.equ pLcdCED = DDRA ; Control E Richtungs-Port
.equ bLcdCED = DDA7 ; Control E Richtungs-Portpin
.equ pLcdCRSO = PORTA ; Control RS Ausgabe-Port
.equ bLcdCRSO = PORTA5 ; Controll RS Ausgabe-Portpin
.equ pLcdCRSD = DDRA ; Control RS Richtungs-Port
.equ bLcdCRSD = DDA5 ; Control RS Richtungs-Portpin
; Wenn LcdWait = 0:
.equ pLcdDI = PINC ; Daten-Input-Port
.equ pLcdCRWO = PORTA ; Control RW Ausgabe-Port
.equ bLcdCRWO = PORTA6 ; Control RW Ausgabe-Portpin
.equ pLcdCRWD = DDRA ; Control RW Richtungs-Port
.equ bLcdCRWD = DDA6 ; Control RW Richtungs-Portpin
; Wenn Dezimalausgabe benoetigt wird:
;.equ LcdDecimal = 1 ; Wenn definiert: einbinden
; Wenn Hexadezimalausgabe benoetigt wird:
;.equ LcdHex = 1 ; Wenn definiert: einbinden
; Wenn nur Simulation im SRAM:
;.equ avr_sim = 1 ; 1=Simulieren, 0=Nicht simulieren
6.1.2 Spezialzeichen
Die Routine LcdChars bietet die Möglichkeit, eigene Zeichen zu
definieren. Sie wird hier benutzt, um die aktuell eingestellte Kurvenform
zu symbolisieren. Die Spezialzeichen sind mit dem
Open-Office-Tool Zeichengenerator
entworfen, das auf der Webseite
hier
näher beschrieben ist.
Im Einzelnen sind die Zeichen 0 bis 3 definiert und werden zu Beginn mit der
Routine LcdChars in die LCD geschrieben. Die diesen Zeichen entsprechende
Zeichentabelle liefert das OpenOffice-Tool, sie sieht folgendermaßen aus:
; Tabelle der Codezeichen
Charcodes:
.db 64,0,0,1,3,5,9,17,1,0 ; Z = 0, Saegezahn
.db 72,0,0,8,20,20,21,5,2,0 ; Z = 1, Sinus
.db 80,0,0,4,4,10,10,17,17,0 ; Z = 2, Dreieck
.db 88,0,0,14,10,10,10,10,27,0 ; Z = 3, Rechteck
.db 0,0 ; Ende der Tabelle
Die Routine LcdChars aus der lcd.inc sieht
folgendermaßen aus:
; +-------+----------------+--------------+
; |Routine|Funktion |Parameter |
; +-------+----------------+--------------+
; |LcdSpec|Erzeugt Spezial-|Z: 2*Tabellen-|
; | |zeichen | adresse |
; +-------+----------------+--------------+
; ...
;
; LcdSpec erzeugt Spezialzeichen auf der LCD
; Z zeigt auf 2*Tabellenadresse
; Tabellenformat:
; 1.Byte: Adresse des Zeichens 0b01zzz000,
; 0: Ende der Tabelle
; 2.Byte: Dummy
; 3. bis 10.Byte: Daten der Zeilen 1 bis 8
LcdSpec:
push R0 ; R0 ist Zaehler
LcdSpec1:
lpm R16,Z+ ; Lese Adresse des Zeichens
tst R16 ; Ende der Tabelle?
breq LcdSpec3 ; Ende Tabelle
rcall LcdCtrl ; Adresse schreiben
adiw ZL,1 ; Ueberlese Dummy
ldi R16,8 ; 8 Byte pro Zeichen
mov R0,R16 ; R1 ist Zaehler
LcdSpec2:
lpm R16,Z+ ; Datenbyte lesen
rcall LcdData ; Datenbyte ausgeben
dec R0 ; Abwaerts zaehlen
brne LcdSpec2 ; Weiter Daten ausgeben
rjmp LcdSpec1 ; Naechstes Zeichen
LcdSpec3:
pop R0 ; R0 wieder herstellen
ldi ZH,0 ; Cursor Home
ldi ZL,0
rjmp LcdPos
Im Registerpaar Z wird die Position der Zeichentabelle im Flash
übergeben (wegen LPM mit 2*Codezeichen). Die Codes 0 bis 3
entsprechen den beiden Kurvenform-Bits, wie sie von den
Eingabeschaltern her eingelesen werden. Sie werden in der ersten
Spalte der LCD-Anzeige angezeigt.
6.2 Start der LCD-Anzeige
Die Initiierung der LCD-Anzeige beinhaltet
- die Konfigurierung der erforderlichen Ports und Pins der LCD,
- die Einstellung der LCD auf die nötigen Parameter (Portbreite,
Anzeigeauflösung, etc.),
- die Erzeugung der vier Spezialzeichen zur symbolisierten Anzeige
der Kurvenform, und
Die Routine LcdInit: erledigt diese Schritte.
6.3 Startanzeige
Die Ausgabe einer Startmeldung mit dem Programmnamen und der Version
erfolgt im Hauptprogramm durch Aufruf der Funktion LcdText: mit
der Flashtabelle in Z als Parameter.
Die Startanzeige zeigt, dass die LCD korrekt initiiert ist und
funktioniert. Sie lautet:
LcdInitText:
.db "SigGenV1",0xFE,0xFE ; Textausgabe bei Init
Sie wird mit folgendem Code in die LCD geschrieben:
clr ZH ; Position = 0/0, Zeile -1
clr ZL : Spalte -1
rcall LcdPos ; Setze Position
ldi ZH,High(2*LcdInitText) ; Z auf Text
ldi ZL,Low(2*LcdInitText)
rcall LcdText
Alle weiteren Funktionen der LCD-Include, die verwendet werden, finden
sich unter dem Suchbegriff call lcd im Quellcode, die Aufrufe
sind in Kopf der Include-Datei näher erläutert.
7.1 Kurvenformen
Die Kurvenform-Tabellen für den 8-Bit DAC sind in der
Include-Datei Tabellen.inc angegeben. Sie wurden mit dem
Open-Office-Spreadsheet Wellentabellengenerator
erzeugt.
Damit es beim Umschalten auf eine andere Kurvenform nicht zu steilen
Pegeländerungen kommt, wurde dabei der Sinus um +270° phasenverschoben.
Damit beginnen alle Kurven bei dem DAC-Wert Null und es kommt beim Umschalten
der Kurvenform nicht zu steilen Sprüngen.
Der Digital-Analog-Wandler ist als R/2R-Netzwerk mit 1%-Widerständen
aus Werten von 1k10 und 2k20 aufgebaut. 1%-Widerstände sind ein Muss
weil das oberste Bit 128-mal so stark in das Ergebnis eingeht als das
unterste Bit und sich höhere Differenzen des Bit7-Widerstandswerts
entsprechend gravierend auf das Endergebnis auswirken. Außerdem gibt
es 1k10-Widerstände ohnehin nur in der E24-Reihe und die ist sowieso
fast immer 1% genau.
Die Auswahl der Kombination 1k10 und 2k20 wurde aus folgenden Gründen
gewählt:
- Natürlich kann man 2*R auch durch Reihenschaltung zweier
R-Widerstände und R auch durch Parallelschaltung zweier
2*R-Widerstände erzeugen. Das vergrößert aber das
Widerstandsgrab von 16 auf 24 zu verbauende Widerstände und
kostet Platz auf der Platine.
- In der E12-Reihe gibt es gar keine, in der E24-Reihe nicht so arg viele
Kombinationen, bei denen der einfache Widerstandswert R UND genau der
doppelte Widerstandswert 2*R vorkommen, das hier sind schon alle:
- 1,0 und 2,0
- 1,1 und 2,2
- 1,2 und 2,4
- 1,5 und 3,0
- 1,8 und 3,6
- 7,5 und 15,0
Das schränkt die Auswahlmöglichkeiten schon mal ziemlich
ein.
- Beim Sinus wird eine RC-Filterkombination mit R = 10k an den
AD-Wandler nachgeschaltet. Die Verwendung von Widerständen
mit >10k hätte die Größe der Kondensatoren
entsprechend verringert. Um eine stabile R/2R-Spannung
hinzukriegen, ist die Belastung durch das RC-Netzwerk
möglichst bei weniger als einem Zehntel des
R/2R-Netzwerk-Innenwiderstands zu halten. Die Wahl von 1k10
als R für das R/2R-Netzwerks trägt dieser Anforderung
Rechnung.
- Bei Auswahl noch niedrigerer Widerstände für das
R/2R-Netzwerk, um die Störung durch das RC-Netzwerk weiter
zu verringern, nehmen die Ströme aus den und in die Portpins
deutlich zu und die CMOS-Ausgänge liefern bzw. treiben
mit abnehmender Spannung (im High-Zustand) bzw. mit zunehmender
Spannung (im Low-Zustand) und die resultierenden Spannungen des
R/2R-Netzwerks werden entsprechend ungenauer (siehe Berechnung
hier).
- Außerdem steigt bei niedrigeren Widerständen der
Stromverbrauch der Schaltung an, da acht Portpins gleichzeitig
zu bedienen sind. In der hier angegebenen Schaltung beträgt;
der maximale Strom aus den bzw. in die acht Portpins 8,08 mA
(übrigens bei der Binärkombination 0x55 oder 0b01010101).
Die Kombination 1k10 und 2k20 stellt daher einen guten Kompromiss
dar.
7.3.1 Spannungen und Ströme im R/2R-Netzwerk
Die Berechnung der Spannungen und Ströme in einem
8-Bit-R/2R-Netzwerk ist nicht ganz trivial, da alle Spannungen
und Ströme vom Zustand von 8 Eingangsbits abhängen
und sich alle gegenseitig beeinflussen. Das Schaltbild zeigt alle
Ströme und Spannungen. Die angezeigten Stromrichtungen zeigen
positive Ströme an, die Ströme können dabei auch
negativ werden (entspricht einer Umkehr des Richtungspfeils).
7.3.2 Berechnung der Ausgangsspannung des Netzwerks
Damit wir nicht die Ausgangsspannungen als Riesenformeln handhaben
müssen, da immer alle acht Eingangsspannungen in das Ergebnis
jeder Spannungsberechnung eingehen, behelfen wir uns damit, dass
wir das Ergebnis der Ausgangsspannung des R/2R-Netzwerks schon
kennen: alle acht Spannungen der Eingangspins Un gehen entsprechend
ihrer binären Gewichtung (1, 2, 4, 8, 16, 32, 64, 128) in das
Endergebnis ein, durch Teilen mit 256 erhält man das Ergebnis
als Spannungswert am Ausgang (UR7).
7.3.3 Berechnung der Spannung an UR6
Aus der bekannten Spannung an UR7 und der
Eingangsspannung an U7 lässt sich die Spannung an
UR6 berechnen, denn die beiden Ströme
IR7 und IE7 sind identisch (sofern
kein Strom an Uout entnommen wird) und betragen
(U7 - UR7) / 2R. Die Formeln zur Berechnung von UR6
ergeben sich demnach wie folgt.
Durch Kombination der beiden Ströme und Umstellung der
Formeln erhält man eine sehr einfache Formel für
UR6 und kann diese Spannung aus UR7
und U7 berechnen.
Bemerkenswert ist hier schon, dass die Widerstandswerte R
gänzlich aus der Formel herausfallen und sich nur
noch die "2" in der Formel hält (in U7/2).
7.3.4 Berechnung der weiteren Spannungen URn
Die nächstniedrige Spannung UR5 im Netzwerk
kann folgendermaßen aus den beiden Strömen
IR6 und IE7 errechnet werden, die
sich zum Strom IE6 ergänzen.
Im Endergebnis ergibt sich wiederum eine recht einfache Formel
für UR5, in die UR6, UR7
und die Spannung am Eingang U6 eingehen.
Analog dazu können alle weiteren Spannungen URn
aus 2,5 * UR(n+1) - UR(n+2) und U(n+1)/2
berechnet werden und erhält so alle Spannungen an den
Verbindungsknoten des Netzwerks für eine gegebene
Kombination von Eingangsspannungen U0 bis U7.
7.3.5 Beispielberechnung für die Eingangskombination 10101010
Dies hier zeigt die Spannungen und Ströme in dem Netzwerk
bei einem Eingabewert von 0xAA oder 0b10101010, bei dem die Bits
die Polarität wechseln.
Man erkennt, dass die Spannungen URn keinen einfachen
Regeln folgen. Je nach der Polarität des Eingangsbits ergibt
sich eine niedrigere oder höhere Spannung als die darunter
oder darüber liegende.
Auch bei den Strömen IRn ergeben sich keine
einfachen Regelmäßigkeiten außer dass diese bei
High-Pegel am Eingang Un negativ sind und bei High-Pegeln positiv.
Alle liegen aber um die ca. 1 mA, was einer Gesamtsumme von
7,85 mA aus allen acht Portausgängen entspricht (egal
welche Polarität der Strom hat). Die einfache Summe liegt
aber bei nur 0,51 mA, weil sich positive und negative
Ströme bei der Kombination 0xAA fast ganz ausgleichen.
Auch die Ströme IEn folgen keinen einfachen
Regelmäßigkeiten außer dass sie bei der
Eingangskombination 0b10 positiv und bei 0b01 negativ sind.
Sie sind aber nicht Null, wenn 0b00 oder 0b11 an zwei
benachbarten Eingängen anliegt, auch in diesen
Fällen fließt noch Strom aus oder in die darunter-
oder darüberliegenden Netzwerk-Knoten.
Dies hier sind alle Ströme aus den und in die Ports bei
allen 256 Kombinationen. Aufsummiert sind die jeweiligen acht
Absolutwerte der Portpin-Ströme.
Der höchste Gesamtstrom mit 8,08 mA ergibt sich
bei der Kombination 0x55 = 0b01010101. Ansonsten sind kaum
Regelmäßigkeiten zu erkennen.
Hypergenaue Zeitgenossen setzen als Eingangsspannungen für
Nullen eher 0,022 V ein, für Einsen 4,97 V, da die
CMOS-Ausgänge bei 0,63 mA im Low-Zustand oder 0,82 mA
Strom im High-Zustand (jeweils Mittelwerte aus allen 256
Eingangs-Kombinationen) schon nicht mehr bis zum Anschlag treiben,
und erhalten ein etwas genaueres Ergebnis.
Um die Abweichungen zu quantifizieren wurden hier
- die Spannungen an den Pins im High-Zustand (Stromwert positiv)
und im Low-Zustand aus dem Strom des Pins errechnet,
- die Spannung im High-Zustand nach dem Datenblatt
des ATmega16A sowie des älteren ATmega16 ermittelt:
bei 5 V Betriebsspannung und 20 Pinstrom nach
Minus beträgt die Spannung 4,2 V, entsprechend
sinkt die Spannung auf U = 5 - 0,04 * I (mA) ab,
- die Spannung im Low-Zustand ebenfalls nach Datenblatt
auf U = 0,035 * I (mA) berechnet,
- die Pinströme der Berechnung im Idealzustand oben
angesetzt, und
- die sich aus dem Netzwerk mit höheren Low- und
niedrigeren High-Spannungen resultierenden
Ausgangspannungen für alle 256 Zustände
errechnet und ihre jeweilige Abweichung gegenüber
dem Sollwert betrachtet und auf 5 V bezogen.
Die Berechnung zeigt, dass die Abweichungen in allen Fällen
unter +/-1% und daher unter der Genauigkeit der Widerstände
im Netzwerk bleiben.
Das würde sich nur dann ändern, wenn die Ströme
im Netzwerk auf etwas mehr als das Doppelte (ca. 2,5 mA)
ansteigen würden. Auch deshalb ist die Auswahl von 1k10
und 2k20 gerechtfertigt, weil die Ströme damit unter
der Grenze bleiben, bei der die realen CMOS-Ausgangsspannungen
sich auf das Ergebnis auswirken würden.
7.4 Tabellen für den Kurvenverlauf
Die Tabelle mit dem Kurvenverlauf ist im Kapitel
9 Listing dokumentiert.
Bei der Software-Entwicklung wurden verschiedene Schalter eingebaut,
die zur Überprüfung der Software im Simulator geeignete
Einstellungen vornehmen.
8.1 Debug-Schalter
Die folgenden Debug-Schalter sind eingebaut:
- debug_CtcCalc: Dieser Schalter erlaubt es, eine Frequenz
cFrequency einzustellen (2..20.000) und die Kurvenform
cCurve einzustellen und sofort zur CTC-Berechnungsroutine
zu springen. Im SRAM wird daraus der neue Parametersatz ausgegeben.
Die weitere Ausführung des Programms erfolgt nicht.
- debug_Transfer: Ist dieser Schalter gesetzt, wird der
neue Parametersatz unmittelbar übernommen und die Wellenform
und CTC-Frequenz eingestellt. Danach erfolgt keine weitere
Programmausführung.
- debug_Second: Nachdem die Software normal mit der
eingestellten Frequenz fStart und der eingestellten
Kurvenform in curvegestartet ist, wird bei der Ermittlung
der nächsten Werte die in cFrequency2 und die
Kurvenform in cCurve2 eingestellt. Der AD-Wandler
wird durch ein schnelleres Timing in cTCCR02
beschleunigt, um die Wartezeit zu verkürzen.
- debug_TC1Off: Mit diesem Schalter kann die Einstellung
der Wellenform im SRAM und die Ausführung des TC1-Interrupts
zur DAC-Ansteuerung abgeschaltet werden, um davon unbeeinflusste
Ausführungsgeschwindigkeiten messen zu können.
- debug_NoLcd: Dieser Schalter unterbindet die Initiierung
der LCD sowie Ausgaben an die LCD.
- debug_insert: Dieser Schalter diente der simulierten
Überprüfung des Einfügens von Dezimal- und
Tausendertrennpunkten in die Zahlenausgabe im SRAM.
- debug_lcdonly: Dieser Schalter dient dem Debugging der
LCD. Nach der Eröffnungsmeldung wird die weitere
Programmausführung unterbunden.
Alle Debug-Schalter müssen auf Null gestellt werden, um eine
endgültig laufende ausführbare Version zu erzeugen.
8.2 Simulationsergebnisse
Hier der Signalgenerator im Simulator, mit
avr_sim
simuliert.
Viel beschäftigt ist der Timer TC1, wenn man Frequenzen über 2.500 Hz
erzeugen lässt:
Links ist der Compare Match erreicht, der einen Takt später (rechts) den
Interrupt-Vektor anstößt.
Entsprechend ist der Zeitanteil, die der Prozessor im Schlafmodus verbringt,
von der eingestellten Frequenz abhängig: ca. 15% bei 20 kHz
(links), ca. 71% bei 10 kHz bei 10 kHz.
Hier dann ein 2.000 Hz Sinussignal mit 256 Stufen, noch ohne
RC-Filternetzwerk.
Die Stufungen des DA-Wandlers sind praktisch nur zu erahnen. Die Frequenz ist
wegen Rundungsfehlern der Ganzzahl-Mathematik nicht exakt.
Die Verringerung der Ausgangsspannung auf Operationsverstärker-Eingangs-freundliche
3 V Spitze wirkt sich hier erkennbar aus.
Eine saubere Dreieckspannung mit 2 kHz.
Die Stufungen des hier nur 32-stufigen DA-Wandlers sind hier noch schön
zu sehen, bevor es in das RC-Filter geht.
Und hier mal ein Sägezahn mit 5 kHz. Die Stufungen des DA-Wandlers
mit 64 Einzelwerten pro Durchgang sind gerade noch zu erkennen.
Ein Rechtecksignal mit 20 kHz.
Hier wird von einem 10-kHz-Sinus auf einen 10-kHz-Sägezahn und dann auf
einen 20 kHz-Sägezahn umgeschaltet. Die Umschaltung von Sinus auf
Sägezahn erfolgt beim Nulldurchgang der Kurve. Die Umstellung auf die
höhere Frequenz erfolgt nicht beim Nulldurchgang.
9.1 Haupt-Sourcecode
Hier ist der gesamte Code für das Hauptprogramm, es ist
hier als Assembler-Quellode
downloadbar.
;
; ***********************************************
; * Signal generator ATmega16: Sawtooth/Sine/ *
; * Triangle/Rectangle, with R/2R-DAC+8-Bit-LCD *
; * Version 1, September 2018 *
; * (C)2018 by http://www.avr-asm-tutorial.net *
; ***********************************************
;
; Include file with type-specific constants
.NOLIST
.INCLUDE "m16def.inc" ; Header file for ATmega16
.LIST
;
; ============================================
; S I M U L A T I O N - S W I T C H E S
; ============================================
;
; Final version: All debug flags = 0
; Debug version: Debugging with simulator, flags = 1
;
; Simulation der CTC-Werte zur Frequenzeinstellung
.equ debug_CtcCalc = 0 ; Debuggen der Berechnungsroutinen CTC
.if debug_CtcCalc == 1
.equ cFrequency = 2 ; Zu simulierende Frequenz in Hz
.equ cCurve = 1 ; Kurvenform (0..3)
; 00 = Saegezahn, 01 = Sinus, 10 = Dreieck, 11 = Rechteck
.endif
;
; Simulation des Transfers der eingestellten Werte
; (wird nur ausgefuehrt wenn auch debug_CtcCalc
; eingeschaltet ist)
.equ debug_Transfer = 0 ; Debuggen des Wertetransfers
;
; Simulation des Uebergangs vom ersten zur zweiten Kurvenform
; definiert Frequenz und Schalterstellungen der zweiten
; Kurve
.equ debug_Second = 0 ; Schnelles Umschalten auf 2. Kurvenform
.if debug_Second == 0
.equ cFrequency2 = 20000 ; Die naechste Frequenz
.equ cCurve2 = 0 ; Die naechste Kurvenform
.equ cTCCR02 = (1<<WGM01)|(1<<CS01) ; Prescaler = 8
.endif
;
; TC1 abschalten
.equ debug_TC1off = 0 ; TC1 zum Simulieren abschalten
;
; Simulation ohne LCD-Ausgaben
; (Dieser Schalter kann mit anderen Schaltern zusammen
; gesetzt werden.)
.equ debug_NoLcd = 0 ; Debuggen ohne LCD-Ausgabeoperationen
;
.equ debug_insert = 0 ; Debuggen der Anzeigenverschiebung
.if debug_insert == 1
.equ InsertDecPos = 5
.equ InsertDecChar = '.'
.equ InsertThdPos = 1
.equ InsertThdChar = ','
.endif
;
; Inbetriebnahme-Tests Schalter
.equ debug_lcdonly = 0 ; Nur LCD testen (Init)
;
; ============================================
; H A R D W A R E I N F O R M A T I O N E N
; ============================================
;
; Device = ATmega16
; ___________
; 1 / |40
; S1A-S2A o--|PB0 ADC0|--o Frequenzpoti
; S1B-S2B o--|PB1 PA1|--o S1 00 = 2 - 20 Hz, 01 = 20 - 200 Hz
; S1C-S1C o--|PB2 PA2|--o S2 10 = 200 - 2000 Hz, 11 = 2 - 20 kHz
; S1D-S2D o--|PB3 PA3|--o S3 Kurve 0, 00 = Saegezahn, 01 = Sinus
; NC o--|PB4 PA4|--o S4 Kurve 1, 10 = Dreieck, 11 = Rechteck
; MOSI o--|PB5 PA5|--o LCD-Ctrl RS
; MISO o--|PB6 PA6|--o LCD-Ctrl R/W
; SCK o--|PB7 PA7|--o LCD-Ctrl E
; 10k+5V o--|RESET AREF|--o C=100n
; +5V o--|VCC GND|--o 0V
; 0V o--|GND AVCC|--o C=100n, L=22uH
; Quarz o--|XTAL2 PC7|--o LCD-D7
; 16 MHz o--|XTAL1 PC6|--o LCD-D6
; DAC-D0 o--|PD0 PC5|--o LCD-D5
; DAC-D1 o--|PD1 PC4|--o LCD-D4
; DAC-D2 o--|PD2 PC3|--o LCD-D3
; DAC-D3 o--|PD3 PC2|--o LCD-D2
; DAC-D4 o--|PD4 PC1|--o LCD-D1
; DAC-D5 o--|PD5 PC0|--o LCD-D0
; DAC-D6 o--|PD6 PD7|--o DAC-D7
; 20|____________|21
;
;
; ============================================
; P O R T S U N D P I N S
; ============================================
;
; Ports und Pins zur RC-Filterkontrolle
.equ pRcfOut = PORTB ; Ausgabeport RC-Filter
.equ pRcfDir = DDRB ; Richtungsport RC-Filter
.equ bRcfA = PORTB0 ; Pin RC-Filter A
.equ bRcfB = PORTB1 ; Pin RC-Filter B
.equ bRcfC = PORTB2 ; Pin RC-Filter C
.equ bRcfD = PORTB3 ; Pin RC-Filter D
.equ mRcf = (1<<bRcfA)|(1<<bRcfB)|(1<<bRcfC)|(1<<bRcfD) ; Maske
; Frequenzbereiche der Kondensatoren
.equ cFein0 = 0x0F ; 2..20 Hz: 111,47 nF
.equ cFein1 = 0x07 ; 20..200 Hz: 11,47 nF
.equ cFein2l = 0x04 ; 20..500 Hz: 10 nF
.equ cFein2h = 0x03 ; 500..2000 Hz: 1n47
.equ cFein3 = 0x02 ; 2000..2500 Hz: 1n
.equ cFein46 = 0x01 ; 2500..20000 Hz: n47
.equ cPot2h = 255*(500-200)/(2000-200)+1 ; 500 Hz-Schwelle
; Ports, Pins und Masken Digital-Analog-Konverter
.equ pDacOut = PORTD ; Ausgabeport DAC
.equ pDacDir = DDRD ; Richtungsport DAC
.equ mDac5 = 0b00011111 ; Maske 5-Bit-DAC
.equ mDac6 = 0b00111111 ; Maske 6-Bit-DAC
.equ mDac7 = 0b01111111 ; Maske 7-Bit-DAC
.equ mDac8 = 0b11111111 ; Maske 8-Bit-DAC
; Ports und Pins zur LCD-Kontrolle siehe LCD-Einstellungen
; (Ports LCD-Datenbus siehe LCD-Einstellungen)
; Ports, Pins und Masken Schalter
.equ pSwOut = PORTA ; Ausgabeport Schalter
.equ pSwDir = DDRA ; Richtungsport Schalter
.equ pSwIn = PINA ; Leseport Schalter
.equ bSw1 = PORTA1 ; Schalter 1
.equ bSw2 = PORTA2 ; Schalter 2
.equ bSw3 = PORTA3 ; Schalter 3
.equ bSw4 = PORTA4 ; Schalter 4
.equ mSwPu = (1<<bSw1)|(1<<bSw2)|(1<<bSw3)|(1<<bSw4) ; Maske
;
; ============================================
; L C D - E I N S T E L L U N G E N
; ============================================
;
; Parameter-Satz der Einstellungen
.equ clock = 16000000 ; Taktfrequenz Prozessor in Hz
; LCD-Groesse:
.equ LcdLines = 1 ; Anzahl Zeilen (1, 2, 4)
.equ LcdCols = 8 ; Anzahl Zeichen pro Zeile (8..24)
; LCD-Ansteuerung
.equ LcdBits = 8 ; Busbreite (4 oder 8)
; Wenn 4-Bit-Ansteuerung:
;.equ Lcd4High = 1 ; Busnibble (1=Oberes, 0=Unteres)
.equ LcdWait = 0 ; Ansteuerung (0 mit Busy, 1 mit Warteschleifen)
; LCD-Datenports
.equ pLcdDO = PORTC ; Daten-Ausgabe-Port
.equ pLcdDD = DDRC ; Daten-Richtungs-Port
; LCD-Kontrollports und -pins
.equ pLcdCEO = PORTA ; Control E Ausgabe-Port
.equ bLcdCEO = PORTA7 ; Controll E Ausgabe-Portpin
.equ pLcdCED = DDRA ; Control E Richtungs-Port
.equ bLcdCED = DDA7 ; Control E Richtungs-Portpin
.equ pLcdCRSO = PORTA ; Control RS Ausgabe-Port
.equ bLcdCRSO = PORTA5 ; Controll RS Ausgabe-Portpin
.equ pLcdCRSD = DDRA ; Control RS Richtungs-Port
.equ bLcdCRSD = DDA5 ; Control RS Richtungs-Portpin
; Wenn LcdWait = 0:
.equ pLcdDI = PINC ; Daten-Input-Port
.equ pLcdCRWO = PORTA ; Control RW Ausgabe-Port
.equ bLcdCRWO = PORTA6 ; Control RW Ausgabe-Portpin
.equ pLcdCRWD = DDRA ; Control RW Richtungs-Port
.equ bLcdCRWD = DDA6 ; Control RW Richtungs-Portpin
; Wenn Dezimalausgabe benoetigt wird:
;.equ LcdDecimal = 1 ; Wenn definiert: einbinden
; Wenn Hexadezimalausgabe benoetigt wird:
;.equ LcdHex = 1 ; Wenn definiert: einbinden
; Wenn nur Simulation im SRAM:
;.equ avr_sim = 1 ; 1=Simulieren, 0=Nicht simulieren
;
; ================================================
; K O N S T A N T E N Z U M E I N S T E L L E N
; ================================================
;
.equ fstart = 10000 ; Startfrequenz in Hz
.equ curve = 1 ; Kurvenform, kann sein:
; 0=Saegezahn, 1=Sinus, 2=Dreieck, 3=Rechteck
;
; Sprachauswahl:
; beeinflusst Dezimalpunkte und Tausender-Trennzeichen
.equ LangEn = 0 ; 0 = Deutsch, 1 = Englisch
;
; =======================================================
; F E S T E + A B G E L E I T E T E K O N S T A N T E N
; =======================================================
;
; Umrechnung der Startfrequenz in SRAM-Tabellenparameter
.if fstart < 2500
.equ cFStart = (clock / 256 + fstart/2) / fstart - 1
.equ cAdd = 1
.equ cAnd = 0xFF
.endif
.if (fstart>=2500)&&(fstart<5000)
.equ cFStart = (clock / 256 * 2 + fstart/2) / fstart - 1
.equ cAdd = 2
.equ cAnd = 0x7F
.endif
.if (fstart>=5000)&&(fstart<10000)
.equ cFStart = (clock / 256 * 4 + fstart/2) / fstart - 1
.equ cAdd = 4
.equ cAnd = 0x3F
.endif
.if (fstart>=10000)&&(fstart<=20000)
.equ cFStart = (clock / 256 * 8 + fstart/2) / fstart - 1
.equ cAdd = 8
.equ cAnd = 0x1F
.endif
;
; Konstante cAdcAdd: Software-Zaehler fuer ADC-Messwerte
;
.equ cTc0Presc = 1024 ; TC0-Prescaler
.equ cTc0Comp = 243 ; TC0-Compare Match
.equ cAdcAdd = 16 ; 16 Compare Match ADC Zyklen
;
; Konstanten zur Sollfrequenzberechnung
;
; Steilheit fuer die Frequenzberechnung
.equ cM0 = (65536*(20-2)+127)/255 ; Bereich 2-20 Hz
.equ cM1 = (65536*(200-20)+127)/255 ; Bereich 20-200 Hz
.equ cM2 = (256*(2000-200)+127)/255 ; Bereich 200-2.000 Hz
.equ cM3 = (256*(20000-2000)+127)/255 ; Bereich 2-20 kHz
;
; Addierwert fuer Potistellung 0
.equ cA0 = 65536*2 ; Bereich 2-20 Hz
.equ cA1 = 65536*20 ; Bereich 20-200 Hz
.equ cA2 = 256*200 ; Bereich 200-2.000 Hz
.equ cA3 = 256*2000 ; Bereich 2-20 kHz
;
; Potentiometerwerte fuer Feinbereich
.equ cP10 = ((10-2)*255+(20-2)/2)/(20-2)+1 ; 10 Hz Schwelle
.equ cP100 = ((100-20)*255+(200-20)/2)/(200-20)+1 ; 100 Hz Schwelle
.equ cP1000 = ((1000-200)*255+(2000-200)/2)/(2000-200) ; 1000 Hz Schwelle
.equ cP2500 = ((2500-2000)*255+(20000-2000)/2)/(20000-2000) ; 2.500 Hz Schwelle
.equ cP5000 = ((5000-2000)*255+(20000-2000)/2)/(20000-2000) ; 5.000 Hz Schwelle
.equ cP10000 = ((10000-2000)*255+(20000-2000)/2)/(20000-2000) ; 10.000 Hz Schwelle
;
; Potentiometerwerte fuer Kondensatorzuschaltung
.equ cPC0 = 0 ; Gesamter Feinbereich 0
.equ cC0L = 0x0F ; Alle C zuschalten
.equ cC0H = 0x0F ; Alle C zuschalten
.equ cPC1 = 0 ; Gesamter Feinbereich 1
.equ cC1L = 0x07 ; Alle ausser der hoechste
.equ cC1H = 0x07 ; Alle ausser der hoechste
.equ cPC2 = 43 ; Feinbereich 2
.equ cC2L = 0x04 ; C bei kleiner als Schwelle
.equ cC2H = 0x03 ; C bei groesser/gleich Schwelle
.equ cPC3 = 0 ; Gesamter Feinbereich 3
.equ cC3L = 0x02 ; Nur 1 nF
.equ cC3H = 0x02 ; Nur 1 nF
.equ cPC46 = 0 ; Gesamte Feinbereiche 4 bis 6
.equ cC46L = 0x01 ; Nur 470 pF
.equ cC46H = 0x01 ; Nur 470 pF
;
; Divisor fuer CTC-Wertberechnung
.equ cDiv01 = clock ; Divisor Clock/(256*Frequenz), 1 Kurvenzug
.equ cDiv23 = clock/256 ; Divisor Clock/Frequenz, 1 Kurvenzug
.equ cDiv4 = clock/128 ; Divisor Clock/Frequenz, 2 Kurvenzuege
.equ cDiv5 = clock/64 ; Divisor Clock/Frequenz, 4 Kurvenzuege
.equ cDiv6 = clock/32 ; Divisor Clock/Frequenz, 8 Kurvenzuege
;
; Divisionskonstanten fuer Anzeigedarstellung
.equ cND0 = ((10*clock+128)/256)*1000 ; 2..9,9999 Hz, 1 Kurvenzug, 4 Dezimalstellen
.equ cND1 = ((10*clock+128)/256)*100 ; 10..99,999 Hz, 1 Kurvenzug, 3 Dezimalstellen
.equ cND2 = ((10*clock+128)/256)*10 ; 100..999,99 Hz, 1 Kurvenzug, 2 Dezimalstellen
.equ cND3 = (10*clock+128)/256 ; 1.000..2.499,9 Hz, 1 Kurvenzug, 1 Dezimalstelle
.equ cND4 = (10*clock+64)/128 ; 2.500..4.999,9 100 Hz, 2 Kurvenzuege, 1 Dezimalstelle
.equ cND5 = (10*clock+32)/64 ; 5.000..9.999,9 Hz, 4 Kurvenzuege, 1 Dezimalstelle
.equ cND6 = (clock+16)/32 ; 10.000..20.000 Hz, 8 Kurvenzuege, 0 Dezimalstellen
;
; Debug-Einstellungen umrechnen
;
; Debug der Ctc-Berechnung
.if debug_CtcCalc == 1 ; CTC-Berechnung simulieren
.if cFrequency < 20
.equ cCourseTune = 0 ; Grobbereich Frequenz (0..3)
.equ cPot = (65536*cFrequency - cA0) / cM0
.endif
.if (cFrequency>=20)&&(cFrequency<200)
.equ cCourseTune = 1 ; Grobbereich Frequenz (0..3)
.equ cPot = (65536*cFrequency - cA1) / cM1
.endif
.if (cFrequency>=200)&&(cFrequency<2000)
.equ cCourseTune = 2 ; Grobbereich Frequenz (0..3)
.equ cPot = (256*cFrequency - cA2) / cM2
.endif
.if (cFrequency>=2000)&&(cFrequency<20000)
.equ cCourseTune = 3 ; Grobbereich Frequenz (0..3)
.equ cPot = (256*cFrequency - cA3) / cM3
.endif
.endif
;
; Debug der Umschaltung auf die zweite Kurvenform/Frequenz
.if debug_Second == 1
.if cFrequency2 < 20
.equ cCourseTune2 = 0
.equ cAdc2 = (1023*(cFrequency2-2))/18
.else
.if cFrequency2 < 200
.equ cCourseTune2 = 1
.equ cAdc2 = (1023*(cFrequency2-20))/180
.else
.if cFrequency2 < 2000
.equ cCourseTune2 = 2
.equ cAdc2 = (1023*(cFrequency2-200))/1800
.else
.equ cCourseTune2 = 3
.equ cAdc2 = (1023*(cFrequency2-2000))/18000
.endif
.endif
.endif
.equ cPot2 = cAdc2/4
.equ cTCCR0 = (1<<WGM01)|(1<<CS01)|(1<<CS00) ; Prescaler = 8
.endif
;
; ============================================
; T I M I N G
; ============================================
;
; Timer TC1 (16-Bit-PWM mit CompA-Interrupt):
; erledigt die Ausgabe der Signale ueber R/2R-DAC
;
; Taktfrequenz = 16.000.000 Hz
; Vorteiler = 1
;
; f = Taktfrequenz / 256 * Kurven / (CTC+1)
; (Kurven = Anzahl Kurvenzuege in 256 Schritten)
; (Feinbereiche: 0..6 in Klammern)
;
; +--------------+----------+--------+---------+
; | Feinbereich | Frequenz | Kurven | TC1-CTC |
; |--------------+----------+--------+---------+
; | 2 - 20 Hz | 2 Hz | 1 | 31.249 |
; | (0) | 20 Hz | | 3.124 |
; +--------------+----------+--------+---------+
; | 20 - 200 Hz | 20 Hz | 1 | 3.124 |
; | (1) | 200 Hz | | 312 |
; +--------------+----------+--------+---------+
; | 200 - 2000 Hz| 200 Hz | 1 | 312 |
; | (2) | 2000 Hz | | 30 |
; +--------------+----------+--------+---------+
; | 2 - 2,5 kHz | 2 kHz | 1 | 30 |
; | (3) | 2,5 kHz | | 24 |
; +--------------+----------+--------+---------+
; | 2,5 - 5 kHz | 2,5 kHz | 2 | 49 |
; | (4) | 5 kHz | | 24 |
; +--------------+----------+--------+---------+
; | 5 - 10 kHz | 5 kHz | 4 | 49 |
; | (5) | 10 kHz | | 24 |
; +--------------+----------+--------+---------+
; | 10 - 20 kHz | 10 kHz | 8 | 49 |
; | (6) | 20 kHz | | 24 |
; +--------------+----------+--------+---------+
;
; Extrema Frequenzbereich:
; Niedrigste Frequenz: Takt / 256 / 65536 = 0,954 Hz
; Hoechste Frequenz : Takt / 256 * 8 / 21 = 23,8 kHz
;
; Timing TC0:
; steuert das Einlesen des ADC-Wertes und
; die Neujustierung der Ausgabefrequenz
;
; Taktfrequenz TC0 = 16.000.000 Hz
; Prescaler TC0 = 1.024
; CTC-Wert TC0 = 243
; Messzyklusfrequenz = 64 Hz
; Messzyklus alle = 15,6 ms
; Anzahl ADC-Messungen = 16
; Update-Zyklus = 249,9 ms
; Update-Frequenz = 4,002 Hz
;
; ============================================
; R E G I S T E R D E F I N I T I O N E N
; ============================================
;
.def rN0 = R0 ; Multi-Rechenregister, Byte 0
.def rN1 = R1 ; dto., Byte 1
.def rN2 = R2 ; dto., Byte 2
.def rN3 = R3 ; dto., Byte 3
.def rN4 = R4 ; dto., Byte 4
.def rN5 = R5 ; dto., Byte 5
.def rN6 = R6 ; dto., Byte 6
.def rCnt = R7 ; Byte-Zaehler fuer R/2R-Tabelle
.def rAdd = R8 ; Addierer fuer R/2R-Tabelle
.def rCntRst = R9 ; Byte-Zahler fuer Restart R/2R-Tabelle
.def rTabEL = R10 ; Tabellenende, nur LSB
.def rTabAL = R11 ; Tabellenanfang, LSB
.def rTabAH = R12 ; dto., MSB
.def rAdcL = R13 ; ADC-Ergebnissumme, LSB
.def rAdcH = R14 ; dto, MSB
.def rSreg = R15 ; Statusregister-Zwischenspeicher
.def rmp = R16 ; Vielzweckregister
.def rimp = R17 ; Vielzweckregister innerhalb Ints
.def rFlag = R18 ; Flaggenregister
.equ bStart = 0 ; Wellenzug komplett, Null-Durchgang
.equ bSetR2R = 1 ; Uebernahme-Flagge fuer neue Einstellung R2R
.equ bSetCtc = 2 ; Uebernahme-Flagge fuer neue Einstellung CTC-Wert
.def rAdc = R19 ; ADC-Werte-Zaehler
.def rAnd = R20 ; Und-Register fuer R/2R-Tabelle
.def rPot = R21 ; Poti-Stellungswert
; Frei: R22..R25
; Verwendet: R27:R26 = X fuer Kopieren R2R-Tabelle
; Verwendet: R29:R28 = Y SRAM-Adresse R/2R-Tabelle
; Verwendet: R31:R30 = Z fuer Tabellenlesen innerhalb Int
;
; ============================================
; S R A M D E F I N I T I O N E N
; ============================================
;
.DSEG
.ORG SRAM_START
; Ablagepeicher fuer die aktuellen Einstellwerte
sCurrStart:
sR2RCurrL: ; 0x0060
.Byte 1 ; Aktuelle R2R-Tabellen-Adresse, LSB
sR2RCurrH: ; 0x0061
.Byte 1 ; dto., MSB
sR2RCurrAdd: ; 0x0062
.Byte 1 ; Aktueller R2R-Tabellen-Addierer
sR2RCurrAnd: ; 0x0063
.Byte 1 ; Aktueller R2R-Tabellen-Wiederholwert
sCmpACurrH: ; 0x0064
.Byte 1 ; Aktueller TC1-CTC-Wert, MSB zuerst
sCmpACurrL: ; 0x0065
.Byte 1 ; dto., LSB danach
sCondCurr: ; 0x0066
.Byte 1 ; Aktueller Kondensator fuer RC-Netzwerk (0..15)
sFineTuneCurr: ; 0x0067
.Byte 1 ; Aktueller Feinbereich Frequenzeinstellung (0..6)
sCourseTuneCurr: ; 0x0068
.Byte 1 ; Aktueller Grobbbereich Frequenzeinstellung (0..3)
sPotCurr: ; 0x0069
.Byte 1 ; Aktueller Potistand, (0..255)
sCurveCurr: ; 0x006A
.Byte 1 ; Aktueller Kurvenform, (0..3)
sCurrEnd: ; Ende aktueller Datenbereich
sDummy1: ; 0x006B
.Byte 0x0070-sCurrEnd ; Fuer Schoenheit
;
; Uebernahmepeicher fuer die Neueinstellung
sTransfStart:
sR2RL: ; 0x0070
.Byte 1 ; R2R-Tabellen-Adresse, LSB
sR2RH: ; 0x0071
.Byte 1 ; dto., MSB
sR2RAdd: ; 0x0072
.Byte 1 ; R2R-Tabellen-Addierer
sR2RAnd: ; 0x0073
.Byte 1 ; R2R-Tabellen-Wiederholwert
sCmpAH: ; 0x0074
.Byte 1 ; TC1-CTC-Wert, MSB zuerst
sCmpAL: ; 0x0075
.Byte 1 ; dto., LSB danach
sCond: ; 0x0076
.Byte 1 ; Kondensator fuer RC-Netzwerk (0..15)
sFineTune: ; 0x0077
.Byte 1 ; Feinbereich Frequenzeinstellung (0..6)
sCourseTune: ; 0x0078
.Byte 1 ; Grobbbereich Frequenzeinstellung (0..3)
sPot: ; 0x0079
.Byte 1 ; Potistand, (0..255)
sCurve: ; 0x007A
.Byte 1 ; Kurvenform, (0..3)
; 00 = Saegezahn, 01 = Sinus, 10 = Dreieck, 11 = Rechteck
sTransfEnd: ; Ende Uebernahme-Datenbereich
;
sDummy2: ; 0x007B
.Byte 0x007F-sTransfEnd ; Fuer Schoenheit
sDisplayTune: ; 0x007F
.Byte 1 ; Displaybereiche, 0 (2-9,9999 Hz), 1 (10-99,999 Hz),
; 2 (100-999,99 Hz), 3 (1-2,4999 kHz),
; 4 (2,5-4,9999 kHz), 5 (5-9,99 kHz), 6 (10-20 kHz)
;
; LCD-Ausgabepuffer
sLcdBuf: ; 0x0080 .. 0x0087
.Byte 8 ; 8 Zeichen auf LCD
;
sDummy3: ; 0x0088
.Byte 8
;
sLcdBufE: ; 0x0090
; R/2R Ausgabepuffer im SRAM
sR2R: ; 0x0090 .. 0x018F
.byte 256
sR2REnd:
;
; ==============================================
; R E S E T U N D I N T V E K T O R E N
; ==============================================
;
.CSEG
.ORG $0000
jmp Main ; Reset-Vektor
reti ; INT0 Int Vektor 1
nop
reti ; INT1 Int Vektor 2
nop
reti ; TC2COMP Int Vektor 3
nop
reti ; TC2OVF Int Vektor 4
nop
reti ; TC1CAPT Int Vektor 5
nop
rjmp TC1CAIsr ; TC1COMPA Int Vektor 6
nop
reti ; TC1COMPB Int Vektor 7
nop
reti ; TC1OVF Int Vektor 8
nop
reti ; TC0OVF Int Vektor 9
nop
reti ; SPI_STC Int Vektor 10
nop
reti ; USART_RXC Int Vektor 11
nop
reti ; USART_UDRE Int Vektor 12
nop
reti ; USART_TXC Int Vektor 13
nop
rjmp AdcIsr ; ADC Int Vektor 14
nop
reti ; EE_RDY Int Vektor 15
nop
reti ; ANA_COMP Int Vektor 16
nop
reti ; TWI Int Vektor 17
nop
reti ; INT2 Int Vektor 18
nop
rjmp Tc0Isr ; TC0COMP Int Vektor 19
nop
reti ; SPM_RDY Int Vektor 20
nop
;
; ==========================================
; I N T E R R U P T S E R V I C E
; ==========================================
;
; CTC-compareA-Interrupt: Ausgabe naechster DAC-Wert
TC1CAIsr: ; 6 Taktzyklen fuer Int und Vektor-RJMP
in rSreg,SREG ; Status retten, +1 = 7
ld rimp,Y+ ; Naechster Tabellenwert, +2 = 9
out pDacOut,rimp ; in DAC schreiben, +1 = 10
cpi YL,Low(sR2REnd) ; Ende Puffer? +1 = 11
brne TC1CAIsrRet ; +1 = 12, +2 = 13
ldi YH,High(sR2R) ; Neustart ; +1 = 13
sbr rFlag,1<<bStart ; +1 = 14
TC1CAIsrRet: ; 13/14 Takte
out SREG,rSreg ; Status wieder herstellen, +1 = 14/15
reti ; + 4 = 18/19
;
; TC0 Compare Match Interrupt
Tc0Isr: ; 6 Taktzyklen fuer Int und Vektor-Sprung
clt ; Loesche T-Flagge, +1 = 7
reti ; +4 = 11
;
; ADC Conversion complete Interrupt
AdcIsr: ; 6 Taktzyklen fuer Int und Vektor-Sprung
set ; Setze T-Flagge, +1 = 7
reti ; +4 = 11
;
; ============================================
; H A U P T P R O G R A M M I N I T
; ============================================
;
Main:
; Initiiere Stapel
ldi rmp, HIGH(RAMEND) ; Initiiere MSB Stapel
out SPH,rmp
ldi rmp, LOW(RAMEND) ; Initiiere LSB Stapel
out SPL,rmp
.if debug_insert == 1 ; Debuggen der Verschiebung
ldi ZH,High(sLcdBuf)
ldi ZL,Low(sLcdBuf)
ldi rmp,' '
st Z+,rmp
st Z+,rmp
st Z+,rmp
ldi rmp,'1'
debug_insert1:
st Z+,rmp
inc rmp
cpi rmp,'7'
brne debug_insert1
ldi rmp,InsertDecPos
mov rN0,rmp
ldi rmp,InsertDecChar
rcall Insert
ldi rmp,InsertThdPos
mov rN0,rmp
ldi rmp,InsertThdChar
rcall Insert
debug_insert_loop:
rjmp debug_insert_loop
.endif
; Initiiere Port A
in rmp,PORTA ; Lese Ausgaberegister
ori rmp,mSwPu ; Setze Pull-Up-Bits Schalter
out PORTA,rmp
in rmp,DDRA ; Lese Richtungsregister
andi rmp,0xE0 ; Loesche Pull-Up Pins und ADC0
out DDRA,rmp
; Init Port B
in rmp,pRcfDir ; Lese Richtungsregister RC-Filter-Port
ori rmp,mRcf ; Setze RC-Filter-Bits
out pRcfDir,rmp ; Setze Richtungsbits RC-Filter
in rmp,pRcfOut ; Lese Ausgangsport RC-Filter-Port
andi rmp,0xFF-mRcf ; Maskiere RC-Filter-Bits
out pRcfOut,rmp
; Init Port C
; (Erfolgt durch lcd.inc)
; Init Port D
ldi rmp,mDac8 ; 8-Bit-DAC
out pDacDir,rmp ; in Richtungsregister
clr rmp ; auf Anfang Signal
out pDacOut,rmp
;
.if (debug_CtcCalc == 0)&&(debug_NoLcd == 0)
; Init LCD
rcall LcdInit ; LCD-Anzeige initiieren
ldi ZH,High(2*Charcodes) ; Spezialzeichen
ldi ZL,Low(2*Charcodes)
rcall LcdSpec ; Spezialzeichen setzen
clr ZH ; Cursor auf Anfang
clr ZL
rcall LcdPos
ldi ZH,High(2*LcdInitText) ; Z auf Text
ldi ZL,Low(2*LcdInitText)
rcall LcdText
ldi rmp,20 ; warte 1 s
LcdStart1:
rcall LcdWait50ms ; Warte fuer 50 ms
dec rmp
brne LcdStart1
.endif
.if debug_lcdonly ; Nur LCD testen
LcdOnlyLoop:
rjmp LcdOnlyLoop
.endif
; Init ADC
ldi rAdc,cAdcAdd ; Anzahl ADC-Werte zum Addieren
ldi rmp,(1<<ADTS1)|(1<<ADTS0) ; TC0 Compare match startet ADC
out SFIOR,rmp ; Special Function I/O Port Register
ldi rmp,1<<REFS0 ; AD-Kanal 0, REFS0=UB
out ADMUX,rmp
; Erste Wandlung beginnen
.set cADStart = (1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADATE)
.set cADStart = cADStart+(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
ldi rmp,cADStart
out ADCSRA,rmp
; Kurvenausgabe starten
InitCurve:
ldi rmp,Curve ; Eingestellte Kurvenform
lsl rmp ; * 2
ldi ZH,High(2*WaveTabAddr) ; Stelle initiale Kurvenform ein
ldi ZL,Low(2*WaveTabAddr)
add ZL,rmp ; Zur Tabellenadresse addieren
ldi rmp,0
adc ZH,rmp ; Und Carry addieren
lpm rmp,Z+ ; LSB Wellenformadresse in Zwischenspeicher
lpm ZH,Z ; und Adresse MSB lesen
mov ZL,rmp ; LSB in ZL
ldi rmp,cAdd ; Addierer (Kurvenvervielfacher
mov rAdd,rmp
sts sR2RAdd,rAdd
ldi rAnd,cAnd ; Und fuer Kurvenneustart
sts sR2RAnd,rAnd
.if (debug_CtcCalc == 0)&&(debug_TC1Off == 0)
rcall Convert2Sram ; Schreibe Kurve in SRAM
.endif
.if debug_CtcCalc == 1
; Debug-Intercept
ldi rmp,2*cCourseTune+8*cCurve ; Stelle Schalter her
out PORTA,rmp
ldi rmp,0x1E ; Schalte Schalter als Outputs
out DDRA,rmp
ldi rmp,cPot ; Lege Potistellung fest
sts sPot,rmp ; Potistellung speichern
rcall debug_CtcCalc_start
.if debug_Transfer == 1
; Uebernahme des Datensatzes
ldi rFlag,(1<<bStart)|(1<<bSetR2R)
rcall Transfer
.endif
Debug_CtcCalc_Loop:
rjmp Debug_CtcCalc_Loop
.endif
; Timer starten
; TC0 ist Update-Timer zum ADC lesen
; Compare Match startet ADC-Wandlung
ldi rmp,cTc0Comp ; Compare-Wert
out OCR0,rmp
.if debug_Second == 1
; Stelle vorgewaehlte Schalterstellungen ein
ldi rmp,2*cCourseTune2+8*cCurve2 ; Stelle Schalter her
out PORTA,rmp
out PINA,rmp ; Geht bei ATmega16, kein Invertier-Feature
ldi rmp,0x1E ; Schalte Schalter als Outputs
out DDRA,rmp
; Fuer Simulation beschleunigt
ldi rmp,cTCCR02 ; CTC, Prescaler = 64
.else
; Normale Rate
ldi rmp,(1<<CS02)|(1<<CS00)|(1<<WGM01) ; CTC, Presc=1024
.endif
out TCCR0,rmp
; TC1 als DA-Wandler-Treiber starten
ldi YH,High(sR2R) ; Y als Zeiger auf DAC-Puffer im SRAM
ldi YL,Low(sR2R)
ldi rmp,High(cfstart) ; Startwert in Compare A
out OCR1AH,rmp
ldi rmp,Low(cfstart)
out OCR1AL,rmp
ldi rmp,(1<<WGM11)|(1<<WGM10) ; Kontrollregister A, Fast PWM OCR1A
out TCCR1A,rmp
ldi rmp,(1<<WGM13)|(1<<WGM12)|(1<<CS10) ; Fast PWM OCR1A, Presc = 1
.if debug_TC1Off == 0
out TCCR1B,rmp
.endif
; Timer Interrupts
ldi rmp,(1<<OCIE1A)|(1<<OCIE0) ; TC1 und TC0 Compare-A-Int
out TIMSK,rmp
; Init Sleep und Interrupts
ldi rmp,1<<SE ; Enable sleep
out MCUCR,rmp
sei
;
; ============================================
; P R O G R A M M - S C H L E I F E
; ============================================
;
Loop:
sleep ; Schlafen legen
nop ; Dummy fuer Aufwecken, +1 = 1
sbrc rFlag,bStart ; Start-Flagge gesetzt?, +1/2 = 2/3
rcall Transfer ; ja, arbeite ab, +3 = 5/6
brtc Loop ; ADC-Flagge gesetzt?
rcall AdcCalc ; ja, arbeite ADC-Wert ab
rjmp loop ; Zurueck nach Loop
;
; ==========================================
; U P D A T E - B E H A N D L U N G
; ==========================================
;
; ADC-Flagge ist gesetzt (ADC-Wandlung komplett)
;
AdcCalc:
clt ; Flagge loeschen
.if debug_Second == 1
; Debug: Addiere voreingestellten Wert
ldi rmp,Low(cAdc2) ; Addiere Einstellwert
add rAdcL,rmp
ldi rmp,High(cAdc2)
adc rAdcH,rmp
.else
; Normal: Addiere ADC-Wandler-Wert
in rmp,ADCL ; AD-Wandler-Ergebnis lesen, LSB
add rAdcL,rmp ; zu Ergebnis addieren
in rmp,ADCH ; dto., MSB
adc rAdcH,rmp
.endif
dec rAdc ; Zyklen abwaerts zaehlen
breq AdcCalc1 ; 16 Zyklen fertig
ret ; noch nicht
AdcCalc1:
ldi rAdc,cAdcAdd ; Zyklen neu starten
lsl rAdcL ; 1 mal links schieben
rol rAdcH
lsl rAdcL ; 2 mal links schieben
rol rAdcH
sts sPot,rAdcH ; Ergebnis in SRAM speichern
mov rPot,rAdcH ; Und in Register
clr rAdcL ; Summenspeicher loeschen
clr rAdcH
;
; Ermittle den Frequenz-Sollwert aus dem Mittelwert der
; Potentiometer-Stellung
;
debug_CtcCalc_Start:
; Freqenzbereich aus Schalterstellung
.if debug_CtcCalc == 1
ldi rmp,cCourseTune
ldi rPot,cPot
sts sPot,rPot
.else
.if debug_Second == 1
ldi rmp,cCourseTune2
ldi rPot,cPot2
sts sPot,rPot
.else
clr rmp ; Hole Schalterstellung
sbic pSwIn,bSw3 ; Schalter 3
ori rmp,1 ; nach Bit 0
sbic pSwIn,bSw4 ; Schalter 4
ori rmp,2 ; nach Bit 1
.endif
.endif
sts sCourseTune,rmp ; Speichere in SRAM
clr rAdd ; rAdd fuer R2R-Tabelle auf 1
inc rAdd
ldi rAnd,0xFF ; rAnd fuer R2R-Tabelle auf alles
; Ermittle den Frequenz-Feinbereich
cpi rmp,3 ; Bereich = 3?
brcs SetFineTune ; Feinbereich = 0, 1 oder 2
; Grobbereich = 3, ermittle Feinbereich
cpi rPot,cP2500 ; ab 2.500 Hz?
brcs SetFineTune
lsl rAdd ; Addiere 2 zu R2R-Tabelle
lsr rAnd ; Neustart R2R-Tabelle bei 128
inc rmp ; Feinbereich = 4
cpi rPot,cP5000 ; ab 5.000 Hz?
brcs SetFineTune
lsl rAdd ; Addiere 4 zu R2R-Tabelle
lsr rAnd ; Neustart R2R-Tabelle bei 64
inc rmp ; Feinbereich = 5
cpi rPot,cP10000 ; ab 10.000 Hz?
brcs SetFineTune
lsl rAdd ; Addiere 8 zu R2R-Tabelle
lsr rAnd ; Neustart R2R-Tabelle bei 32
inc rmp ; Feinbereich = 6
SetFineTune:
sts sFineTune,rmp ; Feinbereich in SRAM ablegen
; Berechnung der Sollfrequenz
; Sollfrequenz = Poti*Steilheit+Basiswert
; Steilheit in rN2:rN1:rN0
; Addierer und Ergebnis in rN5:rN4:rN3
lds R0,sCourseTune ; Grobbereich
lsl R0 ; Mal zwei
mov rmp,R0 ; Kopieren
lsl rmp ; Mal vier
add rmp,R0 ; Mal sechs
ldi ZH,High(2*MultTab) ; Z auf Multiplikationstabelle
ldi ZL,Low(2*MultTab)
add ZL,rmp ; Zur Adresse addieren
ldi rmp,0
adc ZH,rmp ; Ueberlauf addieren
lpm rN0,Z+ ; Steilheit, LSB
lpm rN1,Z+ ; dto., MSB
clr rN2 ; Multiplikator HSB leeren
lpm rN3,Z+ ; Addierer in Ergebnis, Byte 1
lpm rN4,Z+ ; dto., Byte 2
lpm rN5,Z+ ; dto., Byte 3
lpm rN6,Z+ ; dto., Byte 4
lds rmp,sPot ; Potentiometerstellung
Multip:
tst rmp ; Multiplikator = Null?
breq MultipEnd
Multip1:
lsr rmp ; naechstes Bit in Carry schieben
brcc Multip2
add rN3,rN0 ; Multiplikator zu Ergebnis addieren
adc rN4,rN1
adc rN5,rN2
Multip2:
lsl rN0 ; Multiplikator mal zwei
rol rN1
rol rN2
rjmp Multip
MultipEnd:
; Soll-Frequenz * 256/65536 ist in rN5:rN4:rN3
; Dividieren der Sollfrequenz / 256 und Runden
ldi rmp,128
cp rN3,rmp
brcs ShiftUp
ldi rmp,1 ; aufrunden
add rN4,rmp
ldi rmp,0
adc rN5,rmp
ShiftUp:
mov rN6,rN5 ; Um ein Register nach oben verschieben
mov rN5,rN4
lds rmp,sFineTune ; Feinbereich lesen
lsl rmp ; Mal zwei
lsl rmp ; Mal vier
ldi ZH,High(2*DivCtcTab) ; CTC-Divisor-Tabelle
ldi ZL,Low(2*DivCtcTab)
add ZL,rmp ; Zu Tabelle addieren
ldi rmp,0
adc ZH,rmp ; Ueberlauf addieren
lpm rN0,Z+ ; Lese Byte 1
lpm rN1,Z+ ; dto., Byte 2
lpm rN2,Z+ ; dto., Byte 3
clr rN3 ; Byte 4 leeren
; Division 32 Bit durch 16 Bit
; Dividend in rN3:rN2:rN1:rN0
; Divisor in rN6:rN5
; Ergebnis in YH:YL = R29:R28
Div32:
clr ZL ; Ergebnis loeschen, LSB
clr ZH ; dto., MSB
ldi rmp,16 ; Anzahl Divisionsschritte
Div32a:
lsl rN0 ; Dividend mal zwei, Byte 1
rol rN1 ; dto., Byte 2
rol rN2 ; dto., Byte 3
rol rN3 ; dto., Byte 4
brcs Div32Sub ; Ueberlauf in 33.tes Bit abfangen
cp rN2,rN5 ; mit Divisor vergleichen, niedriges Byte
cpc rN3,rN6 ; dto., hoeheres Byte und Carry
brcs Div32b ; Null in Ergebnis schieben
Div32Sub:
sub rN2,rN5 ; Divisor abziehen, niedriges Byte
sbc rN3,rN6 ; dto., hoeheres Byte und Carry
sec ; 1 in Ergebnis schieben
rjmp Div32c
Div32b:
clc ; Carry loeschen
Div32c:
rol ZL ; Carry in Ergebnis rotieren
rol ZH
dec rmp ; weiter dividieren?
brne Div32a ; ja
sbiw ZL,1 ; CTC um Eins vermindern
sts sCmpAH,ZH ; in Uebergabespeicher ablegen, MSB
sts sCmpAL,ZL ; dto., LSB
.if debug_CtcCalc == 1
ldi rmp,cCurve
.else
clr rmp ; Kurvenform ermitteln
sbic pSwIn,bSw3 ; Schalter 3 lesen
sbr rmp,1<<0 ; und bei Eins auswerten
sbic pSwIn,bSw4 ; Schalter 4 lesen
sbr rmp,1<<1 ; und bei Eins auswerten
.endif
sts sCurve,rmp ; Kurvenform in SRAM speichern
; Setze Wellenform
ldi ZH,High(2*WaveTabAddr) ; Adressen Wellentabellen
ldi ZL,Low(2*WaveTabAddr)
lds rmp,sCurve
lsl rmp
add ZL,rmp ; Kurvenform addieren
ldi rmp,0
adc ZH,rmp
lpm rmp,Z+ ; Lese Adresse Tabelle, LSB
sts sR2RL,rmp
lpm rmp,Z ; Adresse, MSB
sts sR2RH,rmp
; Setze Wellenparameter
ldi ZH,High(2*FineTab) ; Adresse Feintabelle
ldi ZL,Low(2*FineTab)
lds rmp,sFineTune ; Lese Feinbereich
lsl rmp ; Mal zwei
add ZL,rmp ; Zu Adresse addieren
ldi rmp,0
adc ZH,rmp
lpm rmp,Z+ ; Lese Addierer
sts sR2RAdd,rmp
lpm rmp,Z ; Lese Restartmaske
sts sR2RAnd,rmp
; Kondensatoren ermitteln
lds rmp,sCurve ; Kurvenform lesen
cpi rmp,1 ; Sinus?
ldi rmp,0 ; Kondensatoren aus
brne SetCond ; Bei allen Nicht-Sinus: Aus
lds rmp,sCourseTune ; Grob
lds rN0,sFineTune ; Feinbereich lesen
mov rmp,rN0 ; Kopieren
lsl rmp ; Mal zwei
add rmp,rN0 ; Mal drei
ldi ZH,High(2*CondTab) ; Z auf Kondensatortabelle, MSB
ldi ZL,Low(2*CondTab) ; dto., LSB
add ZL,rmp ; Zu Tabellenadresse addieren, LSB
ldi rmp,0
adc ZH,rmp ; Carry zu MSB
lpm rN0,Z+ ; Lese Potentiometer-Schwelle
lds rmp,sPot ; Lese Potentiometer-Stellung
cp rmp,rN0 ; mit Schwelle vergleichen
lpm rmp,Z+ ; Niedrigeren Kondensator lesen
brcs SetCond ; niedriger als Schwelle
lpm rmp,Z ; Hoeheren Kondensator lesen
SetCond:
sts sCond,rmp ; Kondensatoren schreiben
; Ueberpruefen ob Kurvenform geaendert
lds rN0,sR2RCurrH ; Bisherige Kurvenform
lds rmp,sR2RH ; Neue Kurvenform
cp rmp,rN0 ; Vergleiche
brne SetR2RFlag ; Setze Kurvenform-Ausgabe-Flagge
lds rN0,sR2RCurrAnd ; Bisheriges UND
lds rmp,sR2RAnd ; Neues AND
cp rmp,rN0 ; Vergleiche
brne SetR2RFlag ; Setze Kurvenform-Ausgabe-Flagge
lds rN0,sCmpACurrL ; Low-Byte CTC-Wert bisher
lds rmp,sCmpAL ; Low-Byte CTC-Wert neu
cp rmp,rN0 ; Vergleiche
brne SetCtcFlag ; Ungleich
lds rN0,sCmpACurrH ; High-Byte CTC-Wert bisher
lds rmp,sCmpAH ; High-Byte CTC-Wert neu
cp rmp,rN0 ; Vergleiche
brne SetCtcFlag ; Ungleich
ret ; Keine Flagge setzen
SetR2RFlag:
sbr rFlag,1<<bSetR2R
ret
SetCtcFlag:
sbr rFlag,1<<bSetCtc
ret
;
; Tabelle der Feinbereichseinstellungen
; Erstes Byte: Addierer, 1 = alle Bytes,
; 2, 4, 8: Mehrere Kurven
; Zweites Byte: UND-Wert, 0xFF = alle Bytes,
; 0x7F, 0x3F, 0x1F: Mehrere Kurven
FineTab:
.db 1,0xFF ; Feinbereich 0
.db 1,0xFF ; Feinbereich 1
.db 1,0xFF ; Feinbereich 2
.db 1,0xFF ; Feinbereich 3
.db 2,0x7F ; Feinbereich 4
.db 4,0x3F ; Feinbereich 5
.db 8,0x1F ; Feinbereich 6
;
; Tabelle der Kondensatoren der Feinbereiche
; Byte 1: Von sPot = 0 bis sPot = Byte 1
; Byte 2: Kondensatorwert
; Byte 3: Kondensatorwert oberhalb
CondTab:
.db cPC0,cC0L,cC0H, cPC1,cC1L,cC1H ; Feinbereiche 0 und 1
.db cPC2,cC2L,cC2H, cPC3,cC3L,cC3H ; Feinbereiche 2 und 3
.db cPC46,cC46L,cC46H, cPC46,cC46L,cC46H ; Feinbereiche 4 und 5
.db cPC46,cC46L,cC46H, 0 ; Feinbereich 6
;
; Steilheit und Addierer Sollfrequenzberechnung
; Multiplikationstabelle: y = a * x + b
; Erstes Wort: Gradient a, 16 Bit
; Zweites Wort: Achsenabschnitt b, LWRD
; Drittes Wort: Achsenabschnitt b, HWRD
MultTab:
.dw cM0,LWRD(cA0),HWRD(cA0) ; 2..20 Hz
.dw cM1,LWRD(cA1),HWRD(cA1) ; 20..200 Hz
.dw cM2,LWRD(cA2),HWRD(cA2) ; 200..2000 Hz
.dw cM3,LWRD(cA3),HWRD(cA3) ; 2..20 kHz
;
; Dividenden der Feinbereiche um CTC zu errechnen
; Erstes Wort: LWRD cDiv, Zweites Word HWRD cDiv
DivCtcTab:
.dw LWRD(cDiv01),HWRD(cDiv01) ; 2..20 Hz
.dw LWRD(cDiv01),HWRD(cDiv01) ; 20..200 Hz
.dw LWRD(cDiv23),HWRD(cDiv23) ; 200..2000 Hz
.dw LWRD(cDiv23),HWRD(cDiv23) ; 2..2,5 kHz
.dw LWRD(cDiv4),HWRD(cDiv4) ; 2,5..5 kHz
.dw LWRD(cDiv5),HWRD(cDiv5) ; 5..10 kHz
.dw LWRD(cDiv6),HWRD(cDiv6) ; 10..20 kHz
;
; ==========================================
; S T A R T , N U L L D U R C H G A N G
; ==========================================
;
; Start-Flagge gesetzt, Transfer der neuen Werte
Transfer: ; 5 Takte nach Sleep
cbr rFlag,1<<bStart ; Setze Flagge zurueck, +1 = 6
sbrc rFlag,bSetR2R ; Uebernahmeflagge Welle gesetzt?, +1/2 = 7/8
rcall NewWave ; Neue Wennform einstellen, +3 = 10
sbrc rFlag,bSetCtc ; Uebernahmeflagge CTC-Wert gesetzt?, +
rcall NewCtc ; Neuen CTC-Wert einstellen
ret ; Nein, ignoriere Neustart
;
; Neue Wellenform einstellen
NewWave: ; 10 Takte
; sR2RL: ; 0x0070
; sR2RH: ; 0x0071
; sR2RAdd: ; 0x0072
; sR2RAnd: ; 0x0073
lds ZL,sR2RL ; Lade Wellenform-Adresse, LSB, +2 = 12
lds ZH,sR2RH ; dto., Lade MSB, +2 = 14
lds rAdd,sR2RAdd ; Lade Addierer, +2 = 16
lds rAnd,sR2RAnd ; Lade Wiederholparameter, +2 = 18
;.if debug_Transfer == 0
rcall Convert2Sram ; +3 = 21
; .endif
NewCtc:
; sCmpAH: ; 0x0074
; sCmpAL: ; 0x0075
; sCond: ; 0x0076
; sFineTune: ; 0x0077
; sCourseTune: ; 0x0078
; sPot: ; 0x0079
; sCurve: ; 0x007A
lds rmp,sCmpAH ; Lese neuen CTC-Wert, MSB
out OCR1AH,rmp ; Schreibe neuen Wert, MSB
lds rmp,sCmpAL ; dto., lese LSB
out OCR1AL,rmp ; dto., schreibe LSB
in rmp,pRcfOut ; Lese Kondesator-Port
andi rmp,255-mRcf ; Loesche Kondesatoren-Bits
push R0 ; Sichern R0
mov R0,rmp ; Bits in R0
lds rmp,sCond ; Lese Kondensatoren
andi rmp,mRcf ; Isoliere Kondensatoren-Bits
or rmp,R0 ; Vereine Bits
out pRcfOut,rmp ; Schreibe in Kondensator-Port
pop R0 ; R0 wieder herstellen
Copy2Curr:
; Kopiere neuen Stand ueber bisherigen Stand
ldi XH,High(sCurrStart)
ldi XL,Low(sCurrStart)
ldi ZH,High(sTransfStart)
ldi ZL,Low(sTransfStart)
Copy2Curr1:
ld rmp,Z+ ; Lese neuen Wert
st X+,rmp ; Ueberschreibe bisherigen Wert
cpi ZL,Low(sTransfEnd) ; Ende erreicht?
brne Copy2Curr1 ; Nein, weiter
cbr rFlag,(1<<bSetR2R)|(1<<bSetCtc) ; Flaggen loeschen
; Ermittle den Displaybereich aus Feinbereich und Potistand
; Displaybereiche:
; 0: 2..9,999 Hz, Fein 0 bis 10 Hz, Display = 0
; 1: 10..99,999 Hz, Fein 0 ab 10 Hz, Fein 1 bis 99,999 Hz
; 2: 100..999,999 Hz, Fein 1 ab 100 Hz, Fein 2 bis 999,99 Hz
; 3: 1000..2499,99 Hz, Fein 2 ab 1 kHz, Fein 3
; 4: 2500..4999,9 Hz, Fein 4
; 5: 5000..9999,9 Hz, Fein 5
; 6: 10000..20000 Hz, Fein 6
lds rmp,sFineTuneCurr ; Lese aktuellen Feinbereich
cpi rmp,3 ; Feinbereich ab 3?
brcc SetDisplayTune ; Ab 3 Displaybereich = Feinbereich
cpi rmp,1 ; Feinbereich 1?
brcs DisplayTune0 ; Feinbereich 0
breq DisplayTune1 ; Feinbereich 1
lds rmp,sPotCurr ; Feinbereich=2, Lese Potistand
cpi rmp,cP1000 ; Poti kleiner 1000?
ldi rmp,2 ; Displaybereich = 2
brcs SetDisplayTune
inc rmp ; Displaybereich = 3
rjmp SetDisplayTune
DisplayTune0: ; Feinbereich = 0
lds rmp,sPotCurr ; Lese Potistand
cpi rmp,cP10 ; Pruefe 10 Hz-Schwelle
ldi rmp,0 ; Displaybereich 0, kleiner 10 Hz Schwelle
brcs SetDisplayTune ; Displaybereich = 0
inc rmp ; Displaybereich = 1
rjmp SetDisplayTune
DisplayTune1: ; Feinbereich = 1
lds rmp,sPotCurr ; Lese Potistand
cpi rmp,cP100 ; 100 Hz Schwelle
ldi rmp,1 ; Displaybereich = 1
brcs SetDisplayTune
inc rmp ; Displaybereich = 2
SetDisplayTune:
sts sDisplayTune,rmp
; Lade Dividend des Displaybereichs
ldi ZH,High(2*DivTab) ; 32-Bit-Dividendentabelle in Z, MSB
ldi ZL,Low(2*DivTab) ; dto., LSB
lds rmp,sDisplayTune ; Lese Displaybereich
lsl rmp ; Mal zwei
lsl rmp ; Mal vier
add ZL,rmp ; Zur Tabellenadresse addieren
ldi rmp,0
adc ZH,rmp ; Ueberlauf addieren
lpm rN0,Z+ ; Lese Dividend, Byte 1
lpm rN1,Z+ ; dto., Byte 2
lpm rN2,Z+ ; dto., Byte 3
lpm rN3,Z+ ; dto., Byte 4
; Division
Div24:
clr rN4 ; Ergebnis loeschen, Byte 1
clr rN5 ; dto., Byte 2
clr rN6 ; dto., Byte 3
lds ZL,sCmpACurrL ; Lese Low-Byte CTC nach ZL
lds ZH,sCmpACurrH ; dto., High-Byte nach ZH
adiw ZL,1 ; erhoehe um 1
ldi rmp,17 ; 17 Bits dividieren
clc
Div24a:
brcs Div24b ; Eins ist herausgeschoben, subtrahiere unmittelbar
cp rN2,ZL ; vergleiche Low-Byte
cpc rN3,ZH ; dito, High-Byte
brcs Div24c ; Nicht subtrahieren
Div24b:
sub rN2,ZL ; Low-Byte abziehen
sbc rN3,ZH ; dito, High-Byte
sec ; Carry-Flagge setzen
rjmp Div24d
Div24c:
clc ; Carry loeschen
Div24d:
rol rN4 ; Carry in Ergebnis schieben
rol rN5
rol rN6
lsl rN0 ; Dividend links schieben
rol rN1
rol rN2
rol rN3
dec rmp ; Zaehler um 1 vermindern
brne Div24a ; weiter dividieren
cp rN2,ZL
cpc rN3,ZH
brcs DecimalConversion ; nicht Aufrunden
inc rN4 ; Aufrunden
brne DecimalConversion
inc rN5 ; Aufrunden
brne DecimalConversion
inc rN6
; Divisionsergebnis ist in rN6:rN5:rN4
DecimalConversion:
ldi XH,High(sLcdBuf) ; X zeigt auf Puffer
ldi XL,Low(sLcdBuf)
lds rmp,sCurve
st X+,rmp ; Modezeichen in LCD-Ausgabepuffer
ldi rmp,' '
DecConv1:
st X+,rmp ; Leerzeichen in den Puffer
cpi XL,Low(sLcdBuf+8)
brne DecConv1
ldi XH,High(sLcdBuf+2) ; Auf Anfang Ausgabe
ldi XL,Low(sLcdBuf+2)
clr rN3 ; Fuehrende Nullen unterdruecken
inc rN3 ; auf Eins
ldi ZH,High(2*Dec6Tab) ; Dezimaltabelle in Z
ldi ZL,Low(2*Dec6Tab)
DecConv2:
lpm rN0,Z+ ; Lese Dezimal-Digit-Binaerzahl
lpm rN1,Z+
lpm rN2,Z+
tst rN0 ; Ende der Tabelle?
breq DecConv6
ldi rmp,0xFF ; Ergebnis in rmp
DecConv3:
inc rmp ; Naechstes Ergebnis
sub rN4,rN0 ; Dezimalzahl abziehen, Byte 1
sbc rN5,rN1 ; dto., Byte 2
sbc rN6,rN2 ; dto., Byte 3
brcc DecConv3 ; Weiter abziehen
add rN4,rN0 ; Letzte Subtraktion rueckgaengig, Byte 1
adc rN5,rN1 ; dto., Byte 2
adc rN6,rN2 ; dto., Byte 3
tst rmp ; Ergebnis Null?
breq DecConv5 ; Ist Null
clr rN3 ; Fuehrende-Nullen-Flagge loeschen
DecConv4:
; Nicht unterdruecken
subi rmp,-'0' ; Digit in ASCII-Ziffer wandeln
st X+,rmp
rjmp DecConv2
DecConv5:
tst rN3 ; Fuehrende Nullen unterdruecken?
breq DecConv4 ; Nein, als '0' ausgeben
ldi rmp,' ' ; Fuehrende Null unterdruecken
st X+,rmp ; in LCD-Puffer
rjmp DecConv2 ; Weiter wandeln
DecConv6:
ldi rmp,'0'
add rmp,rN4 ; Letzte Ziffer
st X,rmp ; in LCD-Puffer
; Setze Dezimalpunkt
ldi ZH,High(2*DecPtPos) ; Dezimalpunkt-Positionen
ldi ZL,Low(2*DecPtPos)
lds rmp,sDisplayTune ; Lade Anzeigebereich
add ZL,rmp ; Addiere
lpm rN0,Z ; Lese Dezimalpunkt-Position
tst rN0 ; Position = 0?
breq Thousand
.if LangEn == 1
ldi rmp,'.'
.else
ldi rmp,','
.endif
rcall Insert
Thousand:
adiw ZL,8 ; Lese Tausendertrennung
lpm rN0,Z
tst rN0 ; Position = 0?
breq DisplayBuffer ; Puffer ausgeben
.if LangEn == 1
ldi rmp,','
.else
ldi rmp,'.'
.endif
rcall Insert
DisplayBuffer:
clr ZH ; Position auf LCD einstellen
clr ZL
.if debug_NoLcd == 0
rcall LcdPos
.endif
ldi ZH,High(sLcdBuf) ; Zeige auf LCD-Puffer-Anfang
ldi ZL,Low(sLcdBuf)
ldi rmp,8 ; Acht Zeichen
.if debug_NoLcd == 0
rcall LcdSram ; Puffer ausgeben
.endif
ret
;
; Insert: Fuege Zeichen in rmp in den Puffer
; an der Position rN0 ein
Insert:
push ZH ; Sichern Z
push ZL
ldi ZH,High(sLcdBuf+1) ; An den Anfang des Zahlenbereichs
ldi ZL,Low(sLcdBuf+1)
Insert1:
ldd rN1,Z+1 ; Lese naechstes Zeichen im Puffer
st Z+,rN1 ; Schreibe Zeichen
dec rN0 ; Anzahl Verschiebungen vermindern
brne Insert1 ; weiter
st Z,rmp ; Setze Dezimal- oder Tausenderpunkt
pop ZL
pop ZH
ret
;
; Divisortabelle
DivTab:
.dw LWRD(cND0),HWRD(cND0)
.dw LWRD(cND1),HWRD(cND1)
.dw LWRD(cND2),HWRD(cND2)
.dw LWRD(cND3),HWRD(cND3)
.dw LWRD(cND4),HWRD(cND4)
.dw LWRD(cND5),HWRD(cND5)
.dw LWRD(cND6),HWRD(cND6)
;
; Dezimaltabelle 6 Ziffern = 24 Bit
Dec6Tab:
.db Byte1(100000),Byte2(100000)
.db Byte3(100000),Byte1(10000)
.db Byte2(10000),Byte3(10000)
.db Byte1(1000),Byte2(1000)
.db Byte3(1000),Byte1(100)
.db Byte2(100),Byte3(100)
.db Byte1(10),Byte2(10)
.db Byte3(10),0
.db 0,0
;
; Dezimalpuunkt-Positionen der Displaybereiche
; Displ. Anzeige Dezimal 1000er
; 0 M 9,9999 +2 +0
; 1 M 99,999 +3 +0
; 2 M 999,99 +4 +0
; 3 M9.999,9 +5 +1
; 4 M9.999,9 +5 +1
; 5 M9.999,9 +5 +1
; 6 M 19.999 +0 +3
DecPtPos:
.db 2,3,4,5,5,5,0,0
.db 0,0,0,1,1,1,3,0 ; +8 Bytes Versatz
;
; =================================
; S R A M - P U F F E R
; =================================
;
; SRAM-Puffer mit Tabellenwerten befuellen
; Eingang: Z zeigt auf Tabellenanfang
; rAdd enthaelt Addierer fuer R/2R-Tabellenwerte
; 1=8-Bit, 2=7-Bit, 4=6-Bit, 8=5-Bit
; rAnd enthaelt Und-Register fuer R/2R-Neustart
; 8 Bit = 0; 7 Bit = 127, 6 Bit = 63, 5 Bit = 31
; Benutzt: rCnt = R7 ; Zaehler fuer Anzahl R/2R-Tabelle
; rCntRst: Byte-Zahler fuer Neustart R/2R-Tabelle
;
Convert2Sram: ; 21 Takte
push XH ; Sichern X, MSB, +2 = 23
push XL ; dto., LSB, +2 = 25
ldi XH,High(sR2R) ; X zeigt auf SRAM-Puffer, MSB, +1 = 26
ldi XL,Low(sR2R) ; dto., LSB, +1 = 27
clr rCnt ; Byte-Zaehler auf Null, +1 = 28
push ZH ; Z sichern, MSB, +2 = 30
push ZL ; dto., LSB, +2 = 32
Convert2Sram1:
lpm rmp,Z ; Lese Wert aus Sinustabelle, +3 = 35
st X+,rmp ; Kopiere in SRAM-Puffer, +2 = 37
add ZL,rAdd ; Addieren Tabellenwert
ldi rmp,0 ; Ueberlauf addieren
adc ZH,rmp ; zu MSB
inc rCnt ; Anzahl Bytes zaehlen
breq Convert2SramEnd ; Null, fertig
mov rmp,rCnt ; Kopiere Byte-Zaehler
and rmp,rAnd ; Isoliere Bits
brne Convert2Sram1
pop ZL ; Anfangsadresse setzen, LSB
pop ZH ; dto., MSB
push ZH ; Z wieder sichern, MSB
push ZL ; dto., LSB
rjmp Convert2Sram1
Convert2SramEnd:
pop ZL
pop ZH
pop XL
pop XH
ret
; LCD-Routinen
;
; LCD-Codezeichen fuer Kurvenform-Darstellung
; Tabelle der Codezeichen
Charcodes:
.db 64,0,0,1,3,5,9,17,1,0 ; Z = 0, Saegezahn
.db 72,0,0,8,20,20,21,5,2,0 ; Z = 1, Sinus
.db 80,0,0,4,4,10,10,17,17,0 ; Z = 2, Dreieck
.db 88,0,0,14,10,10,10,10,27,0 ; Z = 3, Rechteck
.db 0,0 ; Ende der Tabelle
;
.include "lcd.inc" ; Import der LCD-Routinen
;
LcdInitText:
.db "SigGenV1",0xFE,0xFE ; Textausgabe bei Init
;
; Tabellen laden
.include "wavetab.inc"
;
; Tabellenanfangsadressen
; 00 = Saegezahn, 01 = Sinus, 10 = Dreieck, 11 = Rechteck
WaveTabAddr:
.dw 2*SawTooth
.dw 2*Sine
.dw 2*Triangle
.dw 2*Rectangle
;
; Ende Quellcode
; Copyright
.db "(C)2018 by Gerhard Schmidt " ; menschenlesbar
.db "C(2)18 0ybG reahdrS hcimtd " ; wortgerecht
;
9.2 Tabelle der Wellenformen
Und hier die zugehörige Wellentabelle als Include-Datei (als
Assembler-Include-Datei hier).
Die Tabellen werden in das SRAM kopiert, wo der CTC-Interrupt
die Werte Byte-für-Byte abholt und in den DAC-Port
schreibt.
Ab 2,5 kHz wird beim Kopieren ins SRAM nur jeder zweite
Wert der Tabelle kopiert, dafür wird beim 128.ten Byte
wieder von vorn begonnen. Dadurch wird eine Verdopplung der
Frequenz erreicht (hier als 7-Bit-Ansteuerung bezeichnet).
Ab 5 kHz wird nur jeder vierte Wert der Tabelle kopiert,
ab 10 kHz jeder achte.
Alle Tabellen sind auf eine Amplitude von 3 Vpp
ausgelegt, um den Eingang des FET-Operationsverstärkers
nicht zu übersteuern. Da der CA3140 schon ab unter
0,0 V Eingangsspannung korrekt arbeitet, wurde keine
Untergrenze addiert (wie dies bei einem 741-Opamp nötig
gewesen wäre).
;
; Tabellen fuer Kurvenverlauf
;
; Sinus, 270 Grad phasenverschoben
;
Sine: ; Sinustabelle 8 bits, 3V max
.db 0,0,0,0,0,1,1,1 ; n=0 - 7
.db 1,2,2,3,3,4,4,5 ; n=8 - 15
.db 6,7,7,8,9,10,11,12 ; n=16 - 23
.db 13,14,15,16,17,19,20,21 ; n=24 - 31
.db 22,24,25,27,28,29,31,32 ; n=32 - 39
.db 34,36,37,39,40,42,44,45 ; n=40 - 47
.db 47,49,51,53,54,56,58,60 ; n=48 - 55
.db 62,63,65,67,69,71,73,75 ; n=56 - 63
.db 77,78,80,82,84,86,88,90 ; n=64 - 71
.db 91,93,95,97,99,100,102,104 ; n=72 - 79
.db 106,108,109,111,113,114,116,117 ; n=80 - 87
.db 119,121,122,124,125,126,128,129 ; n=88 - 95
.db 131,132,133,134,136,137,138,139 ; n=96 - 103
.db 140,141,142,143,144,145,146,146 ; n=104 - 111
.db 147,148,149,149,150,150,151,151 ; n=112 - 119
.db 152,152,152,152,153,153,153,153 ; n=120 - 127
.db 153,153,153,153,153,152,152,152 ; n=128 - 135
.db 152,151,151,150,150,149,149,148 ; n=136 - 143
.db 147,146,146,145,144,143,142,141 ; n=144 - 151
.db 140,139,138,137,136,134,133,132 ; n=152 - 159
.db 131,129,128,126,125,124,122,121 ; n=160 - 167
.db 119,117,116,114,113,111,109,108 ; n=168 - 175
.db 106,104,102,100,99,97,95,93 ; n=176 - 183
.db 91,90,88,86,84,82,80,78 ; n=184 - 191
.db 76,75,73,71,69,67,65,63 ; n=192 - 199
.db 62,60,58,56,54,53,51,49 ; n=200 - 207
.db 47,45,44,42,40,39,37,36 ; n=208 - 215
.db 34,32,31,29,28,27,25,24 ; n=216 - 223
.db 22,21,20,19,17,16,15,14 ; n=224 - 231
.db 13,12,11,10,9,8,7,7 ; n=232 - 239
.db 6,5,4,4,3,3,2,2 ; n=240 - 247
.db 1,1,1,1,0,0,0,0 ; n=248 - 255
;
; Dreiecktabelle
;
Triangle: ; Dreiecktabelle 8 bits, 3V max
.db 0,1,2,4,5,6,7,8 ; n=0 - 7
.db 10,11,12,13,14,16,17,18 ; n=8 - 15
.db 19,20,22,23,24,25,26,27 ; n=16 - 23
.db 29,30,31,32,33,35,36,37 ; n=24 - 31
.db 38,39,41,42,43,44,45,47 ; n=32 - 39
.db 48,49,50,51,53,54,55,56 ; n=40 - 47
.db 57,59,60,61,62,63,65,66 ; n=48 - 55
.db 67,68,69,71,72,73,74,75 ; n=56 - 63
.db 77,78,79,80,81,82,84,85 ; n=64 - 71
.db 86,87,88,90,91,92,93,94 ; n=72 - 79
.db 96,97,98,99,100,102,103,104 ; n=80 - 87
.db 105,106,108,109,110,111,112,114 ; n=88 - 95
.db 115,116,117,118,120,121,122,123 ; n=96 - 103
.db 124,126,127,128,129,130,131,133 ; n=104 - 111
.db 134,135,136,137,139,140,141,142 ; n=112 - 119
.db 143,145,146,147,148,149,151,152 ; n=120 - 127
.db 153,152,151,149,148,147,146,145 ; n=128 - 135
.db 143,142,141,140,139,137,136,135 ; n=136 - 143
.db 134,133,131,130,129,128,127,126 ; n=144 - 151
.db 124,123,122,121,120,118,117,116 ; n=152 - 159
.db 115,114,112,111,110,109,108,106 ; n=160 - 167
.db 105,104,103,102,100,99,98,97 ; n=168 - 175
.db 96,94,93,92,91,90,88,87 ; n=176 - 183
.db 86,85,84,82,81,80,79,78 ; n=184 - 191
.db 77,75,74,73,72,71,69,68 ; n=192 - 199
.db 67,66,65,63,62,61,60,59 ; n=200 - 207
.db 57,56,55,54,53,51,50,49 ; n=208 - 215
.db 48,47,45,44,43,42,41,39 ; n=216 - 223
.db 38,37,36,35,33,32,31,30 ; n=224 - 231
.db 29,27,26,25,24,23,22,20 ; n=232 - 239
.db 19,18,17,16,14,13,12,11 ; n=240 - 247
.db 10,8,7,6,5,4,2,1 ; n=248 - 255
;
; Saegezahn-Tabelle
;
Sawtooth: ; Saegezahntabelle 8 bits, 3V max
.db 0,1,1,2,2,3,4,4 ; n=0 - 7
.db 5,5,6,7,7,8,8,9 ; n=8 - 15
.db 10,10,11,11,12,13,13,14 ; n=16 - 23
.db 14,15,16,16,17,17,18,19 ; n=24 - 31
.db 19,20,20,21,22,22,23,23 ; n=32 - 39
.db 24,25,25,26,26,27,28,28 ; n=40 - 47
.db 29,29,30,31,31,32,32,33 ; n=48 - 55
.db 34,34,35,35,36,37,37,38 ; n=56 - 63
.db 38,39,40,40,41,41,42,43 ; n=64 - 71
.db 43,44,44,45,46,46,47,47 ; n=72 - 79
.db 48,49,49,50,50,51,52,52 ; n=80 - 87
.db 53,53,54,55,55,56,56,57 ; n=88 - 95
.db 58,58,59,59,60,61,61,62 ; n=96 - 103
.db 62,63,64,64,65,65,66,67 ; n=104 - 111
.db 67,68,68,69,70,70,71,71 ; n=112 - 119
.db 72,73,73,74,74,75,76,76 ; n=120 - 127
.db 77,77,78,79,79,80,80,81 ; n=128 - 135
.db 82,82,83,83,84,85,85,86 ; n=136 - 143
.db 86,87,88,88,89,89,90,91 ; n=144 - 151
.db 91,92,92,93,94,94,95,95 ; n=152 - 159
.db 96,97,97,98,98,99,100,100 ; n=160 - 167
.db 101,101,102,103,103,104,104,105 ; n=168 - 175
.db 106,106,107,107,108,109,109,110 ; n=176 - 183
.db 110,111,112,112,113,113,114,115 ; n=184 - 191
.db 115,116,116,117,118,118,119,119 ; n=192 - 199
.db 120,121,121,122,122,123,124,124 ; n=200 - 207
.db 125,125,126,127,127,128,128,129 ; n=208 - 215
.db 130,130,131,131,132,133,133,134 ; n=216 - 223
.db 134,135,136,136,137,137,138,139 ; n=224 - 231
.db 139,140,140,141,142,142,143,143 ; n=232 - 239
.db 144,145,145,146,146,147,148,148 ; n=240 - 247
.db 149,149,150,151,151,152,152,153 ; n=248 - 255
;
; Rechteck-Tabelle
;
Rectangle: ; Rechtecktabelle 8 bits, 3V max
.db 0,0,0,0,0,0,0,0 ; n=0 - 7
.db 0,0,0,0,0,0,0,0 ; n=8 - 15
.db 0,0,0,0,0,0,0,0 ; n=16 - 23
.db 0,0,0,0,0,0,0,0 ; n=24 - 31
.db 0,0,0,0,0,0,0,0 ; n=32 - 39
.db 0,0,0,0,0,0,0,0 ; n=40 - 47
.db 0,0,0,0,0,0,0,0 ; n=48 - 55
.db 0,0,0,0,0,0,0,0 ; n=56 - 63
.db 0,0,0,0,0,0,0,0 ; n=64 - 71
.db 0,0,0,0,0,0,0,0 ; n=72 - 79
.db 0,0,0,0,0,0,0,0 ; n=80 - 87
.db 0,0,0,0,0,0,0,0 ; n=88 - 95
.db 0,0,0,0,0,0,0,0 ; n=96 - 103
.db 0,0,0,0,0,0,0,0 ; n=104 - 111
.db 0,0,0,0,0,0,0,0 ; n=112 - 119
.db 0,0,0,0,0,0,0,0 ; n=120 - 127
.db 153,153,153,153,153,153,153,153 ; n=128 - 135
.db 153,153,153,153,153,153,153,153 ; n=136 - 143
.db 153,153,153,153,153,153,153,153 ; n=144 - 151
.db 153,153,153,153,153,153,153,153 ; n=152 - 159
.db 153,153,153,153,153,153,153,153 ; n=160 - 167
.db 153,153,153,153,153,153,153,153 ; n=168 - 175
.db 153,153,153,153,153,153,153,153 ; n=176 - 183
.db 153,153,153,153,153,153,153,153 ; n=184 - 191
.db 153,153,153,153,153,153,153,153 ; n=192 - 199
.db 153,153,153,153,153,153,153,153 ; n=200 - 207
.db 153,153,153,153,153,153,153,153 ; n=208 - 215
.db 153,153,153,153,153,153,153,153 ; n=216 - 223
.db 153,153,153,153,153,153,153,153 ; n=224 - 231
.db 153,153,153,153,153,153,153,153 ; n=232 - 239
.db 153,153,153,153,153,153,153,153 ; n=240 - 247
.db 153,153,153,153,153,153,153,153 ; n=248 - 255
;
; Ende Tabellen
;
Die Tabellen belegen 512 Speicherworte oder 1/16tel des
verfügbaren Flash-Speichers.
9.3 Die LCD-Include-Routine
Dies ist die angepasste Include-Routine für die Ansteuerung
der LCD im 8-Bit-Busy-Modus. Sie ist
hier näher beschrieben und
hier als Assembler-Quellcode
downloadbar.
;
; *********************************
; * LCD include routines *
; * (C)2018 avr-asm-tutorial.net *
; *********************************
;
; ***********************************************
; * L C D I N T E R F A C E R O U T I N E N *
; ***********************************************
;
; +-------+----------------+--------------+
; |Routine|Funktion |Parameter |
; +-------+----------------+--------------+
; |LcdInit|Initiiert die |Ports, Pins |
; | |LCD im einge- | |
; | |stellten Modus | |
; +-------+----------------+--------------+
; |LcdText|Gibt den Text im|Z=2*Tabellen- |
; | |Flash ab Zeile 1| adresse |
; | |aus |0x0D: Naechste|
; | | | Zeile |
; | | |0xFF: Ignorie-|
; | | | re |
; | | |0xFE: Textende|
; +-------+----------------+--------------+
; |LcdSram|Gibt den Text im|Z=SRAM-Adresse|
; | |SRAM aus |R16: Anzahl |
; +-------+----------------+--------------+
; |LcdChar|Gibt ein Zeichen|R16: Zeichen |
; | |aus | |
; +-------+----------------+--------------+
; |LcdCtrl|Gibt Kontrollbe-|R16: Befehl |
; | |fehl aus | |
; +-------+----------------+--------------+
; |LcdPos |Setzt Ausgabe- |ZH: Zeile 0123|
; | |position |ZL: Spalte 0..|
; +-------+----------------+--------------+
; |LcdSpec|Erzeugt Spezial-|Z: 2*Tabellen-|
; | |zeichen | adresse |
; +-------+----------------+--------------+
; | S C H A L T E R L C D D E C I M A L |
; +-------+----------------+--------------+
; |LcdDec2|Gibt zwei Dezi- |R16: Binaer- |
; | |malstellen aus | zahl |
; +-------+----------------+--------------+
; |LcdDec3|Gibt drei Dezi- |R16: Binaer- |
; | |malstellen aus | zahl |
; +-------+----------------+--------------+
; |LcdDec5|Gibt fuenf Dezi-|Z: Binaerzahl |
; | |malstellen aus | 16 Bit |
; +-------+----------------+--------------+
; | S C H A L T E R L C D H E X |
; +-------+----------------+--------------+
; |LcdHex2|Gibt zwei Hexa- |R16: Binaer- |
; | |dezimalen aus | zahl |
; +-------+----------------+--------------+
; |LcdHex4|Gibt vier Hexa- |Z: Binaerzahl |
; | |dezimalen aus | 16 Bit |
; +-------+----------------+--------------+
;
; ***********************************
; P A R A M E T E R - V O R L A G E
; ***********************************
;
; Standard-Parameter-Satz der Einstellungen
;.equ clock = 1000000 ; Taktfrequenz Prozessor in Hz
; LCD-Groesse:
;.equ LcdLines = 1 ; Anzahl Zeilen (1, 2, 4)
;.equ LcdCols = 8 ; Anzahl Zeichen pro Zeile (8..24)
; LCD-Ansteuerung
;.equ LcdBits = 4 ; Busbreite (4 oder 8)
; Wenn 4-Bit-Ansteuerung:
;.equ Lcd4High = 1 ; Busnibble (1=Oberes, 0=Unteres)
;.equ LcdWait = 0 ; Ansteuerung (0 mit Busy, 1 mit Warteschleifen)
; LCD-Datenports
;.equ pLcdDO = PORTA ; Daten-Ausgabe-Port
;.equ pLcdDD = DDRA ; Daten-Richtungs-Port
; LCD-Kontrollports und -pins
;.equ pLcdCEO = PORTB ; Control E Ausgabe-Port
;.equ bLcdCEO = PORTB0 ; Controll E Ausgabe-Portpin
;.equ pLcdCED = DDRB ; Control E Richtungs-Port
;.equ bLcdCED = DDB0 ; Control E Richtungs-Portpin
;.equ pLcdCRSO = PORTB ; Control RS Ausgabe-Port
;.equ bLcdCRSO = PORTB1 ; Controll RS Ausgabe-Portpin
;.equ pLcdCRSD = DDRB ; Control RS Richtungs-Port
;.equ bLcdCRSD = DDB1 ; Control RS Richtungs-Portpin
; Wenn LcdWait = 0:
;.equ pLcdDI = PINA ; Daten-Input-Port
;.equ pLcdCRWO = PORTB ; Control RW Ausgabe-Port
;.equ bLcdCRWO = PORTB2 ; Control RW Ausgabe-Portpin
;.equ pLcdCRWD = DDRB ; Control RW Richtungs-Port
;.equ bLcdCRWD = DDB2 ; Control RW Richtungs-Portpin
; Wenn Dezimalausgabe benoetigt wird:
;.equ LcdDecimal = 1 ; Wenn definiert: einbinden
; Wenn Hexadezimalausgabe benoetigt wird:
;.equ LcdHex = 1 ; Wenn definiert: einbinden
; Wenn nur Simulation im SRAM:
;.equ avr_sim = 1 ; 1=Simulieren, 0=Nicht simulieren
;
; *****************************************
; T E X T A U S G A B E - V O R L A G E N
; *****************************************
;
; Tabellen zum Kopieren fuer verschiedene Formate
;
; --------------------------
; Einzeilige LCD
; 8 Zeichen pro Zeile
; Text_1_8:
; .db " ",0xFE,0xFF
; 01234567
;
; 16 Zeichen pro Zeile
; Text_1_16:
; .db " ",0xFE,0xFF
; 0123456789012345
;
; 20 Zeichen pro Zeile
; Text_1_20:
; .db " ",0xFE,0xFF
; 01234567890123456789
;
; 24 Zeichen pro Zeile
; Text_1_24:
; .db " ",0xFE,0xFF
; 012345678901234567890123
;
; --------------------------
; Zweizeilige LCD
; 16 Zeichen pro Zeile
; Text_2_16:
; .db " ",0x0D,0xFF
; .db " ",0xFE,0xFF
; 0123456789012345
;
; 20 Zeichen pro Zeile
; Text_2_20:
; .db " ",0x0D,0xFF
; .db " ",0xFE,0xFF
; 01234567890123456789
;
; 24 Zeichen pro Zeile
; Text_2_24:
; .db " ",0x0D,0xFF
; .db " ",0xFE,0xFF
; 012345678901234567890123
;
; --------------------------
; Vierzeilige LCD
; 16 Zeichen pro Zeile
; Text_4_16:
; .db " ",0x0D,0xFF
; .db " ",0x0D,0xFF
; .db " ",0x0D,0xFF
; .db " ",0xFE,0xFF
; 0123456789012345
;
; 20 Zeichen pro Zeile
; Text_4_20:
; .db " ",0x0D,0xFF
; .db " ",0x0D,0xFF
; .db " ",0x0D,0xFF
; .db " ",0xFE,0xFF
; 01234567890123456789
;
; 24 Zeichen pro Zeile
; Text_4_24:
; .db " ",0x0D,0xFF
; .db " ",0x0D,0xFF
; .db " ",0x0D,0xFF
; .db " ",0xFE,0xFF
; 012345678901234567890123
;
; *********************************
; P A R A M E T E R C H E C K
; *********************************
;
; Sind alle Parameter korrekt angegeben?
;
; Groesse definiert?
.ifndef LcdLines
.error "LCD line size (LcdLines) undefined!"
.else
.if (LcdLines!=1)&&(LcdLines!=2)&&(LcdLines!=4)
.error "LCD illegal line size (LcdLines)!"
.endif
.endif
.ifndef LcdCols
.error "LCD column size (LcdCols) undefined!"
else
.if (LcdCols<8)||(LcdCols>24)
.error "LCD illegal column size (LcdCols)!"
.endif
.endif
;
; clock definiert?
.ifndef clock
.error "Clock frequency (clock) undefined!"
.endif
;
; 4- oder 8-Bit-Interface gewaehlt?
.ifndef LcdBits
.error "LCD data bus bits (LcdBits) undefined!"
.else
.if (LcdBits != 4) && (LcdBits != 8)
.error "LCD data bus bits (LcdBits) not 4 or 8!"
.endif
.if LcdBits == 4
.ifndef Lcd4High
.error "LCD 4 bit data bus nibble (Lcd4High) undefined!"
.else
.if (Lcd4High != 0) && (Lcd4High != 1)
.error "LCD 4 bit data bus nibble (Lcd4High) not 0 or 1!"
.endif
.endif
.endif
.endif
;
; LCD data ports
.ifndef pLcdDO
.error "LCD data output port (pLcdDO) undefined!"
.endif
.ifndef pLcdDD
.error "LCD data direction port (pLcdDD) undefined!"
.endif
.if LcdWait == 0
.ifndef pLcdDI
.error "LCD data input port (pLcdDI) undefined!"
.endif
.endif
;
; LCD Control Ports und Pins
.ifndef pLcdCEO
.error "LCD control E output port (pLcdCEO) undefined!"
.endif
.ifndef pLcdCED
.error "LCD control E direction port (pLcdCED) undefined!"
.endif
.ifndef bLcdCEO
.error "LCD control E output pin (bLcdCEO) undefined!"
.endif
.ifndef bLcdCED
.error "LCD control E direction pin (bLcdCED) undefined!"
.endif
.ifndef pLcdCRSO
.error "LCD control RS output port (pLcdCRSO) undefined!"
.endif
.ifndef pLcdCRSD
.error "LCD control RS direction port (pLcdCRSD) undefined!"
.endif
.ifndef bLcdCRSO
.error "LCD control RS output pin (bLcdCRSO) undefined!"
.endif
.ifndef bLcdCRSD
.error "LCD control RS direction pin (bLcdCRSD) undefined!"
.endif
.ifndef LcdWait
.error "LCD operating property (LcdWait) undefined!"
.else
.if LcdWait == 0
.ifndef pLcdCRWO
.error "LCD control RW output port (pLcdCRWO) undefined!"
.endif
.ifndef bLcdCRWO
.error "LCD control RW output pin (bLcdCRWO) undefined!"
.endif
.ifndef pLcdCRWD
.error "LCD control RW direction port (pLcdCRWD) undefined!"
.endif
.ifndef bLcdCRWD
.error "LCD control RW direction pin (bLcdCRWD) undefined!"
.endif
.endif
.endif
;
; *************************************
; S I M U L A T I O N A V R _ S I M
; *************************************
;
; Simulation ermitteln
.ifdef avr_sim
.equ simulation = avr_sim
.else
.equ simulation = 0
.endif
.if simulation == 1
.dseg
SimStart:
SimDisplayPos:
.byte 1
SimCtrlClear:
.byte 1
SimCtrlReset:
.byte 1
SimCtrlInputmode:
.byte 1
SimCtrlDisplay:
.byte 1
SimCtrlCursorShift:
.byte 1
SimCtrlFunctionset:
.byte 1
SimCtrlCharGenRamAdr:
.byte 1
SimCtrlDisplayRamAdr:
.byte 1
SimDataDisplay:
.byte LcdLines*LcdCols
SimEnd:
.cseg
.endif
;
; *********************************
; L C D - R O U T I N E N
; *********************************
;
; LcdInit: Ports und Pins initiieren
; Einschalt-Wartezyklus
; Funktionseinstellungen
; LCD loeschen
LcdInit:
; Init der LCD Kontroll-Bits
cbi pLcdCEO,bLcdCEO ; E-Pin low
sbi pLcdCED,bLcdCED ; E-Pin output
cbi pLcdCRSO,bLcdCRSO ; RS-Pin low
sbi pLcdCRSD,bLcdCRSD ; RS-Pin output
.ifdef pLcdCRWO
.ifdef bLcdCRWO
cbi pLcdCRWO,bLcdCRWO ; RW-Pin low
.endif
.endif
.ifdef pLcdCRWD
.ifdef pLcdCRWD
sbi pLcdCRWD,bLcdCRWD ; RW-Pin output
.endif
.endif
; Init der LCD Datenbus-Ports
.if LcdBits == 8
clr R16 ; Datenbus auf Null
.else
in R16,pLcdDO ; Lese Outputbits Datenbus
.if Lcd4High == 1
andi R16,0x0F ; Oberes Nibble loeschen
.else
andi R16,0xF0 ; Unteres Nibble loeschen
.endif
.endif
out pLcdDO,R16 ; Datenbus Output auf Null
.if LcdBits == 8
ldi R16,0xFF ; Setze alle Richtungsbits
.else
in R16,pLcdDD ; Lese Richtungsbits Datenbus
.if Lcd4High == 1
ori R16,0xF0 ; Oberes Nibble high
.else
ori R16,0x0F ; Unteres Nibble high
.endif
.endif
out pLcdDD,R16 ; Richtungsbits Datenbus setzen
; LCD-Startphase
.if simulation == 0
rcall LcdWait50ms
.endif
; LCD auf 8-Bit-Datenbusbreite einstellen
ldi R16,0x30
rcall Lcd8Ctrl ; im 8-Bit-Modus an LCD-Kontroll
.if simulation == 0
rcall LcdWait5ms ; 5 ms lang warten
.endif
ldi R16,0x30
rcall Lcd8Ctrl ; im 8-Bit-Modus an LCD-Kontroll
.if simulation == 0
rcall LcdWait5ms ; 5 ms lang warten
.endif
ldi R16,0x30
rcall Lcd8Ctrl ; im 8-Bit-Modus an LCD-Kontroll
.if simulation == 0
rcall LcdWait5ms ; 5 ms lang warten
.endif
ldi R16,0x30
rcall Lcd8Ctrl ; im 8-Bit-Modus an LCD-Kontroll
.if simulation == 0
rcall LcdWait5ms ; 5 ms lang warten
.endif
; Wenn 4-Bit-Interface: auf 4-Bit-Datenbus umschalten
.if LcdBits == 4
ldi R16,0x20 ; 4-Bit-Interface
rcall Lcd8Ctrl
.if simulation == 0
rcall LcdWait5ms
.endif
.endif
; Function set
.if LcdBits == 8
ldi R16,0x30 ; 8-Bit-Datenbus
.else
ldi R16,0x20 ; 4-Bit-Datenbus
.endif
.if LcdLines > 1
ori R16,0x08 ; Mehrzeiliges Display
.endif
rcall LcdCtrl
; Display mode
ldi R16,0x0C ; Display an, Unterstrich aus, Cursorblink aus
rcall LcdCtrl
; LCD Entry Mode setzen
ldi R16,0x06 ; Cursor nach rechts, Cursor shift
rcall LcdCtrl
; LCD loeschen
ldi R16,0x01 ; Display loeschen
rjmp LcdCtrl
;
; LcdText
; Gibt den Text im Flash auf der LCD aus
; Z zeigt auf Texttabelle
; 0x0D: Zeilenvorschub und Wagenruecklauf
; 0xFF: Ignoriere (Fuellzeichen)
; 0xFE: Ende des Texts
LcdText:
push R0 ; R0 sichern
ldi R16,LcdLines ; Max Zeilenanzahl
mov R0,R16 ; in R0
LcdText1:
lpm R16,Z+ ; Lese naechstes Zeichen
cpi R16,0xFE ; Endezeichen?
breq LcdText3
brcc LcdText1 ; Ueberlese Fuellzeichen
cpi R16,0x0D ; Zeilenvorschub+Wagenruecklauf
brne LcdText2
dec R0 ; Zeilenzaehler vermindern
breq LcdText1
.if simulation == 1
push ZH
push ZL
push R16
ldi ZH,High(SimDataDisplay) ; Wagenruecklauf und Zeilenvorschub
ldi ZL,Low(SimDataDisplay)
ldi R16,LcdLines
LcdText1a:
adiw ZL,LcdCols
dec R16
cp R16,R0
brne LcdText1a
ldi R16,Low(SimDataDisplay)
sub ZL,R16
ldi R16,High(SimDataDisplay)
sbc ZH,R16
sts SimDisplayPos,ZL
pop R16
pop ZL
pop ZH
.endif
push ZH ; Z sichern
push ZL
ldi ZH,LcdLines ; Zeile berechnen
sub ZH,R0
clr ZL ; Zeilenanfang
rcall LcdPos
pop ZL ; Z wieder herstellen
pop ZH
rjmp LcdText1 ; nachstes Zeichen
LcdText2:
rcall LcdChar ; Zeichen in R16 an LCD ausgeben
rjmp LcdText1 ; Naechstes Zeichen
LcdText3:
pop R0 ; R0 wieder herstellen
ret
;
; LcdSRam gibt Text im SRAM an der aktuellen LCD-Position aus
; Z zeigt auf SRAM-Adresse
; R16: Anzahl Zeichen
LcdSRam:
push R16 ; Rette R16
ld R16,Z+ ; Lese Zeichen
rcall LcdChar ; Zeichen in R16 an LCD ausgeben
pop R16 ; R16 wieder herstellen
dec R16 ; Anzahl Zeichen abwaerts
brne LcdSRam ; weiter Zeichen ausgeben
ret
;
; LcdChar gib Zeichen in R16 an der aktuellen Position aus
; R16: Zeichen
LcdChar:
.if simulation == 1
push ZH
push ZL
push R16
ldi ZH,High(SimDataDisplay)
ldi ZL,Low(SimDataDisplay)
lds R16,SimDisplayPos
inc R16
sts SimDisplayPos,R16
dec R16
add ZL,R16
ldi R16,0
adc ZH,R16
pop R16
st Z,R16 ; Ausgabe in SRAM schreiben
pop ZL
pop ZH
.endif
rjmp LcdData
;
; LcdByte gibt Kontrollbefehl in R16 an die LCD aus
LcdCtrl:
.if simulation == 1
cpi R16,0x80 ; Display RAM-Adresse?
brcs LcdCtrlSim1
sts SimCtrlDisplayRamAdr,R16
push ZH
push ZL
mov ZH,R16
andi R16,0x40
brne LcdCtrlSim0a
clr ZL ; Zeile 1 oder 3
rjmp LcdCtrlSim0b
LcdCtrlSim0a:
ldi ZL,LcdCols ; Zeile 2 oder 4
LcdCtrlSim0b:
mov R16,ZH
andi R16,0x3F
subi R16,LcdCols
brcc LcdCtrlSim0c ; Zeilen 3 oder 4
subi R16,-LcdCols
rjmp LcdCtrlSim0d
LcdCtrlSim0c:
sbrc ZH,6 ; Zeile 4?
subi R16,-LcdCols ; Spaltenlaenge addieren
LcdCtrlSim0d:
add ZL,R16
sts SimDisplayPos,ZL
pop ZL
pop ZH
rjmp LcdCtrlSim9
LcdCtrlSim1:
cpi R16,0x40 ; Character Generator RAM-Adresse?
brcs LcdCtrlSim2
sts SimCtrlCharGenRamAdr, R16
rjmp LcdCtrlSim9
LcdCtrlSim2:
cpi R16,0x20 ; Functionset?
brcs LcdCtrlSim3
sts SimCtrlFunctionSet, R16
rjmp LcdCtrlSim9
LcdCtrlSim3:
cpi R16,0x10 ; Cursor shift
brcs LcdCtrlSim4
sts SimCtrlCursorShift, R16
rjmp LcdCtrlSim9
LcdCtrlSim4:
cpi R16,0x08 ; Display control?
brcs LcdCtrlSim5
sts SimCtrlDisplay, R16
rjmp LcdCtrlSim9
LcdCtrlSim5:
cpi R16,0x04 ; Display control?
brcs LcdCtrlSim6
sts SimCtrlInputmode, R16
rjmp LcdCtrlSim9
LcdCtrlSim6:
cpi R16,0x02 ; Reset?
brcs LcdCtrlSim7
sts SimCtrlReset,R16
rjmp LcdCtrlSim9
LcdCtrlSim7:
cpi R16,0x01 ; Clear?
brcs LcdCtrlSim8
sts SimCtrlClear,R16
; LCD clear display
push ZH
push ZL
ldi ZH,High(SimEnd)
ldi ZL,Low(SimEnd)
clr R16
LcdCtrl7a:
st -Z,R16 ; Rueckwaerts mit Nullen fuellen
cpi ZL,Low(SimStart)
brne LcdCtrl7a
cpi ZH,High(SimStart)
brne LcdCtrl7a
ldi R16,0x01
pop ZL
pop ZH
rjmp LcdCtrlSim9
LcdCtrlSim8: ; 00-Befehl
LcdCtrlSim9:
.endif
.if LcdWait == 0
rcall LcdBusy
.endif
cbi pLcdCRSO,bLcdCRSO ; RS-Bit low
.if LcdBits == 4
push ZL
push R16
in ZL,pLcdDO ; Daten Output Port lesen
.if Lcd4High == 1
andi ZL,0x0F ; Oberes Nibble loeschen
andi R16,0xF0 ; Oberes Nibble Eingabezahl loeschen
.else
andi ZL,0xF0 ; Unteres Nibble Output loeschen
swap R16 ; Oberes und unteres Nibble vertauschen
andi R16,0x0F ; Oberes Nibble loeschen
.endif
or R16,ZL ; Kombinieren
out pLcdDO,R16
rcall LcdPulseE ; E aktivieren
pop R16 ; R16 wieder herstellen
push R16 ; und sichern
in ZL,pLcdDO ; Daten Output Port lesen
.if Lcd4High == 1
andi ZL,0x0F ; Unteres Nibble erhalten
swap R16 ; Unteres und oberes Nibble vertauschen
andi R16,0xF0
.else
andi ZL,0xF0 ; Oberes Nibble erhalten
andi R16,0x0F ; Unteres Nibble erhalten
.endif
out pLcdDO,R16 ; Auf Datenbus
rcall LcdPulseE ; E aktivieren
pop R16 ; Eingabezahl wieder herstellen
pop ZL ; ZL wieder herstellen
.else
out pLcdDO,R16 ; Eingabezahl auf Datenbus
rcall LcdPulseE ; E aktivieren
.endif
.if LcdWait == 1
andi R16,0xFC ; Obere Bits Kontrollbefehl
brne LcdCtrl1 ; Kurze Verzoegerung
rjmp LcdWait1640us ; 1,64 ms warten
LcdCtrl1:
rjmp LcdWait40us ; 40 us warten
.else
ret
.endif
;
; LcdPos setzt den LCD Cursor an die Position in Z
; ZH: Zeile (0 bis Anzahl Zeilen - 1)
; ZL: Spalte (0 bis Anzahl Spalten - 1)
LcdPos:
cpi ZH,1 ; Zeile = 1?
ldi R16,0x80 ; Zeile 0
.if LcdLines < 2 ; LCD hat nur eine Zeile
rjmp LcdPos1
.endif
brcs LcdPos1
ldi R16,0xC0 ; Zeile 2 bei zweizeiliger LCD
.if LcdLines == 2
rjmp LcdPos1
.endif
breq LcdPos1
ldi R16,0x80+LcdCols ; Zeile 3
cpi ZH,2 ; Zeile = 3
breq LcdPos1
ldi R16,0xC0+LcdCols ; Zeile 4
LcdPos1:
add R16,ZL ; Spaltenadresse addieren
rjmp LcdCtrl
;
; LcdSpec erzeugt Spezialzeichen auf der LCD
; Z zeigt auf 2*Tabellenadresse
; Tabellenformat:
; 1.Byte: Adresse des Zeichens 0b01zzz000,
; 0: Ende der Tabelle
; 2.Byte: Dummy
; 3. bis 10.Byte: Daten der Zeilen 1 bis 8
LcdSpec:
push R0 ; R0 ist Zaehler
LcdSpec1:
lpm R16,Z+ ; Lese Adresse des Zeichens
tst R16 ; Ende der Tabelle?
breq LcdSpec3 ; Ende Tabelle
rcall LcdCtrl ; Adresse schreiben
adiw ZL,1 ; Ueberlese Dummy
ldi R16,8 ; 8 Byte pro Zeichen
mov R0,R16 ; R1 ist Zaehler
LcdSpec2:
lpm R16,Z+ ; Datenbyte lesen
rcall LcdData ; Datenbyte ausgeben
dec R0 ; Abwaerts zaehlen
brne LcdSpec2 ; Weiter Daten ausgeben
rjmp LcdSpec1 ; Naechstes Zeichen
LcdSpec3:
pop R0 ; R0 wieder herstellen
ldi ZH,0 ; Cursor Home
ldi ZL,0
rjmp LcdPos
;
; **********************************
; D E Z I M A L A U S G E B E N
; **********************************
; Routinen zur Ausgabe von Dezimalzahlen
;
.ifdef LcdDecimal
;
; LcdDec2 gibt Binaerzahl in R16 dezimal mit zwei Stellen aus
; ohne Unterdrueckung fuehrender Nullen
LcdDec2:
mov ZL,R16 ; Zahl kopieren
ldi R16,'0'-1 ; Zehnerzaehler
cpi ZL,100 ; Zweistellig?
brcs LcdDec2a
ldi ZL,99 ; dreistellig, auf Maximalwert setzen
LcdDec2a:
inc R16
subi ZL,10
brcc LcdDec2a
rcall LcdData ; Zehner ausgeben
ldi R16,10+'0'
add R16,ZL
rcall LcdData
ret
;
; LcdDec3 gibt Binaerzahl in R16 dezimal mit drei Stellen aus
; mit Unterdrueckung fuehrender Nullen
LcdDec3:
ldi ZH,1 ; Fuehrende Nullen unterdruecken
LcdDec3null: ; behalte fuehrende Nullen-Unterdrueckung bei
push R0
mov ZL,R16 ; Zahl in ZL
ldi R16,100 ; Hunderter
rcall LcdDec3a
rcall LcdData
ldi R16,10 ; Zehner
rcall LcdDec3a
rcall LcdData
ldi R16,'0'
add R16,ZL
rcall LcdData
pop R0
ret
;
LcdDec3a: ; Dezimalziffer ermitteln
clr R0 ; Zaehler
dec R0 ; auf - 1
LcdDec3b:
inc R0 ; Erhoehe Zaehler
sub ZL,R16 ; Subtrahiere 100 oder 10
brcc LcdDec3b ; Kein Ueberlauf: weiter
add ZL,R16 ; Letzte Subtraktion rueckgaengig
tst R0 ; Ziffer Null?
breq LcdDec3c ; Ja, pruefe Nullunterdrueckung
clr ZH ; Keine Nullenunterdrueckung mehr
ldi R16,'0' ; Ziffer in ASCII
add R16,R0
ret
LcdDec3c:
tst ZH ; Nullenunterdrueckung?
breq LcdDec3d
ldi R16,' '
ret
LcdDec3d:
ldi R16,'0'
ret
;
; LcdDec5 gibt Zahl in Z mit fuenf Dezimalstellen aus
; mit Unterdrueckung fuehrender Nullen
LcdDec5:
push R2
push R1
push R0
clr R2 ; Fuehrende Nullen
inc R2 ; unterdruecken
mov R1,ZH ; Kopiere Zahl nach R1:R0
mov R0,ZL
ldi ZH,High(10000) ; 1. Dezimalstelle
ldi ZL,Low(10000)
rcall LcdDec5a ; Konvertiere 1. Dezimalstelle
ldi ZH,High(1000) ; 2.Dezimalstelle
ldi ZL,Low(1000)
rcall LcdDec5a ; Konvertiere 2. Dezimalstelle
ldi ZH,High(100) ; 2.Dezimalstelle
ldi ZL,Low(100)
rcall LcdDec5a ; Konvertiere 3. Dezimalstelle
ldi ZH,High(10) ; 2.Dezimalstelle
ldi ZL,Low(10)
rcall LcdDec5a ; Konvertiere 4. Dezimalstelle
ldi R16,'0'
add R16,R0
rcall LcdChar ; Zeige 5. Dezimalstelle an
pop R0
pop R1
pop R2
ret ; Gib R16 als Dezimal mit drei Stellen aus
;
LcdDec5a: ; Ermittle Dezimalstelle
ldi R16,'0'-1
LcdDec5b:
inc R16 ; Erhoehe Zaehler
sub R0,ZL ; Subtrahiere Dezimalstelle LSB
sbc R1,ZH ; und MSB mit Carry
brcc LcdDec5b
add R0,ZL ; Letzte Subtraction rueckgaengig
adc R1,ZH
cpi R16,'0' ; Nullunterdrueckung?
breq LcdDec5c ; Ja, pruefe
clr R2 ; Nullunterdrueckung ausschalten
rjmp LcdChar
LcdDec5c:
tst R2 ; Nullunterdrueckung aus?
breq LcdDec5d ; Ja
ldi R16,' ' ; Null unterdruecken
LcdDec5d:
rjmp LcdChar
;
.endif
;
; ****************************************
; H E X A D E Z I M A L A U S G E B E N
; ****************************************
; Routinen zur Ausgabe von Dezimalzahlen
.ifdef LcdHex
;
; LcdHex2 gibt Zahl in R16 in Hexadezimalformat
; auf der LCD aus
LcdHex2:
push R16 ; Wird noch gebraucht
swap R16 ; Oberes Nibble in unteres Nibble
rcall LcdNibble
pop R16
LcdNibble:
andi R16,0x0F ; Unteres Nibble erhalten
subi R16,-'0' ; ASCII-Null addieren
cpi R16,'9'+1 ; A bis F?
brcs LcdNibble1 ; Nein
subi R16,-7 ; 7 addieren
LcdNibble1:
rjmp LcdChar ; Zeichen in R16 ausgeben
;
; LcdHex4
LcdHex4:
mov R16,ZH ; Oberes Byte in R16
rcall LcdHex2
mov R16,ZL ; Unteres Byte in R16
rjmp LcdHex2
;
.endif
;
; *********************************
; L C D - A N S T E U E R U N G
; *********************************
;
; Warte bis LCD Busy-Flagge geloescht
; wird nur benoetigt wenn Warten aus
.if LcdWait == 0
LcdBusy:
push R16 ; Inhalt von R16 sichern
.if LcdBits == 8
clr R16 ; Datenbus-Richtung auf Input
.else
in R16,pLcdDD ; Lese Richtungs-Bits
.if Lcd4High == 1
andi R16,0x0F ; Loesche oberes Nibble
.else
andi R16,0xF0 ; Loesche unteres Nibble
.endif
.endif
out pLcdDD,R16 ; Richtungsregister setzen
.if LcdBits == 8
clr R16 ; Alle Ausgabepins low
.else
in R16,pLcdDO ; Lese Ausgabeport
.if Lcd4High == 1
andi R16,0x0F ; Loesche oberes Nibble
.else
andi R16,0xF0 ; Loesche unteres Nibble
.endif
.endif
out pLcdDO,R16 ; Loesche Pull-Ups
cbi pLcdCRSO,bLcdCRSO ; RS-Pin auf Low
sbi pLcdCRWO,bLcdCRWO ; RW-Pin auf High
LcdBusyWarte:
rcall LcdIn ; Aktiviere E, lese Datenport, deaktiviere E
.if LcdBits == 4
rcall LcdPulseE ; Dummy fuer lower nibble
.endif
lsl R16 ; Busy-Flag in Carry schieben
brcs LcdBusyWarte ; Flagge 1, weiter warten
cbi pLcdCRWO,bLcdCRWO ; RW-Pin auf Low
.if LcdBits == 8
ldi R16,0xFF ; Datenbus wieder auf Ausgang
.else
in R16,pLcdDD ; Lese Richtung Datenport
.if Lcd4High == 1
ori R16,0xF0 ; Oberes Nibble High
.else
ori R16,0x0F ; Unteres Nibble High
.endif
.endif
out pLcdDD,R16 ; Richtungsport setzen
pop R16 ; R16 wieder herstellen
ret ; Fertig
; Lese Busy-Flagge in R16
; wird nur benoetigt, wenn Warten ausgeschaltet
LcdIn:
cbi pLcdCRSO,bLcdCRSO ; LCD-RS-Pin auf Low
sbi pLcdCEO,bLcdCEO ; Setze Bit bLcdCEO im LCD-Kontrollport
nop ; Warte einen Takt
.if clock>1000000
nop
.endif
.if clock>2000000
nop
.endif
.if clock>3000000
nop
.endif
.if clock>3000000
nop
.endif
.if clock>4000000
nop
.endif
.if clock>5000000
nop
.endif
.if clock>6000000
nop
.endif
.if clock>7000000
nop
.endif
.if clock>8000000
nop
.endif
.if clock>9000000
nop
.endif
.if clock>10000000
nop
.endif
.if clock>11000000
nop
.endif
.if clock>12000000
nop
.endif
.if clock>13000000
nop
.endif
.if clock>14000000
nop
.endif
.if clock>15000000
nop
.endif
.if clock>16000000
nop
.endif
.if clock>17000000
nop
.endif
.if clock>18000000
nop
.endif
.if clock>19000000
nop
.endif
in R16,pLcdDI ; Lese Datenbus
cbi pLcdCEO,bLcdCEO ; Loesche Bit bLcdCEO
ret
.endif
;
; 1 us Impuls am LCD-E-Pin ausgeben
LcdPulseE:
sbi pLcdCEO,bLcdCEO ; Setze Bit bLcdCEO im LCD-Kontrollport
.if clock>1000000
nop
.endif
.if clock>2000000
nop
.endif
.if clock>3000000
nop
.endif
.if clock>3000000
nop
.endif
.if clock>4000000
nop
.endif
.if clock>5000000
nop
.endif
.if clock>6000000
nop
.endif
.if clock>7000000
nop
.endif
.if clock>8000000
nop
.endif
.if clock>9000000
nop
.endif
.if clock>10000000
nop
.endif
.if clock>11000000
nop
.endif
.if clock>12000000
nop
.endif
.if clock>13000000
nop
.endif
.if clock>14000000
nop
.endif
.if clock>15000000
nop
.endif
.if clock>16000000
nop
.endif
.if clock>17000000
nop
.endif
.if clock>18000000
nop
.endif
.if clock>19000000
nop
.endif
cbi pLcdCEO,bLcdCEO ; Loesche Bit bLcdCEO
ret
;
; R16 im 8-Bit-Modus an LCD-Kontrolle ausgeben
Lcd8Ctrl:
.if simulation == 1
push R16
clr R16
sts SimDisplayPos,R16
pop R16
.endif
.if LcdBits == 4
push ZL ; ZL sichern
in ZL,pLcdDO ; Daten Output Port lesen
.if Lcd4High == 1
andi ZL,0x0F ; Oberes Nibble Output loeschen
andi R16,0xF0 ; Unteres Nibble Eingabezahl loeschen
.else
andi ZL,0xF0 ; Unteres Nibble Output loeschen
swap R16 ; Oberes und unteres Nibble vertauschen
andi R16,0x0F ; Unteres Nibble Daten isolieren
.endif
or R16,ZL ; Kombinieren
pop ZL
.endif
out pLcdDO,R16 ; Daten auf LCD Output Port
cbi pLcdCRSO,bLcdCRSO ; RS-Bit low
rjmp LcdPulseE ; E-Puls ausloesen
;
; LcdData: R16 im eingestellten Modus Datenausgabe an LCD
; R16: Zeichen
LcdData:
.if LcdWait == 0
rcall LcdBusy ; Warte auf Busy-Flag der LCD
.endif
sbi pLcdCRSO,bLcdCRSO ; Setze RS-Bit
.if LcdBits == 4
push ZL
push R16
in ZL,pLcdDO ; Daten Output Port lesen
.if Lcd4High == 1
andi ZL,0x0F ; Oberes Nibble loeschen
andi R16,0xF0 ; Unteres Nibble Eingabezahl loeschen
.else
andi ZL,0xF0 ; Unteres Nibble Output loeschen
swap R16 ; Oberes und unteres Nibble vertauschen
andi R16,0x0F ; Oberes Nibble loeschen
.endif
or R16,ZL ; Kombinieren
out pLcdDO,R16
rcall LcdPulseE ; E aktivieren
pop R16 ; R16 wieder herstellen
push R16 ; und wieder sichern
in ZL,pLcdDO ; Daten Output Port lesen
.if Lcd4High == 1
andi ZL,0x0F ; Unteres Nibble Output erhalten
swap R16 ; Unteres zum oberen Nibble machen
andi R16,0xF0 ; Oberes Nibble Eingabezahl erhalten
.else
andi ZL,0xF0 ; Oberes Nibble Output erhalten
andi R16,0x0F ; Unteres Nibble Eingabezahl erhalten
.endif
or R16,ZL ; Kombinieren
out pLcdDO,R16 ; Auf Datenbus
rcall LcdPulseE ; E aktivieren
pop R16 ; Eingabezahl wieder herstellen
pop ZL ; ZL wieder herstellen
.else
out pLcdDO,R16 ; Eingabezahl auf Datenbus
rcall LcdPulseE ; E aktivieren
.endif
.if LcdWait == 1
rjmp LcdWait40us
.endif
ret
;
; ********************************
; W A R T E R O U T I N E N
; ********************************
;
; 50 ms lang warten
; Da bei Taktfrequenzen oberhalb von 3,1 MHz die
; maximale Dauer der Z-Schleife ueberschritten
; wuerde, wird 10 mal die 5ms-Routine aufgerufen
LcdWait50ms:
push R16 ; 10 mal 5 ms aufrufen
ldi R16,10
LcdWait50ms1:
rcall LcdWait5ms
dec R16
brne LcdWait50ms1
pop R16
ret
;
LcdWait5ms:
; 5 ms warten, RCALL 3 Takte
.equ cLcdZ5ms = (5*clock/1000 - 18 + 2) / 4
push ZH ; Rette ZH, + 2 Takte
push ZL ; Rette ZL, + 2 Takte
ldi ZH,High(cLcdZ5ms) ; + 1 Takt
ldi ZL,Low(cLcdZ5ms) ; + 1 Takt
rjmp LcdWaitZ ; + 2 Takte
; Insgesamt: 11 Takte
;
.if LcdWait == 1 ; mit Wait-Zyklen
; 1,64 ms warten
.equ cLcdZ1640us = (164*(clock/1000)/100 - 18 + 2) / 4
LcdWait1640us:
push ZH ; Rette ZH
push ZL ; Rette ZL
ldi ZH,High(cLcdZ1640us)
ldi ZL,Low(cLcdZ1640us)
rjmp LcdWaitZ
;
; 40 us warten
.equ cLcdZ40us = (40*clock/1000000 - 18 + 2) / 4
LcdWait40us:
push ZH ; Rette ZH
push ZL ; Rette ZL
ldi ZH,High(cLcdZ40us)
ldi ZL,Low(cLcdZ40us)
rjmp LcdWaitZ
.endif
;
; Warteroutine mit Z Zyklen
LcdWaitZ: ; 11 Takte
sbiw ZL,1 ; Abwaerts zaehlen, 2 Takte
brne LcdWaitZ ; Nicht Null, weiter, 2 Takte bei Sprung, 1 Takt am Ende
pop ZL ; ZL wieder herstellen, 2 Takte
pop ZH ; ZH wieder herstellen, 2 Takte
ret ; Zurueck, 4 Takte
; Insgesamt: 11 + 4 * (Z - 1) + 3 + 8
;
; Anzahl Takte = 11 + 4 * (Z - 1) + 3 + 8
; 11: RCALL, PUSH, LDI, LDI, RJMP
; 4: 4 Takte pro Z-Zyklus (minus 1)
; 3: 3 Takte fuer letzten Zyklus
; 8: POP, RET
; = 4 * Z + 11 - 4 + 3 + 8
; = 4 * Z + 18
; Z = (Anzahl Takte - 18 + 2) / 4
; +2: Aufrunden vor Teilen durch 4
;
; End of include file
Lob, Tadel, Fehlermeldungen, Genöle und Geschimpfe oder Spam bitte über
das Kommentarformular an mich.
Zum Anfang dieser Seite
©2017 by http://www.avr-asm-tutorial.net