Pfad:
Home ==>
Mikrobeginner ==> 9. Tongenerator
This page in English (external):
Lektion 9: Tongenerator mit AD-Wandler, Tontabellen, Multiplikation
Der OC0A-Ausgang dient hier als variabler Tonerzeuger mit Poti-Einstellung der
Tonhöhe. Außerdem werden hier 8-Bit-Zahlen miteinander multipliziert und
Musikstücke abgespielt.
9.0 Übersicht
- Einführung in die Tonerzeugung
- Hardware, Bauteile, Aufbau
- Tonhöhenregelung
- Einführung in die Tabellenprogrammierung
- Einführung in die Multiplikation
- Tonleiterausgabe
- Musikstück
Das Erzeugen von Tönen kennen wir im Grunde schon, weil es nichts anderes als eine
blinkende LED ist, nur mit höheren Frequenzen im Niederfrequenzbereich (NF) bis
20 kHz (für Fledermäuse bis 40 kHz). Wir lernen daher hier nichts
Neues über Timer, nur über den Anschluss von Lautsprechern,
das Multiplizieren und den Umgang mit
Tabellen.
9.2.1 Die Schaltung
Zur Tonausgabe wird ein Lautsprecher benötigt, der an OC0A angeschlossen wird. Der Elko
mit 47 µF entkoppelt den Gleichstrom und lässt nur die Pegelwechsel an OC0A
durch.
Taster und Potentiometer sind wie gehabt angeschlossen.
9.2.2 Der Lautsprecher
Das ist der
Lautsprecher. Er hat 45 Ω Impedanz, damit die Lautstärke ordentlich hoch ist.
Die beiden Anschlüsse kriegen einen Kabelanschluss mit Pins angelötet, die in das
Breadboard passen. Die Polarität ist für unsere Zwecke egal, da sie nur akustische
Folgen hat.
9.2.3 Der Elko
Das hier ist ein Elko. Der Minuspol ist auf dem Gehäuse gekennzeichnet, der
Pluspol-Anschlussdraht ist der längere.
9.2.4 Der Aufbau
Der Anschluss des Lautsprechers erfolgt an Pin 5 über den Elko.
Damit kann es mit den Tönen losgehen.
9.3.1 Einfache Aufgabe 1
Bei der Aufgabe 1 sollen Töne auf dem Lautsprecher ausgegeben werden, deren
Tonhöhe mit dem Poti geregelt werden kann. Töne zwischen 300 Hz
und 75 kHz ("Fledermausschreck") sollen erzeugt werden. Der Ton
soll nur ausgegeben werden, wenn die Taste gedrückt ist.
9.3.2 Lösung
9.3.2.1 Frequenzbereiche
Es ist klar, dass hier der CTC-Modus des Timers verwendet werden muss. Der Ausgang muss, wenn
der Ton gehört werden soll, torkeln (von Null auf Eins und zurück auf Null). Da
für jede Schwingung zwei CTC-Durchläufe nötig sind, ist die erzeugte Frequenz
halb so groß. Der Frequenzbereich bei verschiedenen Takten und Vorteilern
überstreicht folgende Bereiche:
Takt | Vor- teiler | OCR0A =0 | OCR0A =255 |
9,6 MHz | 1 | 4,8 MHz | 18,75 kHz |
8 | 600 kHz | 2,34 kHz |
64 | 75 kHz | 292,5 Hz |
256 | 18,75 kHz | 73,1 Hz |
1024 | 4,69 kHz | 18,3 Hz |
1,2 MHz | 1 | 600 kHz | 2,34 kHz |
8 | 75 kHz | 292,5 Hz |
64 | 9,38 kHz | 36,6 Hz |
256 | 2,35 kHz | 9,15 Hz |
1024 | 586 Hz | 2,29 Hz |
Der hörbare Bereich lässt sich bei 1,2 MHz Takt mit einem Vorteiler von 8 gut
überdecken.
9.3.2.2 AD-Werte und OCR0A-Werte
Je höher die Spannung am Poti ist, desto höher soll der Ton sein. Da der OCR0A-Wert
sich umgekehrt verhält (je höher der OCR0A-Wert desto niedriger die Frequenz), muss
entweder das Potentiometer umgekehrt angeschlossen werden oder eine Umkehr der gemessenen
Werte erfolgen. Eine Softwarelösung dafür wäre, den gemessenen Wert von 0xFF
abzuziehen. Das ginge mit
ldi Register1,0xFF
sub Register1,Register2 ; Register2 = Messwert
mov Register2,Register1
Das Umkehren aller Bits in einem Register kann der Prozessor aber von sich aus schon (siehe
Quelltext und die Erläuterung darunter), wir können uns also diese
"Von-Hinten-durch-die-Brust-ins-Auge-Lösung" sparen.
9.3.3 Programm
Das hier ist das Programm. Den Quellcode gibt es hier.
;
; ********************************************
; * Tonerzeugung mit Taster und Regelung *
; * (C)2016 by http://www.gsc-elektronic.net *
; ********************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; --------- Programmablauf -----------------
;
; Die Spannung am Poti-Eingang wird mit dem
; ADC staendig eingelesen und in das Ver-
; gleichsregister des Timers TC0 geschrieben.
; Mit dem PCINT am Tasteneingang wird fest-
; gestellt, ob die Taste gedrueckt ist. Wenn
; ja wird der Timer auf Toggle eingestellt,
; wenn nicht wird OC0A auf Clear eingestellt.
;
; --------- Register -------------------------
; frei: R0 .. R14
.def rSreg = R15 ; Sichern Statusregister
.def rmp = R16 ; Vielzweckregister
.def rimp = R17 ; Vielzweckregister Interrupts
; frei: R18 .. R31
;
; --------- Ports ----------------------------
.equ pOut = PORTB ; Ausgabeport
.equ pDir = DDRB ; Richtungsport
.equ pInp = PINB ; Eingangsport
.equ bLspD = DDB0 ; Lautsprecherausgang
.equ bTasO = PORTB3 ; Pullup Tasteneingang
.equ bTasI = PINB3 ; Tasteninputpin
.equ bAdID = ADC2D ; ADC-Input-Disable
;
; --------- Timing ---------------------------
; Takt = 1200000 Hz
; Vorteiler = 8
; CTC-TOP-Bereich = 0 .. 255
; CTC-Teiler-Bereich = 1 .. 256
; Toggle-Teiler = 2
; Frequenzbereich: 75 kHz .. 293 Hz
;
; --------- Reset- und Interruptvektoren -----
.CSEG ; Assemblieren ins Code-Segment
.ORG 0 ; Start bei Null
rjmp Start ; Reset Vektor, Sprung zur Initiierung
reti ; INT0-Int, nicht aktiv
rjmp PcIntIsr ; PCINT-Int, aktiv
reti ; TIM0_OVF, nicht aktiv
reti ; EE_RDY-Int, nicht aktiv
reti ; ANA_COMP-Int, nicht aktiv
reti ; TIM0_COMPA-Int, nicht aktiv
reti ; TIM0_COMPB-Int, nicht aktiv
reti ; WDT-Int, nicht aktiv
rjmp AdcIsr ; ADC-Int, aktiv
;
; ---------- Interrupt Service Routinen -----
;
; PCINT Interrupt
; Wird von Tastenereignissen ausgeloest.
; Falls Taste gedrueckt, wird der Timer
; auf Torkeln gestellt, wenn nicht wird
; der Timer auf Clear gestellt.
;
PcIntIsr: ; PCINT-Interrupt Tasten-Interrupt
sbic pInp,bTasI ; Ueberspringe bei Taste = 0
rjmp PcIntIsrAus ; Taste ist nicht gedrueckt
ldi rimp,(1<<COM0A0)|(1<<WGM01) ; Toggle, CTC-A
out TCCR0A,rimp ; in Kontrollregister A
rjmp PcIntIsrRet ; zurueck
PcIntIsrAus:
ldi rimp,(1<<COM0A1)|(1<<WGM01) ; Clear, CTC-A
out TCCR0A,rimp
PcIntIsrRet:
reti
;
; ADC Ready Interrupt
;
; Wird vom AD-Wandler bei abgeschlossener
; Wandlung ausgeloest.
; Liest die obersten 8 Bit des AD-Wandlers
; und schreibt es in das Vergleichregister
; A des Timers. Die naechste Wandlung wird
; angestossen.
;
AdcIsr: ; ADC-Interrupt
in rSreg,SREG ; sichern Statusregister
in rimp,ADCH ; lese MSB Ergebnis
com rimp ; Wert umkehren
out OCR0A,rimp ; in CTC-TOP-Register
ldi rimp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rimp ; in ADC-Kontrollregister A
out SREG,rSreg ; wiederherstellen Statusregister
reti
;
; ---------- Programmstart und Init ----------
Start:
; Stapel einrichten
ldi rmp,LOW(RAMEND) ; SRAM-Ende
out SPL,rmp ; in Stackzeiger
; In- und Output-Ports
ldi rmp,1<<bLspD ; Lautsprecherausgang Richtung
out pDir,rmp ; in Richtungsregister
ldi rmp,1<<bTasO ; Pullup am Tastenport
out pOut,rmp ; in Ausgangsregister
; Timer als CTC konfigurieren
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear, CTC-A
out TCCR0A,rmp ; in Kontrollregister A
ldi rmp,1<<CS01 ; Vorteiler = 8, Timer starten
out TCCR0B,rmp ; in Kontrollregister B
; AD-Wandler konfigurieren und starten
ldi rmp,(1<<ADLAR)|(1<<MUX1) ; Linksjustieren, ADC2
out ADMUX,rmp ; in ADC-MUX
ldi rmp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rmp ; in Kontrollregister A, starten
; PCINT fuer Tasteneingang
ldi rmp,1<<PCINT3 ; PB3-Int ermoeglichen
out PCMSK,rmp ; in PCINT-Maskenregister
ldi rmp,1<<PCIE ; PCINT ermoeglichen
out GIMSK,rmp ; in Interrupt-Maskenregister
; Schlafen ermoeglichen
ldi rmp,1<<SE ; Schlafen, Idle-Modus
out MCUCR,rmp ; in MCU-Kontrollregister
; Interrupts einschalten
sei
; ------------ Hauptprogramm-Schleife --------
Schleife:
sleep ; schlafen legen
nop ; Aufwachen
rjmp Schleife ; wieder schlafen legen
;
; Ende Quellcode
;
An neuen Instruktionen gibt es hier:
Damit haben wir eine tonhöhen-regulierbare Morsetaste gebastelt.
9.3.4 Simulation des Programmes
Die Simulation erfolgt mit avr_sim
wie folgt.
Das sind die Einstellungen nach der Initphase. Der I/O-Port PB0 ist
als Ausgang konfiguriert, er treibt den Lautsprecher an. Sein
Ausgang ist Low.
Das Eingangsbit PB3, an das die Taste angeschlossen ist, hat
den Pullup-Widerstand eingeschaltet und ist High, solange die
Taste nicht gedrückt ist.
Ebenfalls am PB3 ist die PCINT-Maske und der PCINT-Interrupt-Enable
gesetzt. Bei Tastendrücken und beim Loslassen wird daher der
PCINT ausgelöst.
Der Timer TC0 ist im CTC-Modus, weshalb er nach Erreichen des
Vergleichswertes in Compare-Match-A zurücksetzt. Der Ausgang
PB0 wird bei Erreichen des Compare-Match-A-Werts auf Null gesetzt,
welches PB0 auf Null lässt und den Lautsprecher stumm macht.
Der Vorteiler ist auf acht gesetzt, was den Timer mit
1.200.000 / 8 = 150 kHz Takt ansteuert.
Der AD-Wandler ist aktiv und wird mit einem Vorteiler von 128
getaktet. Das bedeutet pro Wandlung bei 13 Taktzyklen
13 * 128 / 1.200.000 = 1,386 ms
Wandlerzeit.
Seine Referenzspannung ist die Betriebsspannung (5 V). Der
eingestellte Spannungswert 0,675 V sollte zu einem
Wandlungsergebnis von 1.024 * 0,675 / 5,0 = 138 führen.
Ist das ADLAR-Bit gesetzt, was hier der Fall ist, liefert
das MSB in ADCH 138 / 4 = 34 (hexadezimal 0x22).
Das ADC-Ready-Interrupt-Enable-Bit ist ebenfalls gesetzt,
der AD-Wandler führt nach jedem Wandlervorgang einen
Interrupt aus.
Die erste Wandlung hat begonnen, was man an der angezeigten
Fortschrittsanzeige erkennt.
Die erste AD-Wandlung hat einigen Fortschritt genommen. Da
die erste Wandlung etwa 1,38 ms dauert (und sogar ein
bisschen länger, weil der AD-Wandler frisch eingeschaltet
wurde) bleibt der Prozessor im Schlafzustand.
Nach Abschluss der Wandlung ist der ADC-Ready-Interrupt
ausgelöst worden und das MSB des ADC-Ergebnisses wird
mit der Instruktion in rimp,ADCH nach R17 eingelesen.
Die Umkehr der Bits des Ergebnisses mit der Instruktion
com rimp liefert 0xFF - 0x22 = 0xDD oder dezimal 221.
Mit out OCR0A,rimp wird dieses invertierte Ergebnis
in das Vergleichsregister Compare A geschrieben. Damit tritt
jetzt alle (221 + 1) * 8 / 1.200.000 = 1,48 ms ein
Compare-Match-A ein. Das führt noch nicht zu einem
hörbaren Ereignis, da PB0 bei Erreichen des
Compare-Match-A-Wertes noch immer so eingestellt ist, dass
der Lautsprecherausgang auf Null stehen bleibt.
Das ändert sich erst, wenn wir den PCINT-Interrupt
ausführen. Das kriegen wir hin, indem wir auf das Bit
PCINT3 in der untersten Zeile der Portdarstellung klicken.
Der PCINT-Interrupt wird nach vier Wartezyklen ausgeführt,
falls kein anderer Interrupt ausgeführt wird und kein
höherwertigerer zur Ausführung ansteht (was nur ein
INT0 sein könnte, der aber hier gar nicht enabled ist).
Innerhalb der PCINT-Interrupt-Service-Routine schalten die
Instruktionen ldi rimp,(1<<COM0A0)|(1<<WGM01)
und out TCCR0A,rimp das Torkeln des PB0-Pins ein,
aber nur falls der Tasteneingang Null (Taste gedrückt)
ist. Ist er hingegen Eins, wird der PB0-Ausgang wieder auf
"Clear" geschaltet.
Nach Erreichen des Compare-Match-A-Wertes (und dem Rücksetzen
des Timers auf Null) ändert sich das Portbit PORTB0 nun
tatsächlich und der Lautsprecher kriegt den ersten
Schlag versetzt.
Der Pin PB0 ist jetzt High, der Elko vor dem Lautsprecher wird
schlagartig auf Plus geladen, der Ladestrom fließt
durch den Lautsprecher und bewegt die Membran.
Da das CTC-Ereignis nach 1,4775 ms eintrat, entsprechen
jeweils zwei solche Ereignisse (Ein- und Ausschalten des
Elko/Lautsprechers) einer hörbaren Frequenz von
338 Hz, also ein Ton irgendwo zwischen e1
und f1 (siehe nächstes Kapitel).
9.4.1 Aufgabenstellung
In diesem Teil der Lektion soll die Tonleiter gespielt werden, das Potentiometer soll der
Tonauswahl dienen.
9.4.2 Die Tonleiter
Leider ist es so, dass Musik in unserer Hemisphäre nur mit ganz bestimmten Frequenzen
geht. Sanft wechselnde Tonfrequenzen sind bei unserem Hörvermögen und musikalischem
Empfinden total out und werden eher als störend empfunden. Wir brauchen daher eine
Tonleitertabelle, die nur die hierzulande zulässigen Töne umfasst. Das sind die
in der nachfolgenden Tabelle stehenden Frequenzen.
Um das dem Timer beizubringen, sind für eine Taktfrequenz von 1,2 MHz auch noch die
optimalen Werte für den Vorteiler und die CTC-Werte angegeben. Da die Timermimik diese
Töne nicht genau trifft, sind die tatsächlich erzeugten Frequenzen und die
Abweichungen vom korrekten Ton auch noch gleich mit angegeben.
Die Abweichungen liegen alle unter 1% und dürften für den ungeübten
Musikliebhaber unmerklich sein. Da der Prozessor werksseitig nur 10% Genauigkeit
des internen RC-Oszillators garantiert, ist das aber alles ohnehin nur akademisch
und wird nur bei einem aufwändigen Nachjustieren des RC-Oszillators oder bei
einer Quarztaktung des Prozessors erreicht (wofür der ATtiny13 ungeeignet
ist, dazu müssten wir auf den ATtiny25 umsteigen, der kann das).
Um nun den Timer dazu zu kriegen, genau nur diese Frequenzen zu treffen, brauchen wir eine
Tonleitertabelle, die dem Prozessor diese Werte irgendwie beibringt. Da die Werte für die
einzelnen Oktaven (von a nach a', von a' nach A, von A nach A' und von A' nach A'') sich immer
um den Faktor 2 unterscheiden, könnten wir die CTC-Werte auch berechnen, aber das würde
bei den höheren Frequenzen oberhalb D etwas ungünstig, weil die optimalerweise mit
einem Vorteiler von 1 statt 8 erzeugt werden. Unsere Tabelle enthält daher sowohl den
CTC-Wert als auch den Vorteiler für alle vier Oktaven.
9.4.3 Einführung in die Tabellenprogrammierung
Unsere Tabelle braucht 2*29 = 58 Bytes Länge. Es gibt prinzipiell drei Orte im
Prozessor, an denen wir eine solche Tabelle ablegen könnten:
- den SRAM-Speicher. Er umfasst im ATtiny13 64 Bytes, wäre also ziemlich voll und
es könnten Konflikte mit dem Stapel ins Haus stehen, der ja auch das SRAM benutzt.
- das EEPROM. Es umfasst ebenfalls 64 Bytes. Es würde passen, wäre aber recht
gefüllt.
- den Flash-Speicher. Er bietet 512 Worte oder 1.024 Bytes Platz, das wäre
also reichlich und böte also Raum für weitere Oktaven.
9.4.5 Einführung in die Multiplikation
Damit hätten wir das Problem mit den Noten gelöst, laufen aber geradewegs in ein
anderes: unser ADC liefert Werte zwischen 0 und 1.023 (ohne ADLAR) bzw. 0 und 255 (mit ADLAR).
Unsere Notentabelle hat aber nur 29 Noten. Wir könnten jetzt alle Werte oberhalb von 28
einfach auf 28 setzen und das Problem wäre schon gelöst. So richtig schön ist
diese Lösung nicht, weil sich die 29 Noten dann auf den unteren 2,8 % (10-Bit-ADC)
bzw. 11 % des Potentiometers eng drängeln und der obere Teil arg langweilig immer
nur A'' liefert.
Irgendwie müssen wir die ankommenden 1.023 oder 255 zu 28 verkleinern. Der C-Programmierer
ist da jetzt fein raus: er teilt den Wert einfach durch 36,5 bzw. durch 9,1 und rundet. Und
schwupp-die-wupp hat er sich die Fließkommabibliothek in seinen Code geholt. Und die
alleine ist schon so groß, dass sie nicht mehr in den Flashspeicher passt. Der
C-Programmierer steigt jetzt auf einen ATxmega um, mit reichlich Speicher, damit sich seine
Monsterbibliothek darin pudelwohl fühlt. Wir ersetzen solche Monster aber gerne mit
Intelligenz und denken uns was Passendes dafür aus.
Die Aufgabe lautet (mit ADLAR, 10 Bit Auflösung nicht nötig) eigentlich:
Ergebnis = 29 * ADC / 256
Und teilen durch 256 ist in der digitalen Prozessorwelt ja sowas von einfach: einfach das
LSB des Multiplikationsergebnisses wegstreichen und nur das MSB nehmen.
9.4.5.1 Einfachstmultiplikation
Bleibt die Multiplikation des ADC-Wertes mit 29. Das ist eine einfache Aufgabe: einfach den
ADC-Wert 29 mal aufaddieren. Z. B. so:
in R0,ADCH ; Wert aus dem ADC in R0 lesen, +1 = 1
clr R2 ; R2:R1 ist das Ergebnis, +1 = 2
clr R1 ; +1 = 3
ldi R16,29 ; Multiplikation mit 29, +1 = 4
Schleife:
add R1,R0 ; Dazu zaehlen, +29*1 = 33
brcc NachUeberlauf ; kein Ueberlauf, +29*1 = 62
inc R2 ; MSB eins hoeher, +29*1 = 91
NachUeberlauf:
dec R16 ; abwaerts zaehlen, + 29*1 = 130
brne Schleife ; noch mal addieren, +28*2 + 1 = 187
Die 187 Takte, die benötigt werden, sind nicht so arg lange. Bei 1,2 MHz sind das
156 µs, weniger als eine einzige NF-Schwingung. Aber es geht schneller.
9.4.5.2 Schneller multiplizieren
Multiplizieren mit zwei ist in der Binärwelt besonders einfach und schnell. Wir
könnten die nächste Zweierpotenz ansteuern und dann dazuzählen oder abziehen.
Das ginge so:
in R0,ADCH ; Wert aus dem ADC in R0 lesen, +1 = 1
clr R2 ; R2:R1 ist das Ergebnis, +1 = 2
mov R1,R0 ; Wert einmal kopieren, +1 = 3
lsl R1 ; LSB mit zwei malnehmen, +1 = 4
rol R2 ; MSB mit zwei malnehmen und Carry hineinschieben, +1 = 5
lsl R1 ; mit vier malnehmen, +1 = 6
rol R2 ; +1 = 7
lsl R1 ; mit acht malnehmen, +1 = 8
rol R2 ; +1 = 9
lsl R1 ; mit 16 malnehmen, +1 = 10
rol R2 ; +1 = 11
lsl R1 ; mit 32 malnehmen, +1 = 12
rol R2 ; +1 = 13
sub R1,R0 ; einmal abziehen, +1 = 14
brcc KeinCarry1 ; kein Carry, +1/2 = 15/16
dec R2 ; MSB vermindern, +1 = 16
KeinCarry1:
sub R1,R0 ; zweimal abziehen, +1 = 17
brcc KeinCarry2 ; kein Carry, +1/2 = 18/19
dec R2 ; MSB vermindern, +1 = 19
KeinCarry2:
sub R1,R0 ; dreimal abziehen, +1 = 20
brcc KeinCarry3 ; kein Carry, +1/2 = 21/22
dec R2 ; MSB vermindern, +1 = 22
Neu ist die Instruktion ROL Register, ROtate Left. Sie ist das Pendant zu ROR,
rollt aber das Übertragbit nach links in das Register und Bit 7 des Registers in das
Übertragsbit.
Das ist mehr als acht mal kürzer als die Primitivmultiplikation. Aber es geht noch
schneller.
9.4.5.3 Noch schneller multiplizieren
Die bisherigen Läsungen sind eng auf die konkrete Aufgabe zugeschnitten. Die echte
binäre Multiplikation, anwendbar auf alle Zahlen mit je acht Bit, ist aber gar nicht so
kompliziert, dass auch C-Programmierer sie erlernen können, wenn sie mal nicht auf
Riesenbibliotheken und Riesenchips umschalten wollen.
Die binäre Multiplikation ist sogar einfacher als die Dezimalmultiplikation. Die geht
bekanntlich so. Die erste Zahl wird mit den Einern der zweiten Zahl malgenommen. Dann wird
die erste Zahl um eine Stelle nach links geschoben, mit den Zehnern der zweiten Zahl
malgenommen und zum Ergebnis addiert. Dann erneutes Linksschieben und Malnehmen mit den
Hunderten. Die Summe, das Ergebnis der Multiplikation ist fertig.
Die binäre Multiplikation ist noch einfacher, weil es ja nur zwei Ziffern gibt. Entweder
wird also die links geschobene Zahl addiert (Ziffer ist Eins) oder nicht (Ziffer ist Null).
Die Multiplikation von 255 mit 29 zeigt das Bild.
Die zweite Zahl in R16 wird rechts geschoben, dadurch gelangt die nächste Ziffer in das
Carry-Bit im Statusregister (C). Ist das eine Null, wird die erste Zahl nicht addiert. Ist es
eine Eins, wird sie addiert (16-Bit-Addition mit Übertrag). Dann wird die erste Zahl
um eine Stelle als links geschoben (16-Bit-Linksschieben). Es folgt Rechtsschieben des
nächsten Bits in der zweiten Zahl, Nichtaddieren/Addieren, etc. Sind alle Einsen
herausgeschoben, ist die Multiplikation beendet.
So sieht der Code aus.
in R0,ADCH ; lese MSB vom ADC als LSB, 1
clr R1 ; MSB leeren, +1 = 2
clr R2 ; Ergebnis LSB leeren, +1 = 3
clr R3 ; dto., MSB, +1 = 4
ldi R16,29 ; Multiplikant setzen, +1 = 5
Schleife:
lsr R16 ; niedrigstes Bit in Carry, +5*1 = 10
brcc NachAddieren ; C = Null, +1*2+4*1 = 16
add R2,R0 ; addieren LSB, +5*1 = 21
adc R3,R1 ; addieren MSB mit Carry, +5*1 = 21
NachAddieren:
lsl R0 ; Linksschieben erste Zahl, MSB, +5*1 = 26
rol R1 ; Carry in MSB, +5*1 = 31
tst R16 ; Ende erreicht?, +1*5 = 36
brne Schleife ; noch Einsen vorhanden, +4*2+1*1 = 45
Das Ergebnis (28) steht im Register R3.
Ok, das sind 45 Takte, also mehr als die Zweierpotenzlösung. Dafür funktioniert
die Routine mit jeder beliebigen 8-Bit-Zahl in gleicher Weise. Und ist so einfach, dass es auch
denkfaule C-Programmierer intellektuell bewältigen könnten.
Damit ist unser Problem gelöst, wie aus 255 nur noch 28 werden. Und ganz ohne Teilen und
Fließkomma-Bibliothek. Fast jedes rechnerische Problem lässt sich damit lösen,
wenn man es irgendwie zu einer Multiplikation umgebogen kriegt.
9.4.6 Das Programm
Damit haben wir alle Grundlagen zusammen, um loszulegen. Das Programm steht unten,
der Quellcode hier.
;
; *************************************
; * Tonleiter-Toene mit dem ATtiny13 *
; * (C)2016 by www.gsc-elektronic.net *
; *************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; Der AD-Wandler ermittelt staendig die
; Spannung am Potentiometereingang,
; wandelt das Ergebnis durch Multipli-
; kation mit 29 und Teilen des Ergebnis-
; ses durch 256 in die Nummer eines Tons
; zwischen 0 und 28 um. Fuer diesen Ton
; wird aus einer Notentabelle die Timer-
; vorteiler- und die Vergleichsregister-
; A-Einstellung ermittelt und in den
; Timer 0 geschrieben.
; Die Taste schaltet die Tonausgabe an
; und aus.
;
; --------- Register ------------------
; Verwendet: R0 fuer LPM und Berechnungen
; Verwendet: R1 fuer Berechnungen
.def rMultL = R2 ; Multiplikator, LSB
.def rMultH = R3 ; dto., MSB
; frei: R4 .. R14
.def rSreg = R15 ; Statusregister sichern
.def rmp = R16 ; Vielzweckregister
.def rimp = R17 ; Vielzweckregister Interrupts
.def rFlag = R18 ; Flaggenregister
.equ bAdcR = 0 ; ADC-Wert eingelesen
; frei R18 .. R29
; Verwendet: R31:R30, ZH:ZL fuer LPM
;
; --------- Ports ---------------------
.equ pOut = PORTB ; Ausgabeport
.equ pDir = DDRB ; Richtungsport
.equ pInp = PINB ; Eingangsport
.equ bLspD = DDB0 ; Lautsprecherausgang
.equ bTasO = PORTB3 ; Pullup Tasteneingang
.equ bTasI = PINB3 ; Tasteninputpin
.equ bAdID = ADC2D ; ADC-Input-Disable
;
; --------- Timing --------------------
; Takt = 1200000 Hz
; Vorteiler = 1 und 8
; CTC-TOP-Bereich = 0 .. 255
; CTC-Teiler-Bereich = 1 .. 256
; Toggle-Teiler = 2
; Frequenzbereich: 600 kHz .. 293 Hz
;
; ---- Reset- und Interruptvektoren ---
.CSEG ; Assemblieren in den Code-Bereich
.ORG 0 ; An den Anfang
rjmp Start ; Rest-Vektor, Init
reti ; INT0-Int, nicht aktiv
rjmp PcIntIsr ; PCINT-Int, aktiv
reti ; TIM0_OVF, nicht aktiv
reti ; EE_RDY-Int, nicht aktiv
reti ; ANA_COMP-Int, nicht aktiv
reti ; TIM0_COMPA-Int, nicht aktiv
reti ; TIM0_COMPB-Int, nicht aktiv
reti ; WDT-Int, nicht aktiv
rjmp AdcIsr ; ADC-Int, aktiv
;
; ---------- Interrupt Service Routinen -----
;
; PCINT Interrupt
; Wird von Pegelaenderungen am PCINT-Eingang
; ausgeloest (Tasteneingang).
; Wenn die Taste gedrueckt ist, wird durch
; Einstellung des OC0A-Ausganges auf Torkeln
; die Tonausgabe eingeschaltet. Wenn nicht,
; der OC0A-Ausgang auf Null geschaltet.
;
PcIntIsr: ; PCINT-Interrupt Tasten-Interrupt
sbic pInp,bTasI ; Ueberspringe bei Taste = 0
rjmp PcIntIsrAus ; Taste ist nicht gedrueckt
; Tonausgabe einschalten
ldi rimp,(1<<COM0A0)|(1<<WGM01) ; Toggle, CTC-A
out TCCR0A,rimp ; in Kontrollregister A
rjmp PcIntIsrRet ; zurueck
PcIntIsrAus:
; Tonausgabe ausschalten
ldi rimp,(1<<COM0A1)|(1<<WGM01) ; Clear, CTC-A
out TCCR0A,rimp
PcIntIsrRet:
reti
;
; ADC Ready Interrupt
;
; Wird vom AD-Wandler ausgeloest, wenn die Wandlung
; beendet ist.
; Liest die oberen 8 Bit des Ergebnisses in das
; Register rMultL und setzt die bAdcR-Flagge.
; Startet die naechste Wandlung.
;
AdcIsr: ; ADC-Interrupt
in rSreg,SREG ; sichern Statusregister
in rMultL,ADCH ; lese MSB Ergebnis
sbr rFlag,1<<bAdcR ; Flagge Neuer ADC-Wert
ldi rimp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rimp ; in ADC-Kontrollregister A
out SREG,rSreg ; wiederherstellen Statusregister
reti
;
; ---------- Programmstart und Init ----------
Start:
; Stapel einrichten
ldi rmp,LOW(RAMEND) ; SRAM-Ende
out SPL,rmp ; in Stackzeiger
; In- und Output-Ports
ldi rmp,1<<bLspD ; Lautsprecherausgang Richtung
out pDir,rmp ; in Richtungsregister
ldi rmp,1<<bTasO ; Pullup am Tastenport
out pOut,rmp ; in Ausgangsregister
; Timer als CTC konfigurieren
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear, CTC-A
out TCCR0A,rmp ; in Kontrollregister A
; Vorteiler und Timerstart erfolgt durch ADC-Interrupt
; AD-Wandler konfigurieren und starten
ldi rmp,(1<<ADLAR)|(1<<MUX1) ; Linksjustieren, ADC2
out ADMUX,rmp ; in ADC-MUX
ldi rmp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rmp ; in Kontrollregister A, starten
; PCINT fuer Tasteneingang
ldi rmp,1<<PCINT3 ; PB3-Int ermoeglichen
out PCMSK,rmp ; in PCINT-Maskenregister
ldi rmp,1<<PCIE ; PCINT ermoeglichen
out GIMSK,rmp ; in Interrupt-Maskenregister
; Schlafen ermoeglichen
ldi rmp,1<<SE ; Schlafen, Idle-Modus
out MCUCR,rmp ; in MCU-Kontrollregister
; Interrupts einschalten
sei
; ------------ Hauptprogramm-Schleife --------
Schleife:
sleep ; schlafen legen
nop ; Aufwachen
sbrc rFlag,bAdcR ; Ueberspringe wenn Adc-Flagge Null
rcall AdcCalc ; Umrechnung von ADC-Wert in Ton
rjmp Schleife ; wieder schlafen legen
;
; ------------ AD-Wert setzen ----------------
; ADC-Wert in Tonhoehe wandeln und Timer starten
; AD-Wert ist in rMultL
; Ausgeloest von der gesetzten Flagge bAdcR, die
; der AD-Wandler nach jeder Wandlung setzt.
;
AdcCalc:
cbr rFlag,1<<bAdcR ; Flagge zuruecksetzen
; ADC-Wert mit 29 multplizieren
clr rMultH ; MSB loeschen
clr R0 ; Ergebnis LSB Null setzen
clr R1 ; dto., MSB
ldi rmp,29 ; Anzahl Toene plus Eins
AdcCalcShift:
lsr rmp ; niedrigstes Bit in Carry
brcc AdcCalcNachAdd
add R0,rMultL ; addiere LSB zum Ergebnis
adc R1,rMultH ; addiere MSB mit Carry
AdcCalcNachAdd:
lsl rMultL ; mal zwei schieben
rol rMultH ; in MSB rollen und mal zwei
tst rmp ; rmp schon leergeschoben?
brne AdcCalcShift
; Ton aus Tabelle holen
lsl R1 ; Tonnummer mal zwei
ldi ZH,HIGH(2*Tonleitertabelle) ; Z auf Tabelle
ldi ZL,LOW(2*Tonleitertabelle)
add ZL,R1 ; Tonnummer addieren
ldi rmp,0 ; Ueberlauf addieren
adc ZH,rmp
lpm R0,Z+; Tabellenwert LSB in R0 einlesen
out TCCR0B,R0 ; in Timerregister B
lpm ; Tabellenwert MSB in R0 einlesen
out OCR0A,R0 ; in Vergleichsregister A
ret ; fertig
;
; ------- Tonleitertabelle -----------
Tonleitertabelle:
.db 1<<CS01, 169 ; a #0
.db 1<<CS01, 151 ; h #1
.db 1<<CS01, 142 ; c #2
.db 1<<CS01, 127 ; d #3
.db 1<<CS01, 113 ; e #4
.db 1<<CS01, 106 ; f #5
.db 1<<CS01, 95 ; g #6
.db 1<<CS01, 84 ; a' #7
.db 1<<CS01, 75 ; h' #8
.db 1<<CS01, 71 ; c' #9
.db 1<<CS01, 63 ; d' #10
.db 1<<CS01, 56 ; e' #11
.db 1<<CS01, 53 ; f' #12
.db 1<<CS01, 47 ; g' #13
.db 1<<CS01, 42 ; A #14
.db 1<<CS01, 37 ; H #15
.db 1<<CS01, 35 ; C #16
.db 1<<CS01, 31 ; D #17
.db 1<<CS00, 227 ; E #18
.db 1<<CS00, 214 ; F #19
.db 1<<CS00, 190 ; G #20
.db 1<<CS00, 169 ; A' #21
.db 1<<CS00, 151 ; H' #22
.db 1<<CS00, 142 ; C' #23
.db 1<<CS00, 127 ; D' #24
.db 1<<CS00, 113 ; E' #25
.db 1<<CS00, 106 ; F' #26
.db 1<<CS00, 95 ; G' #27
.db 1<<CS00, 84 ; A'' #28
;
; Ende Quellcode
;
Neben den schon eingeführten Instruktionen LPM mit allen seinen Untervarianten sind keine
neuen Instruktionen verwendet.
Noch ein wichtiger Hinweis: Am Pin 5 befindet sich jetzt der Elko und ein niederohmiger
Lautsprecher. Beim Programmieren steht dies den hochfrequenten Programmierimpulsen im Weg, wir
kriegen Fehlermeldungen beim Programmieren. Vor dem Programmieren also einfach den Elko
abziehen und nach dem Programmieren wieder reinstecken.
9.4.5 Debuggen mit dem Studio
Wenn wir solche Routinen schreiben wie die Multiplikationsroutine oder das Auslesen aus der
Tabelle wüssten wir schon gerne, ob da auch das richtige Ergebnis herauskommt.
Spätestens dann, wenn auf Tastendruck aus dem Lautsprecher nichts heraustönt. Es
gibt eine Möglichkeit, dem Prozessor bei der Arbeit zuzugucken und jeden Schritt einzeln
zu beobachten. Sie heißt Simulator und ist im Studio eingebaut.
Um den Simulator zu starten, drücken wir einfach "Start Debugging" Bevor wir
das tun, fügen wir in unseren Quellcode die folgenden Zeilen ein:
; ---- Multplikationsroutine debuggen
.equ debug = 1 ; Debuggen einschalten
.if debug == 1
ldi rmp,0xFF ; ADC-Wert vorwaehlen
mov rMultL,rmp
rjmp AdcCalc ; springe zur Multiplikationsroutine
.else
;
; ---- Reset- und Interruptvektoren ---
.CSEG ; Assemblieren in den Code-Bereich
.ORG 0 ; An den Anfang
rjmp Start ; Rest-Vektor, Init
reti ; INT0-Int, nicht aktiv
rjmp PcIntIsr ; PCINT-Int, aktiv
reti ; TIM0_OVF, nicht aktiv
reti ; EE_RDY-Int, nicht aktiv
reti ; ANA_COMP-Int, nicht aktiv
reti ; TIM0_COMPA-Int, nicht aktiv
reti ; TIM0_COMPB-Int, nicht aktiv
reti ; WDT-Int, nicht aktiv
rjmp AdcIsr ; ADC-Int, aktiv
.endif
;
Die Zeilen maskieren mit dem Schalter "debug" die Reset- und Vektortabelle: Ist der
Schalter == 1, dann assembliert der Assembler die drei markierten Zeilen, die Reset- und
Interruptvektortabelle nicht. Damit springt die Simulation mit dem Wert 0xFF im Register
rMultL direkt in die Multplikationsroutine. Der Grund für diese Maskierung der Reset-
und Vektortabelle ist die ".ORG"-Direktive: sie würde beim Assemblieren zu
einem Fehler führen, weil die drei eingefügten Instruktionen die
Ausführungsadresse erhöhen und mit ".ORG" immer nur vorwärts
gesprungen werden kann. Alternativ könnten wir auch die Multiplikationsroutine
herauskopieren und als "stand alone" assemblieren, weil uns der Rest des Programmes
ja eigentlich nicht interessiert.
Nach dem Start des Simulators bietet dieser eine Vielzahl an Informationen zum Zustand. Man
kann den Inhalt von Registern beobachten (links unten), man kann eine Stopuhr starten, die
Instruktionen und Ausführungszeiten werden angezeigt (links Mitte). Der Cursor in gelb
(rechts oben) zeigt auf die erste ausführbare Instruktion.
Mit "Step into" (F11) wird die Ausführung der Instruktion simuliert.
Ausgeführt wurde "ldi rmp,0xFF", mit rmp = R16. In der Registerliste ist der
geänderte Wert rot markiert, der Programmzähler steht auf 0001 und der Cursor steht
auf der nächsten Instruktion.
Um nicht jede Instruktion einzeln ausführen zu müssen, können wir
"Breakpoints" setzen. Dazu setzt man den Cursor in die betreffende Zeile, drückt
die rechte Maustaste und wählt "Toggle breakpoint". Dann führt der
Simulator mit "Run" im Debug-Menue alle Instruktionen aus, bis er auf einen solchen
Breakpoint stößt und hält dann an.
In unserem Fall sind das zwei Punkte: das Ende der Multiplikationsroutine und das Ende des
Schreibens in die Timerregister. Im ersten Fall steht im Register R1 0x1C, was dezimal 28
bedeutet.
Das ist der Zustand des Timers beim zweiten Breakpoint. Die beiden Register, die beschrieben
wurden (TCCR0B und OCR0A) zeigen die richtigen Werte aus der Notentabelle (0x01 und 0x54). Da
wir das Timer-Init im Hauptprogramm übersprungen haben, sind die anderen Timer-Register
nicht korrekt.
Mit diesem Werkzeug kommen wir Fehlern in den eigenen Programmen auf die Spur.
9.4.6 Debuggen mit avr_sim
Die Simulation mit avr_sim
zeigt die folgenden Abläufe. Simuliert wird die Multiplikationsroutine und
der Zugriff auf die Notentabelle mit LPM.
Um die Hardware zu initiieren steppen wir durch die ersten Instruktionen bis die
Interrupts mit SEI ermöglicht werden.
Wie im ersten Fall wird der Pin PB0 als Ausgang geschaltet und mit Low-Potential
initiiert. PB3 hat wie immer seinen Pullup-Widerstand eingeschaltet und ist in
der PCINT-Maske als Interruptquelle bei allen Flankenwechseln nominiert.
Der AD-Wandler misst wieder laufend die Spannung am ADC2-Eingang, was dem
I/O-Pin PB4 entspricht. Die simulierte Spannung ist diesmal 3,75 V,
was nach der Wandlung 3,75 / 5,00 * 1.023 = 767
oder links-adjustiert 191 dezimal oder 0xBF entspricht.
Nach Erreichen des ADC-Ready-Interrupts hat die Interrupt-Service-Routine
die obersten 8 Bits in das Register R2 geschrieben und die bAdcR-Flagge
gesetzt. Ihre Bearbeitung findet ausserhalb der Interrupt-Sevice-Routine,
nach dem Aufwachen vom SLEEP statt, weil sie die Interrupts zu
lange blockieren würde. In der Routine AdcCalc: erfolgt die Umrechnung
von ADC-Werten in Noten. Die folgenden Instruktionen zeigen zuerst die
Multiplikation, um aus dem ADC-Wert einen Wert von 0 bis 29 zu
fabrizieren:
clr rMultH ; MSB loeschen
clr R0 ; Ergebnis LSB Null setzen
clr R1 ; dto., MSB
ldi rmp,29 ; Anzahl Toene plus Eins
clr rMultH (R3) und clr R0/R1 löschen erstmal das High-Byte
für das Multiplizieren und das Ergebnis. ldi rmp,29 wird auf die
Anzahl der Notentöne gesetzt. Nun kann die Multiplikation beginnen.
Die Anweisung
lsr rmp ; niedrigstes Bit in Carry
schiebt R16 nach rechts und das niedrigste Bit in R16 in die Carry-Flagge
im Statusregister. In diesem Fall ist es eine Eins.
Weil es eine Eins ist, addieren die folgenden Instruktionen
brcc AdcCalcNachAdd
add R0,rMultL ; addiere LSB zum Ergebnis
adc R1,rMultH ; addiere MSB mit Carry
bei gesetzter C-Flagge die 16-Bit-Zahl in R3:R2 zum Ergebnis in R1:R0.
Da es 16-Bits sind, addiert adc eventuelle Überläufe
(Carry) aus der unteren Addition mit dazu.
Die beiden Instruktionen
lsl rMultL ; mal zwei schieben
rol rMultH ; in MSB rollen und mal zwei
schieben den Multiplikator in R3:R2 um eine Position nach links und
multiplizieren damit den Inhalt mit zwei. Die Instruktion rol
schiebt noch das aus dem unteren Byte stammende Carry in das MSB ein.
Weil R16 noch immer Bits zum Multiplizieren enthält, wird es
wieder rechts geschoben (geteilt durch 2) und das niedrigste Bit in
das Carry geschoben. Diesmal ist Carry 0 und das Addieren von R3:R2
zum Ergebnis in R1:R0 entfällt.
Dann wird R3:R2 erneut mit Shift Left und Rotate Left mit zwei
malgenommen.
Es gibt immer noch Einsen in rmp, daher müssen wir weiter durch
zwei teilen und Bits in das Carry schieben. Diesmal ist es wieder eine
Eins.
Wieder wird daher R3:R2 zu R1:R0, in 16-Bit-Manier mit ADD und
ADC, addiert.
Die nachfolgende Multiplikation von R3:R2 mit zwei ist nicht dargestellt.
Es wird langsam langweilig, aber rmp muss noch mal rechts geschoben
werden. Weil dabei eine Eins herausrollt muss wieder addiert werden.
Das nächste Addieren von R3:R2 zu R1:R0 erfolgt hier.
Und die nächste Multiplikation mit 2 für R3:R2 hier.
Nun rollt die letzte Eins in das Carry-Flag und wir sind fast fertig mit
der Multiplikation.
Die letzte Addition von R3:R2 zu R1:R0. Die nachfolgende Multiplikation
mit zwei ist eigentlich nicht mehr nötig, weil R16 ja schon leer ist.
Die Stoppuhr wurde benutzt um die Dauer der Multiplikation zu messen.
Sie zeigt 55 µs an, was ziemlich schnell ist für die
vielen Schieben-, Rotieren- und Addieren-Befehle. Jedenfalls kein
vernünftiger Grund für das Wechseln auf einen ATmega (mit
eingebauter schneller Hardware-Multiplikation) oder für das
Hinzuladen einer ausgiebigen Rechenbibliothek. Das würde die
gerade mal zwölf Instruktionen auf einige Hundert bis Tausend
aufblasen und zu einem anderen Prozessortyp zwingen, weil dafür
der Flashspeicher vom ATtiny13 zu klein ist. Arme C-Programmierer:
ersetzen Intelligenz durch Blow-Up-Code!
Nach der Multiplikation ist die zu spielende Note 0x15 oder dezimal
21 im MSB des Ergebnisses in R1. Nun müssen wir das zu unserer
Notentabelle addieren, um die Notenparameter aus der Tabelle ablesen
zu können. Die Instruktionen
lsl R1 ; Tonnummer mal zwei
ldi ZH,HIGH(2*Tonleitertabelle) ; Z auf Tabelle
ldi ZL,LOW(2*Tonleitertabelle)
nehmen das Ergebnis erst mal mal zwei, weil die Notentabelle pro
Eintrag aus zwei Bytes besteht. Dann wird Z auf die Adresse des
Tabellenanfangs (mal zwei wegen Low- und High-Byte) gestellt.
Die Tonleitertabelle ist hier:
150: ; ------- Tonleitertabelle -----------
151: Tonleitertabelle:
152: .db 1<<CS01, 169 ; a #0
000049 A902
153: .db 1<<CS01, 151 ; h #1
00004A 9702
...
Ihre Adresse ist 0x000049, was sich zu 0x0092 in Z verdoppelt, um
auf die Worttabelle byteweise zugreifen zu können.
Nun wird der verdoppelte Wert in R1 zur Tabellenadresse 0x0092
in Z addiert.
add ZL,R1 ; Tonnummer addieren
ldi rmp,0 ; Ueberlauf addieren
adc ZH,rmp
ZL und R1 werden normal addiert, das Ergebnis in ZL abgelegt. Um
den Fall abzudecken, dass sich die Tabelle über die Bytegrenze
erstreckt, wird ein eventuelles Carry noch zur MSB hinzu addiert.
Um die Null und das Carry zu addieren, darf nicht clr rmp
verwendet werden, denn das würde das Carry-Flag löschen.
ldi rmp,0 beeinflusst das Carry-Flag hingegen nicht.
Z zeigt jetzt auf 0x00BC, was der Note an Adresse 0x00005E (mal
zwei) entspricht. Dort steht korrekterweise Note #21:
173: .db 1<<CS00, 169 ; A' #21
00005E A901
Daher wird der Prescaler nun auf CS00 = 1 (Vorteiler = 1) und der
Vergleichswert A auf 169 gesetzt. Das bedeutet eine Frequenz von
f = 1.200.000 / 1 / (169 + 1) / 2 = 3.529,4 Hz
und entspricht ungefähr der Note A' (Sollwert: 3.520 Hz.)
Die Instruktionen
lpm R0,Z+; Tabellenwert LSB in R0 einlesen
out TCCR0B,R0 ; in Timerregister B
lesen das LSB aus der Tabelle, erhöhen die Adresse Z und
schreiben das gelesene Byte in das Timer-Kontrollregister B.
Nun kommt noch das zweite lpm, diesmal ohne irgendwas. Das
liest das Byte an der schon erhöhten Adresse in das Register
R0 (0xA9 oder dezimal 129). Das braucht dann nur noch ins
Vergleichsregister A geschrieben zu werden und fertig ist der Lack.
lpm ; Tabellenwert MSB in R0 einlesen
out OCR0A,R0 ; in Vergleichsregister A
Nun steht der Spielerei von Kammerton A' nix mehr im Weg herum,
wenn wir per PCINT das Torkeln des PB0-Ausganges einschalten
würden.
9.5 Musikstück
9.5.1 Aufgabe
Auf Tastendruck soll der Prozessor "Völker hört die Signale" abspielen.
9.5.2 Das Musikstück
Das hier ist zu spielen.
Dieses sind die Notenhoehen, mit denen wir die Melodie in unsere Tonleitertabelle
übersetzen müssen. Damit wir die Melodie in eine Tabelle übersetzen können
wäre es angenehm, wenn wir diese Tabelle nicht mit Zahlen zwischen 0 und 28 sondern mit
benannten Symbolen generieren könnten. Dazu wären die Noten mit ".EQU Notenname
= Notennummer" zu definieren. Notennamen sollen mit "n" beginnen, dann den Ton
mit "a" bis "g" und danach die Oktave mit "0" bis "4"
angeben, also ungefähr so:
; Notensymbole
.equ na0 = 0
.equ nh0 = 1
.equ nc0 = 2
.equ nd0 = 3
.equ ne0 = 4
.equ nf0 = 5
.equ ng0 = 6
.equ na1 = 7
.equ nh1 = 8
.equ nc1 = 9
.equ nd1 = 10
.equ ne1 = 11
.equ nf1 = 12
.equ ng1 = 13
.equ nA2 = 14
.equ nH2 = 15
.equ nC2 = 16
.equ nD2 = 17
.equ nE2 = 18
.equ nF2 = 19
.equ nG2 = 20
.equ nA3 = 21
.equ nH3 = 22
.equ nC3 = 23
.equ nD3 = 24
.equ nE3 = 25
.equ nF3 = 26
.equ nG3 = 27
.equ nA4 = 28
Die in den Notenregeln verwendeten Klein- und Großbuchstaben sind in Assembler nicht
verwendbar, weil dieser zwischen beiden nicht unterscheidet.
Mit diesen Definitionen lautet unsere Tontabelle für dieses Musikstück nun:
Musik:
.db ne1,nd1,nc1,ng0,ne0,na1,nf0,0xFF
Eine handhabbare Kodierung.
Das angefügte 0xFF signalisiert, dass damit die Musik zu Ende ist, weil sonst der
Prozessor den gesamten Inhalt seines Flashspeichers als Musik interpretieren und abspielen
würde.
9.5.3 Tondauer
Die Notensymbolik kodiert die Dauer, über die diese Noten ertönen, wie in der nebenstehenden
Grafik gezeigt. Wir brauchen daher als zusätzliche Information noch die Dauern 1/2, 1/4, 3/8 und
1/8. In Achteln ausgedrückt: 1 (1/8), 2 (1/4), 3 (3/8) und 4 (1/2). Das bräuchte zwei Bits und
wir könnten die Dauer in der Tabelle in die Noten dazukodieren, z. B. so:
.equ bLow = 1<<5 ; Dauerkodierung, low Bit
.equ bHigh = 1<<6 ; Dauerkodierung, high Bit
.equ d12 = bLow + bHigh ; 1/2, beide Dauerbits gesetzt
.equ d14 = bHigh ; 1/4, oberes Dauerbit gesetzt
.equ d38 = bLow ; 3/8, unteres Dauerbit gesetzt
.equ d18 = 0 ; 1/8, weder oberes noch unteres Dauerbit gesetzt
Musik:
.db ne1+d38,nd1+d14,nc1+d12,ng0+d38,ne0+d18,na1+d12,nf0+d14,0xFF
Um diese Noten zu dekodieren, würden wir folgendes programmieren:
; Z-Zeiger auf Musiktabelle setzen
ldi ZH,HIGH(2*Musik) ; Zeiger auf Melodietabelle
ldi ZL,LOW(2*Musik)
; Notenbyte lesen
lpm R17,Z ; lese erste Note aus Tabelle in R17
; Musikende feststellen
cpi R17,0xFF ; Ende Musik?
breq Musikaus ; Musik ausschalten
; Notendauer ermitteln
andi R17,0b01100000 ; Bits 5 und 6 isolieren
ldi R16,1 ; Anzahl Achtel Dauer
sbrc R17,5 ; Bit 5 auswerten
subi R16,-1 ; eins hinzuzählen (addi gibt es nicht)
sbrc R17,6 ; Bit 6 auswerten
subi R17,-2 ; zwei hinzuzählen
; Note in Timertakt und Timer-CTC wandeln
lpm R17,Z+ ; Note noch mal lesen, Zeiger erhoehen
andi R17,0b10011111 ; die Dauerbits loeschen
[Wandeln und Note ausgeben]
Musikaus:
[Musikausgabe abschalten]
Wir könnten die Dauer aber auch als zweites Byte hinzufügen, was die Notenauswertung
vereinfacht, aber die Länge der Notentabelle verdoppelt. Da wir mit dieser einfachen Melodie kaum
an Flashgrenzen stoßen werden, kodieren wir das so:
Musik: ; LSB: Note oder FF, MSB: Dauer in Achtel
.db ne1,3,nd1,2,nc1,4,ng0,3,ne0,1,na1,4,nf0,2,0xFF,0xFF
Jetzt muss noch ein weiteres 0xFF angefügt werden, um wieder geradzahlig zu werden.
9.5.4 Tonpausen
Noten abspielen bedeutet nicht nur Töne, sondern auch Pausen zwischen den Tönen. Zwischen
der ersten und der zweiten Note nicht, aber zwischen allen anderen. Wir brauchen daher neben dem
Endezeichen 0xFF auch noch ein Pausenzeichen und wählen dafür 0xFE. Unsere Tabelle sieht
daher so aus:
Musik: ; LSB: Note oder FF, MSB: Dauer in Achtel
.db ne1,3,nd1,2,0xFE,1,nc1,4,0xFE,1,ng0,3,0xFE,1,ne0,1,0xFE,1,na1,4,0xFE,1,nf0,2,0xFF,0xFF
9.5.5 Spieldauer von Noten
Es kommt aber noch etwas hinzu, wenn wir mit der Tonleiter Musik abspielen wollen. Bei den
Frequenzen zwischen 440 und 7.040 Hz liegen prozessortechnisch sehr unterschiedlich lange
CTC-Sequenzen zugrunde. Bei 440 Hz entsprechen 880 CTC-Durchläufe einer Sekunde Dauer,
bei 7.040 Hz aber 14.080. Die Anzahl Durchläufe pro Sekunde Spieldauer ist umgekehrt
proportional zum CTC-Wert, wie er in der Tonleitertabelle steht. Wir könnten uns dem mit zwei
Möglichkeiten nähern:
- Wir teilen 600.000 durch den den Vorteilerwert und den CTC-Wert und kriegen die Anzahl zu
durchlaufender CTC-Zyklen pro Sekunde heraus, oder
- wir fügen diese Zahl unserer Tonleitertabelle hinzu und lesen sie mit LPM aus.
Wir sind faul, noch nicht reif zum Dividieren von 24-Bit-Ganzzahlen durch 8-Bit-Zahlen in Assembler
und möchten dem Prozessor diese Prozedur nicht zumuten. Der C-Programmierer hat dieses Problem
nicht und importiert seine Fließkomma-Bibliothek, um festzustellen, dass er doch einen ATxmega
braucht.
Die Umformulierung unserer Notentabelle mit der Dauer des jeweiligen Tones in Achtel-Sekunden sieht
dann so aus:
Notentabelle_Dauer:
.DB 1<<CS01, 169, 111, 0 ; a #0
.DB 1<<CS01, 151, 124, 0 ; h #1
.DB 1<<CS01, 142, 132, 0 ; c #2
.DB 1<<CS01, 127, 148, 0 ; d #3
.DB 1<<CS01, 113, 166, 0 ; e #4
.DB 1<<CS01, 106, 177, 0 ; f #5
.DB 1<<CS01, 95, 197, 0 ; g #6
.DB 1<<CS01, 84, 223, 0 ; a' #7
.DB 1<<CS01, 75, 250, 0 ; h' #8
.DB 1<<CS01, 71, 8, 1 ; c' #9
.DB 1<<CS01, 63, 42, 1 ; d' #10
.DB 1<<CS01, 56, 79, 1 ; e' #11
.DB 1<<CS01, 53, 98, 1 ; f' #12
.DB 1<<CS01, 47, 143, 1 ; g' #13
.DB 1<<CS01, 42, 190, 1 ; A #14
.DB 1<<CS01, 37, 251, 1 ; H #15
.DB 1<<CS01, 35, 24, 2 ; C #16
.DB 1<<CS01, 31, 93, 2 ; D #17
.DB 1<<CS00, 227, 149, 2 ; E #18
.DB 1<<CS00, 214, 189, 2 ; F #19
.DB 1<<CS00, 190, 21, 3 ; G #20
.DB 1<<CS00, 169, 120, 3 ; A' #21
.DB 1<<CS00, 151, 225, 3 ; H' #22
.DB 1<<CS00, 142, 32, 4 ; C' #23
.DB 1<<CS00, 127, 157, 4 ; D' #24
.DB 1<<CS00, 113, 47, 5 ; E' #25
.DB 1<<CS00, 106, 135, 5 ; F' #26
.DB 1<<CS00, 95, 43, 6 ; G' #27
.DB 1<<CS00, 84, 250, 6 ; A'' #28
; Stumme Achteldauer
.DB (1<<CS01)|(1<<CS00), 256, 18, 0 ; Pause #254
Unsere Tabelle hat jetzt pro Ton vier Bytes oder zwei Worte und wir können uns der Programmierung
nähern.
9.5.4 Programmablaufstruktur
Bei dieser Aufgabe muss
- bei einem Tastendruck entschieden werden, ob das Musikstück schon läuft. Wenn
nicht, wird eine Startflagge gesetzt,
- die abzuspielende Note aus der Musiktabelle gelesen werden,
- die Tonhöhe dieser Note (der Vorteiler, der CTC-Wert), aus der Notentabelle
gelesen werden und dem Timer mitgeteilt,
- die Dauer, über die die Note zu spielen ist, aus der Musiktabelle geholt werden
muss (ganze Noten, halbe Noten, Viertelnoten, Achtelnoten), umgerechnet und einem 16-Bit-Zähler
zugeführt werden,
- bei Pausen (0xFE) die Tonausgabe abgeschaltet, andernfalls eingeschaltet werden,
- das Ende des Stücks (0xFF) festgestellt werden, worauf der Timer abgeschaltet und
nachfolgende Tastendrücke wieder zuzulassen sind.
Innerhalb der Interrupt-Service-Routinen werden folgende Dinge zu erledigen sein:
- PCINT: Abfrage der Polarität des Tasteneingangs, wenn Eins beenden, wenn Null dann T-Flagge
abfragen, wenn Null dann Startflagge setzen.
- TC0-Compare-A-Int: 16-Bit-Zähler um Eins vermindern, wenn Null, dann Nullflagge setzen.
In der Hauptprogrammschleife werden beide Flaggen ausgewertet und entsprechend behandelt
(Musikstück neu starten, nächste Note auswerten).
9.5.5 Programm
Das hier ist das fertige Programm (den Quelltext gibt es hier).
;
; **************************************
; * Musikstueck abspielen mit ATtiny13 *
; * (C)2016 by www.gsc-elektronic.net *
; **************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; --------- Programmablauf ------------
;
; Beim Schliessen des Tasters wird ein PCINT
; ausgeloest und, falls das Abspielen der
; Melodie nicht schon im Gange ist, mit dem
; Einlesen und Abspielen der ersten Note der
; Melodie begonnen. Sind alle Noten und Pau-
; sen gespielt, kann von vorne begonnen wer-
; den.
;
; ------- Register ---------------------
; frei: R0 .. R14
.def rSreg = R15 ; Sichern Statusregister
.def rmp = R16 ; Vielzweckregister
.def rFlag = R17 ; Flaggenregister
.equ bStart = 0 ; Starten Musikstueck
.equ bNote = 1 ; naechste Note spielen
; frei: R18 .. R23
.def rCtrL = R24 ; 16-Bit-Zaehler CTC-Durchlaeufe
.def rCtrH = R25
; benutzt: X, XH:XL fuer Notendauerberechnung, Sichern Z
; benutzt: Y, YH:YL fuer Achteldauer
; benutzt: Z, ZH:ZL fuer Lesen aus Proigrammspeicher, Musikzeiger
;
; ------ Ports -------------------------
.equ pOut = PORTB ; Ausgabeport
.equ pDir = DDRB ; Richtungsport
.equ pInp = PINB ; Eingangsport
.equ bLspD = DDB0 ; Lautsprecherausgang
.equ bTasO = PORTB3 ; Pullup Tasteneingang
.equ bTasI = PINB3 ; Tasteninputpin
;
; --------- Timing --------------------
; Takt = 1200000 Hz
; Vorteiler = 1, 8, 64
; CTC-TOP-Bereich = 0 .. 255
; CTC-Teiler-Bereich = 1 .. 256
; Toggle-Teiler = 2
; Frequenzbereich: 600 kHz .. 36 Hz
;
; ---- Reset- und Interruptvektoren ---
.CSEG ; Assemblieren in den Code-Bereich
.ORG 0 ; An den Anfang
rjmp Start ; Rest-Vektor, Init
reti ; INT0-Int, nicht aktiv
rjmp PcIntIsr ; PCINT-Int, aktiv
reti ; TIM0_OVF, nicht aktiv
reti ; EE_RDY-Int, nicht aktiv
reti ; ANA_COMP-Int, nicht aktiv
rjmp TC0CAIsr ; TIM0_COMPA-Int, aktiv
reti ; TIM0_COMPB-Int, nicht aktiv
reti ; WDT-Int, nicht aktiv
reti ; ADC-Int, nicht aktiv
;
; ---- Reset- und Interruptvektoren ---
;
; PCINT Interrupt
; Wird von Tastenaenderungen ausgeloest. Ist
; der Tasteingang High, erfolgt nichts. Ist
; er Low, wird geprueft, ob die T-Flagge
; schon gesetzt ist. Wenn nicht, wird die
; Flagge bStart gesetzt.
;
PcIntIsr: ; PCINT Taste
in rSreg,SREG ; Status retten
sbic pInp,bTasI ; Ueberspringe bei Null
rjmp PcIntIsrRet ; Fertig
brts PcIntIsrRet ; T-Flagge gesetzt
sbr rFlag,1<<bStart ; Start-Flagge setzen
PcIntIsrRet:
out SREG,rSreg ; Status wieder herstellen
reti
;
; TC0 Compare Match A Interrupt
; Wird bei Ablauf jeder CTC-Periode ausge-
; loest.
; Vermindert den CTC-Zaehler um Eins. Er-
; reicht dieser Null wird die Flagge bNote
; gesetzt.
;
TC0CAIsr: ; Timer-CTC-A-Int
in rSreg,SREG ; Status retten
sbiw rCtrL,1 ; 16-Bit-Zaehler abwaerts
brne TC0CAIsrRet ; noch nicht Null
sbr rFlag,1<<bNote ; Flagge setzen
TC0CAIsrRet:
out SREG,rSreg ; Status wieder herstellen
reti
;
; ---- Programmstart, Init -------------
Start:
; Stapel initiieren
ldi rmp,LOW(RAMEND) ; Stapelzeiger auf Ende
out SPL,rmp
; T-Flagge loeschen
clt ; Inaktiv
; Portpins einstellen
ldi rmp,1<<bLspD ; Lautsprecherausgang Richtung
out pDir,rmp ; in Richtungsregister
ldi rmp,1<<bTasO ; Pullup am Tastenport
out pOut,rmp ; in Ausgangsregister
; Timer als CTC starten
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear, CTC-A
out TCCR0A,rmp ; in Kontrollregister A
; Vorteiler, Timerstart und Int erfolgt durch Startroutine
; PCINT fuer Tasteneingang
ldi rmp,1<<PCINT3 ; PB3-Int ermoeglichen
out PCMSK,rmp ; in PCINT-Maskenregister
ldi rmp,1<<PCIE ; PCINT ermoeglichen
out GIMSK,rmp ; in Interrupt-Maskenregister
; Schlafen ermoeglichen
ldi rmp,1<<SE ; Schlafen, Idle-Modus
out MCUCR,rmp ; in MCU-Kontrollregister
; Interrupts einschalten
sei
; ---- Hauptprogramm-Schleife -----------
Schleife:
sleep ; schlafen legen
nop ; Aufwachen
sbrc rFlag,bStart ; Ueberspringe wenn Start-Flagge Null
rcall Musikstart ; Musikausgabe starten
sbrc rFlag,bNote ; Note ausgeben
rcall NoteAus ; Note ausgeben
rjmp Schleife ; wieder schlafen legen
;
; ----- Behandlungsroutinen -------------
;
; Musikstart: wird von der gesetzten Flagge bStart
; ausgeloest. Startet die Melodieausgabe mit der
; ersten Note.
;
Musikstart: ; Ausgabe Musikstueck starten
cbr rFlag,1<<bStart ; Flagge loeschen
set ; setze T-Flagge
ldi ZH,HIGH(2*Melodie) ; Zeiger auf Musikstueck
ldi ZL,LOW(2*Melodie)
rcall NoteSpielen ; gib die Note aus
ldi rmp,1<<OCIE0A ; Interrupts einschalten
out TIMSK0,rmp ; in Interrupt Maske
ret
;
; NoteAus: wird von der gesetzten Flagge bNote
; ausgeloest. Gibt die naechste Note aus
;
NoteAus: ; naechste Note ausgeben
cbr rFlag,1<<bNote ; Flagge loeschen
rcall NoteSpielen ; naechste Note ausgeben
ret
;
; Notespielen:
; Wird von den Flaggen bStart und bNote aufgerufen
; und spielt die Note, auf die das Doppelregister
; Z zeigt.
; Ist das Ende der Melodie erreicht (Note = 0xFF),
; wird das T-Flag geloescht, der TC0-Interrupt aus-
; und der Lautsprecherausgang stumm geschaltet.
; Sind noch Noten und Pausen zu spielen, wird
; a) die zu spielende Notennummer und
; b) die Dauer in Achtelnotendauern gelesen.
; Ist die Notennummer eine Pause (Note = 0xFE),
; wird die Tonausgabe stumm geschaltet.Sonst
; wird die Lautsprecherausgabe eingeschaltet und
; fuer die Note die Noteneinstellungen aus der
; Notentabelle gelesen und an den Timer ausge-
; geben. Die Dauer der Note bzw. der Pause
; werden in die Anzahl CTC-Zyklen umgerechnet
; und in das Doppelregister R25:R24 geschrieben.
;
NoteSpielen: ; Spiele die Note auf die Z zeigt
lpm rmp,Z+ ; lese Note
cpi rmp,0xFF ; stelle Tabellenende fest
brne NoteSpielen1 ; Nicht Ende
; Musikstueck ist zu Ende
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear, CTC-A
out TCCR0A,rmp ; in Kontrollregister A
clr rmp ; Null
out TIMSK0,rmp ; TC0-Int abschalten
pop rmp ; entferne Ruecksprungadresse vom Stapel
pop rmp
clt ; T-Flagge loeschen
ret
NoteSpielen1: ; Nicht zu Ende
cpi rmp,0xFE ; Pause?
brne NoteSpielen2
; Pause, stumm LSP schalten
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear, CTC-A
out TCCR0A,rmp ; in Kontrollregister A
ldi rmp,(1<<CS01)|(1<<CS00) ; Vorteiler = 64
out TCCR0B,rmp ; in Kontrollregister B
ldi rmp,255 ; CTC auf Hoechstwert
ldi rCtrL,18 ; Zaehler auf Achteldauer
ldi rCtrH,0
lpm R16,Z+ ; Dauerbyte ueberlesen
reti
NoteSpielen2: ; Normale Note
mov XH,ZH ; Musikzeiger sichern
mov XL,ZL
ldi ZH,HIGH(2*Notentabelle_Dauer) ; Zeiger auf Tontabelle
ldi ZL,LOW(2*Notentabelle_Dauer)
lsl rmp ; mal zwei
lsl rmp ; mal vier
add ZL,rmp ; zum Zeiger addieren
ldi rmp,0
adc ZH,rmp
lpm rmp,Z+ ; lese Vorteiler
out TCCR0B,rmp ; in Timer
lpm rmp,Z+ ; lese CTC-Wert
out OCR0A,rmp ; in Vergleicher A
lpm YL,Z+ ; lese Achtel-Dauer nach Y
lpm YH,Z+
mov ZH,XH ; Zeiger auf Musik wieder herstellen
mov ZL,XL
lpm rmp,Z+ ; Lese Laengen-Byte
mov XH,YH ; Einfache Achtel-Laenge
mov XL,YL
NoteSpielen3:
dec rmp ; Laengen-Byte abwaerts
breq NoteSpielen4 ; fertig
add XL,YL ; Achteldauer addieren
adc XH,YH
rjmp NoteSpielen3
NoteSpielen4:
mov rCtrL,XL ; in Zaehler
mov rCtrH,XH
ldi rmp,(1<<COM0A0)|(1<<WGM01) ; Toggle, CTC-A
out TCCR0A,rmp ; in Kontrollregister A
ret
;
; Notentabelle mit Dauer
Notentabelle_Dauer:
.DB 1<<CS01, 169, 111, 0 ; a #0
.DB 1<<CS01, 151, 124, 0 ; h #1
.DB 1<<CS01, 142, 132, 0 ; c #2
.DB 1<<CS01, 127, 148, 0 ; d #3
.DB 1<<CS01, 113, 166, 0 ; e #4
.DB 1<<CS01, 106, 177, 0 ; f #5
.DB 1<<CS01, 95, 197, 0 ; g #6
.DB 1<<CS01, 84, 223, 0 ; a' #7
.DB 1<<CS01, 75, 250, 0 ; h' #8
.DB 1<<CS01, 71, 8, 1 ; c' #9
.DB 1<<CS01, 63, 42, 1 ; d' #10
.DB 1<<CS01, 56, 79, 1 ; e' #11
.DB 1<<CS01, 53, 98, 1 ; f' #12
.DB 1<<CS01, 47, 143, 1 ; g' #13
.DB 1<<CS01, 42, 190, 1 ; A #14
.DB 1<<CS01, 37, 251, 1 ; H #15
.DB 1<<CS01, 35, 24, 2 ; C #16
.DB 1<<CS01, 31, 93, 2 ; D #17
.DB 1<<CS00, 227, 149, 2 ; E #18
.DB 1<<CS00, 214, 189, 2 ; F #19
.DB 1<<CS00, 190, 21, 3 ; G #20
.DB 1<<CS00, 169, 120, 3 ; A' #21
.DB 1<<CS00, 151, 225, 3 ; H' #22
.DB 1<<CS00, 142, 32, 4 ; C' #23
.DB 1<<CS00, 127, 157, 4 ; D' #24
.DB 1<<CS00, 113, 47, 5 ; E' #25
.DB 1<<CS00, 106, 135, 5 ; F' #26
.DB 1<<CS00, 95, 43, 6 ; G' #27
.DB 1<<CS00, 84, 250, 6 ; A'' #28
;
; ---- Notensymbole ----------
; Notensymbole
.equ na0 = 0
.equ nh0 = 1
.equ nc0 = 2
.equ nd0 = 3
.equ ne0 = 4
.equ nf0 = 5
.equ ng0 = 6
.equ na1 = 7
.equ nh1 = 8
.equ nc1 = 9
.equ nd1 = 10
.equ ne1 = 11
.equ nf1 = 12
.equ ng1 = 13
.equ nA2 = 14
.equ nH2 = 15
.equ nC2 = 16
.equ nD2 = 17
.equ nE2 = 18
.equ nF2 = 19
.equ nG2 = 20
.equ nA3 = 21
.equ nH3 = 22
.equ nC3 = 23
.equ nD3 = 24
.equ nE3 = 25
.equ nF3 = 26
.equ nG3 = 27
.equ nA4 = 28
;
; ---- Melodie -----------------
Melodie: ; LSB: Note oder FF, MSB: Dauer in Achtel
; Völ- ker hört die
.db ne1,3,nd1,2,0xFE,1,nc1,4,0xFE,1,ng0,3,0xFE,1
; Sig- na- le!
.db ne0,1,0xFE,1,na1,4,0xFE,1,nf0,2,0xFF,0xFF
;
;
; Ende Quellcode
;
Im Quelltext gibt es keine neuen Instruktionen.
9.5.6 Simulieren der Programmausführung
Simulation verwendet avr_sim
um die Ausführung der ersten beiden Noten der Melodie zu verifizieren.
Die erste Note der Melodie,
nämlich Note ne1 mit
; ---- Notensymbole ----------
.equ ne1 = 11
und
Notentabelle_Dauer:
.DB 1<<CS01, 56, 79, 1 ; e' #11
ist in den Timer TC0 geladen. Der Ausgang PB0 mit dem Lautsprecher
torkelt. TC0 ist als CTC mit Compare-Match A als TOP-Wert
konfiguriert, der auf 56 eingestellt ist. Der Vorteiler steht auf
acht und die CTC-Zeit bzw. die Schwingfrequenz beträgt
tCTC = 8 * (56 + 1) / 1,2 = 380 µs
f = 1.200.000 / 8 / (56 + 1) / 2 = 1.315,8 Hz
Die Dauer der Note in Achteln ist in der Tabelle mit 73 und 1
definiert. Das bedeutet 1 * 256 + 73 = 329 CTC-Zyklen. Die
"3", die in der Melodietabelle auf die Note ne1 folgt,
sagt: Dauer = 3 Achtel, also werden die 329 mit drei malgenommen.
Das ergibt 987 CTC-Zyklen Dauer oder eine Dauer von
t3/8 = 987 * 380 µs = 375 ms.
Das entspricht auffällig genau 3/8 Sekunden.
Die Dauer, 987 oder 0x03DB, steht im Doppelregister R25:R24 und wird
von der TC0-Interrupt-Service-Routine bei jedem Compare-Match A
abwärts gezählt.
Z zeigt auf die nächste Note der Melodie im Flashspeicher bei
Adresse 0x00A8.
Dies ist die Zeit, wenn das Registerpaar R25:R24 Null erreicht und
die nächste Note gespielt werden soll. Die 379 ms sind
fast genau.
Die zweite Note der Melodie wird jetzt in den TC0-Timer geladen.
Der Vorteiler ist wieder 8, der Compare-Match-A-Wert ist nun 63.
Das entspricht
f2 = 1.200.000 / 8 / (63 + 1) / 2 = 1.171 Hz
und liegt nahe genug bei den 1.173 Hz von Note nd1. Die
Dauer der Note ist jetzt auf zwei Achtel eingestellt:
t = 2 * 8 * (63 + 1) * (1 * 256 + 42) / 1.200.000 = 254,3 ms
Der Zählerwert in R25:R24 ist ok, das Doppelregister Z zeigt
schon auf die dritte Note (eine Pause).
Wie vorherberechnet sind 250 ms am Ende der zweiten Note vergangen.
Die dritte Note ist eine Pause (Code: 0xFE). Der einzige Unterschied
zwischen einer Note und einer Pause ist, dass der Ausgang OC0A nicht auf
Torkeln sondern auf Clear eingestellt wird, was den Ausgang auf Dauer-Null
einstellt.
Simulation ist ein wertvolles Werkzeug, mit dem selbst komplexe Abläufe
anschaulich beobachtet und analysiert werden können. Hier wurden
z. B. Zeiten genau bestimmt und verschachtelte Tabellenzugriffe
(Musikstück, Notentabelle) im Einzelnen nachvollzogen.
Das Tool ersetzt mühelos teure Debug-Werkzeuge, die auch nicht
viel mehr tun als das kostenlose
avr_sim.
©2016-2019 by http://www.gsc-elektronic.net