Home ==> Mikrobeginner ==> 9. Tongenerator
ATtiny13

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

  1. Einführung in die Tonerzeugung
  2. Hardware, Bauteile, Aufbau
  3. Tonhöhenregelung
  4. Einführung in die Tabellenprogrammierung
  5. Einführung in die Multiplikation
  6. Tonleiterausgabe
  7. Musikstück

9.1 Einführung in die Tonerzeugung

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.

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


9.2 Hardware, Bauteile und Aufbau

9.2.1 Die Schaltung

Schaltbild 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

LautsprecherDas 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

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

Aufbau Der Anschluss des Lautsprechers erfolgt an Pin 5 über den Elko.

Damit kann es mit den Tönen losgehen.


Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


9.3 Tonhöhenregelung

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:
TaktVor-
teiler
OCR0A
=0
OCR0A
=255
9,6 MHz14,8 MHz18,75 kHz
8600 kHz2,34 kHz
6475 kHz292,5 Hz
25618,75 kHz73,1 Hz
10244,69 kHz18,3 Hz
1,2 MHz1600 kHz2,34 kHz
875 kHz292,5 Hz
649,38 kHz36,6 Hz
2562,35 kHz9,15 Hz
1024586 Hz2,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.

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


9.3.4 Simulation des Programmes

Die Simulation erfolgt mit
avr_sim wie folgt.

Ton 1 Port init 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.

Ton 1 TC0 Init 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.

Ton 1 ADC Init 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.

Ton 1 ADC hat zu tun 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.

Ton 1 ADC Lesen Ergebnis 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.

Ton 1 COM Die Umkehr der Bits des Ergebnisses mit der Instruktion com rimp liefert 0xFF - 0x22 = 0xDD oder dezimal 221.

Ton 1 Ergebnis nach OCR0A 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.

Ton 1 PCINT-Anforderung 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).

Ton 1 PB0 torkelt 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.

Ton 1 PB0 torkelt jetzt 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.

Ton 1 PB0 ist Eins 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.

Ton 1 CTC Timing 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).

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


9.4 Aufgabe 2: Die Tonleiter

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.

Notentabelle

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:
  1. 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.
  2. das EEPROM. Es umfasst ebenfalls 64 Bytes. Es würde passen, wäre aber recht gefüllt.
  3. 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.3.1 Tabelle im SRAM

Im ersten Fall, der Ablage im SRAM-Speicher, gibt es keine andere Möglichkeit, unsere Tabellenwerte hineinzuschreiben als jeden Wert einzeln mittels der Instruktion "STS Adresse,Register" dort abzulegen. Dazu müssten wir folgendes programmieren:

	ldi R16,8 ; Prescaler-Wert
	sts 0x60,R16 ; speichere an Adresse 0x0060 im SRAM
	ldi R16,170 ; CTC-Wert
	sts 0x60+1,R16 ; speichere an Adresse 0x0061 im SRAM
	[...]
	ldi R16,1 ; Prescaler-Wert
	sts 0x60+56,R16 ; speichere an Adresse 0x0098 im SRAM
	ldi R16,85 ; CTC-Wert
	sts 0x61,R16 ; speichere an Adresse 0x0099 im SRAM

Jedes Wertepaar würde vier Instruktionen benötigen, von denen zwei (STS) auch noch Zwei-Wort-Instruktionen sind (sechs Worte pro Paar). Selbst wenn wir diese Instruktionen etwas vereinfachen, indem wir die Instruktion "ST Z+,Register" verwenden, wäre auch das mühsam. ST Z+ geht folgendermaßen:

	ldi ZH,HIGH(0x0060) ; Zeiger Z auf SRAM-Startadresse, MSB
	ldi ZL,LOW(0x0060) ; dto., LSB
	ldi R16,8 ; Prescaler-Wert
	st Z+,R16 ; speichern R16 im SRAM und Adresse in Z erhoehen
	ldi R16,170 ; CTC-Wert
	st Z+,R16 ; an naechste Adresse
	[...]
	ldi R16,1 ; Prescaler-Wert
	st Z+,R16 ; speichere R16 im SRAM und Adresse in Z erhoehen
	ldi R16,85 ; CTC-Wert
	st Z,R16 ; speichere R16 an letzter Adresse im SRAM

Das Speichern mit automatischer Adresserhöhung "ST Z+,Register" wird beim letzten zu schreibenden Wert geändert in "ST Z,Register", die Adresse in Z wird dann nicht mehr erhöht.

Das Umgekehrte, nämlich Adresse vermindern, gibt es auch, aber etwas anders. Mit "ST -Z,Register" vermindert sich die Adresse zuerst und das Schreiben erfolgt an die schon erniedrigte Adresse. Was sich norwegische Sprachkonstrukteure so alles ausdenken, um das Leben von Anfängern ein wenig bunter zu machen.

Mal abgesehen von der langweiligen Tipperei ist auch diese Lösung alles andere als elegant und praktikabel. Das SRAM ist einfach nicht der richtige Ort, um so was wie eine lange Tabelle komfortabel abzulegen.

9.4.3.2 Tabelle im EEPROM

Der zweite Ort, die Tabelle abzulegen, das EEPROM, bietet schon etwas komfortablere Bedingungen. Hier lautet die Konstruktion:

.ESEG
.ORG 0
.db 8,170
[...]
.db 1,85
.CSEG
Die Assemblerdirektive ".ESEG" bewirkt, dass der Assembler das nachfolgende im EEPROM-Segment ablegt, nicht im Programmspeicher. Ab jetzt werden alle Inhalte separat gehandhabt und in eine Datei mit der Endung ".eep" geschrieben. Der Inhalt dieser Datei kann mit der Brennsoftware direkt in das EEPROM des Chips geschrieben werden. Wenn die Tabelle irgendwo zwischen dem Code steht, wird mit ".CSEG" abschließend wieder auf Programmcode-Erzeugung umgeschaltet.

Das ".ORG 0"bewirkt, dass an der EEPROM-Adresse 0 mit der Tabelle begonnen wird.

Die einzelnen ".DB"-Direktiven bewirken, dass die mit Kommata getrennten folgenden Zahlen nacheinander im EEPROM an die nachfolgenden Adressen abgelegt werden. Texte kann man mit der Direktive .DB "Text" in das EEPROM ablegen. Gespeichert werden die ASCII-Codes des Textes, Buchstabe für Buchstabe.

Wie wir an die Werte im EEPROM wieder herankommen, kommt erst in einer späteren Lektion dran.

Das ist schon wesentlich komfortabler, aber immer noch nicht so elegant wie das Folgende.

9.4.3.3 Tabelle im Programmspeicher

Jetzt wird es etwas komplizierter, weil der Programmspeicher ja 16-bittig ausgelegt ist und ganze Worte speichert. Mit

Tabelle:
.db 8,170
[...]
.db 1,85

passiert jetzt Folgendes: Es ist klar, dass pro ".DB" immer ganze Worte abgelegt werden. Schreibt man also ".DB 1", wird effektiv 0x0001 abgelegt. Da die Anzahl abzulegender Bytes in diesem Fall nur Eins und daher ungerade ist, quittiert das der Assembler mit einer Warnung, er habe ein weiteres Byte (0x00) hinzugefügt, um eine geradzahlige Anzahl an Bytes zu erhalten. Grundsätzlich gilt, dass zuerst das LSB, dann das MSB befüllt wird.

Die gleiche Warnung resultiert, wenn wir den Text "ABC" mit .DB "ABC" im Flashspeicher ablegen wollen. Auch hier wird noch ein Nullbyte angefügt, wenn wir nicht .DB "ABC_" hinschreiben.

Es gibt noch eine zweite Möglichkeit, um die Tabelle zu füllen:

Tabelle:
.dw 8+170*256
[...]
.dw 1+85*256

Das erzeugt direkt Worte und legt sie in der Tabelle ab. Hier haben wir es in der Hand darüber zu bestimmen, was LSB und was MSB wird.

Wie kriegen wir nun die so erzeugte Tabelle wieder ausgelesen? Um das erste Byte zu lesen, formulieren wir Folgendes:

	ldi ZH,HIGH(2*Tabelle) ; LSB-Zeiger in Z
	ldi ZL,LOW(2*Tabelle)
	lpm ; Load from Program Memory

Das gelesene LSB, in unserem Fall die 8, kommt nun in das Register R0. Um es woandershin zu laden, schreiben wir

	ldi ZH,HIGH(2*Tabelle) ; LSB-Zeiger in Z
	ldi ZL,LOW(2*Tabelle)
	lpm R16,Z ; Load from Program Memory nach R16

Um das MSB zu lesen, könnten wir formulieren:

	ldi ZH,HIGH(2*Tabelle+1) ; MSB-Zeiger in Z
	ldi ZL,LOW(2*Tabelle+1)
	lpm R16,Z ; Load from Program Memory nach R16

Damit ist auch klar, weshalb das Label Tabelle mit zwei malgenommen werden muss: das unterste Bit der Leseadresse dient der LSB/MSB-Auswahl.

Um beide (oder mehr) Bytes nacheinander zu lesen, kann man

	ldi ZH,HIGH(2*Tabelle) ; MSB-Zeiger in Z
	ldi ZL,LOW(2*Tabelle)
	lpm XL,Z+ ; Load LSB from Program Memory nach XL
	lpm XH,Z+ ; Load MSB from Program Memory nach XH

schreiben. Das erhöht die Adresse im Zeigerpaar Z automatisch nach dem Lesen und zeigt schon mal auf das nächste Byte. Rückwärts geht es auch, mit "lpm -Z,Register", aber nicht beim ATtiny13. XL und XH sind die beiden Einzelregister des Zeigerpaares X. XL, XH, YL, YH, ZL und ZH sind übrigens in der Datei "*def.inc" definiert, weshalb bei der Verwendung ohne diese Typdefinition in der def.inc eine Fehlermeldung resultiert. Das hat historische Gründe, weil die ersten AVR-Typen noch gar keine Zeigerregister hatten.

9.4.4 Notentabelle programmieren

Damit ist unsere Notentabelle klar: hier ist sie.

Notentabelle:
.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

Die Tabelle nimmt jetzt 29 Worte im Flash-Speicher ein. Das sind 5,7 % des Speichers und ist verträglich.

Wenn wir jetzt die beiden Bytes der zehnten Note holen und in den Timer schreiben wollen, lautet der Code dafür so:

	ldi R16,10 ; zehnte Note
	lsl R16 ; Note mal zwei (2 Bytes pro Note)
	ldi ZH,HIGH(2*Notentabelle)
	ldi ZL,LOW(2*Notentabelle)
	add ZL,R16 ; addiere Note zu Zeiger
	ldi R16,0 ; CLR wuerde Carry-Flagge loeschen
	adc ZH,R16 ; addiere eventuelles Carry
	lpm R0,Z+ ; lese Vorteiler
	out TCCR0B,R0 ; schreibe in Timer-Kontrollport B
	lpm R0,Z ; lese CTC-Wert
	out OCR0A,R0 ; schreibe in CTC-Vergleichswert
Damit wäre das Auslesen und Verwenden der Notentabelle für unseren Fall gelöst.

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


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.

Dezimalmultiplikation 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.

Binärmultiplikation

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.

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


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.

Debug-Menue 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.

Debug-Start 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.

Einzelschritt 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.

Breakpoints 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.

Ende der Multiplikation 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.

Timer-Register 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.

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


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.

Ton 2 Port Init 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.

Ton 2 ADC-Lauf 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.

Ton 2 Verzweigen zur Multiplikation 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.

Ton 2: Erstes Schieben ins Carry 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.

Ton 2: Erstes Addieren zum Ergebnis 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.

Ton 2: Erstes Multiplizieren mit 2 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.

Ton 2: Zweites Schieben 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.

Ton 2: Zweite Multiplikation mit 2 Dann wird R3:R2 erneut mit Shift Left und Rotate Left mit zwei malgenommen.

Ton 2: Drittes Schieben 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.

Ton 2: Drittes Addieren 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.

Ton 2: Viertes Schieben Es wird langsam langweilig, aber rmp muss noch mal rechts geschoben werden. Weil dabei eine Eins herausrollt muss wieder addiert werden.

Ton 2: Viertes Addieren Das nächste Addieren von R3:R2 zu R1:R0 erfolgt hier.

Ton 2: Vierte Multiplikation mit 2 Und die nächste Multiplikation mit 2 für R3:R2 hier.

Ton 2: Fünftes Schieben Nun rollt die letzte Eins in das Carry-Flag und wir sind fast fertig mit der Multiplikation.

Ton 2: Fünftes Addieren 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.

Ton 2: Dauer der Multiplikation 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!

Ton 2: Lade Notentabelle in Z 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.

Ton 2: Addieren zur Tabellenaddresse 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.)

Ton 2: Erstes LPM aus der Notentabelle 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.

Ton 2: Zweites LPM aus Notentabelle 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.


Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


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

Melodie Das hier ist zu spielen.

Noten 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

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

Pausen 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 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 Innerhalb der Interrupt-Service-Routinen werden folgende Dinge zu erledigen sein: 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.

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


9.5.6 Simulieren der Programmausführung

Simulation verwendet avr_sim um die Ausführung der ersten beiden Noten der Melodie zu verifizieren.

Ton 3: TC0 beginnt mit der ersten Note 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.

Ton 3: Erste Note, Register 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.

Ton 3: Ende der ersten Note 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.

Ton 3: Beginn der zweiten Note 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




Ton 3: Note 2 Registerstand Der Zählerwert in R25:R24 ist ok, das Doppelregister Z zeigt schon auf die dritte Note (eine Pause).

Ton 3: Note 2 ist vorbei 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.

Home Top Töne Hardware Tonregelung Tonleiter Multiplikation Tonleiter Musik


©2016-2019 by http://www.gsc-elektronic.net