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 Die Bauteile

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.

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.3 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 Lausprecher 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

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.

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
;
; --------- 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 -----
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
;
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.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 Einführung in die Tabellenprogrammierung

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.
TonHzPrescCTCIst(Hz)Delta(%)#
a4408170441,180,27%0
h4958152493,42-0,32%1
cis5508136551,470,27%2
d586,668128585,94-0,12%3
e6608114657,89-0,32%4
fis733,338102735,290,27%5
gis825891824,18-0,10%6
a'880885882,350,27%7
h'990876986,84-0,32%8
cis'11008681102,940,27%9
d'1173,328641171,88-0,12%10
e'13208571315,79-0,32%11
fis'1466,668511470,590,27%12
gis'16508451666,671,01%13
A17608431744,19-0,90%14
H19808381973,68-0,32%15
CIS22008342205,880,27%16
D2346,648322343,75-0,12%17
E264012272643,170,12%18
FIS2933,3212052926,82-0,22%19
GIS330011823296,70-0,10%20
A'352011703529,410,27%21
H'396011523947,36-0,32%22
CIS'440011364411,760,27%23
D'4693,2811284687,5-0,12%24
E'528011145263,15-0,32%25
FIS'5866,6411025882,350,27%26
GIS'66001916593,40-0,10%27
A''70401857058,820,27%28
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.

Tabellen und ihre Platzierung

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

Damit ist unsere Notentabelle klar: hier ist sie.

Notentabelle:
.db 1<<CS01, 169 ; a #0
.db 1<<CS01, 151 ; h #1
.db 1<<CS01, 135 ; cis #2
.db 1<<CS01, 127 ; d #3
.db 1<<CS01, 113 ; e #4
.db 1<<CS01, 101 ; fis #5
.db 1<<CS01, 90 ; gis #6
.db 1<<CS01, 84 ; a' #7
.db 1<<CS01, 75 ; h' #8
.db 1<<CS01, 67 ; cis' #9
.db 1<<CS01, 63 ; d' #10
.db 1<<CS01, 56 ; e' #11
.db 1<<CS01, 50 ; fis' #12
.db 1<<CS01, 44 ; gis' #13
.db 1<<CS01, 42 ; A #14
.db 1<<CS01, 37 ; H #15
.db 1<<CS01, 33 ; CIS #16
.db 1<<CS01, 31 ; D #17
.db 1<<CS00, 226 ; E #18
.db 1<<CS00, 204 ; FIS #19
.db 1<<CS00, 181 ; GIS #20
.db 1<<CS00, 169 ; A' #21
.db 1<<CS00, 151 ; H' #22
.db 1<<CS00, 135 ; CIS' #23
.db 1<<CS00, 127 ; D' #24
.db 1<<CS00, 113 ; E' #25
.db 1<<CS00, 101 ; FIS' #26
.db 1<<CS00, 90 ; GIS' #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 TCCR0A,R0 ; schreibe in Timer-Kontrollport
	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.3 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.

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.

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.

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.4 Die Tonleiter programmieren

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
;
; --------- 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 -----
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
;
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
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, 135 ; cis #2
.db 1<<CS01, 127 ; d #3
.db 1<<CS01, 113 ; e #4
.db 1<<CS01, 101 ; fis #5
.db 1<<CS01, 90 ; gis #6
.db 1<<CS01, 84 ; a' #7
.db 1<<CS01, 75 ; h' #8
.db 1<<CS01, 67 ; cis' #9
.db 1<<CS01, 63 ; d' #10
.db 1<<CS01, 56 ; e' #11
.db 1<<CS01, 50 ; fis' #12
.db 1<<CS01, 44 ; gis' #13
.db 1<<CS01, 42 ; A #14
.db 1<<CS01, 37 ; H #15
.db 1<<CS01, 33 ; CIS #16
.db 1<<CS01, 31 ; D #17
.db 1<<CS00, 226 ; E #18
.db 1<<CS00, 204 ; FIS #19
.db 1<<CS00, 181 ; GIS #20
.db 1<<CS00, 169 ; A' #21
.db 1<<CS00, 151 ; H' #22
.db 1<<CS00, 135 ; CIS' #23
.db 1<<CS00, 127 ; D' #24
.db 1<<CS00, 113 ; E' #25
.db 1<<CS00, 101 ; FIS' #26
.db 1<<CS00, 90 ; GIS' #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.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, 110, 0 ; a #0
.DB 1<<CS01, 151, 123, 0 ; h #1
.DB 1<<CS01, 135, 138, 0 ; cis #2
.DB 1<<CS01, 127, 146, 0 ; d #3
.DB 1<<CS01, 113, 164, 0 ; e #4
.DB 1<<CS01, 101, 184, 0 ; fis #5
.DB 1<<CS01, 90, 206, 0 ; gis #6
.DB 1<<CS01, 84, 221, 0 ; a' #7
.DB 1<<CS01, 75, 247, 0 ; h' #8
.DB 1<<CS01, 67, 20, 1 ; cis' #9
.DB 1<<CS01, 63, 37, 1 ; d' #10
.DB 1<<CS01, 56, 73, 1 ; e' #11
.DB 1<<CS01, 50, 112, 1 ; fis' #12
.DB 1<<CS01, 44, 161, 1 ; gis' #13
.DB 1<<CS01, 42, 180, 1 ; A #14
.DB 1<<CS01, 37, 237, 1 ; H #15
.DB 1<<CS01, 33, 39, 2 ; CIS #16
.DB 1<<CS01, 31, 74, 2 ; D #17
.DB 1<<CS00, 226, 149, 2 ; E #18
.DB 1<<CS00, 204, 220, 2 ; FIS #19
.DB 1<<CS00, 181, 56, 3 ; GIS #20
.DB 1<<CS00, 169, 114, 3 ; A' #21
.DB 1<<CS00, 151, 219, 3 ; H' #22
.DB 1<<CS00, 135, 79, 4 ; CIS' #23
.DB 1<<CS00, 127, 148, 4 ; D' #24
.DB 1<<CS00, 113, 36, 5 ; E' #25
.DB 1<<CS00, 101, 191, 5 ; FIS' #26
.DB 1<<CS00, 90, 112, 6 ; GIS' #27
.DB 1<<CS00, 84, 229, 6 ; A'' #28
; Stumme Achteldauer
; .DB (1<<CS01)|(1<<CS00), 255, 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
;
; ------- 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 ---
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
;
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: ; 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: ; naechste Note ausgeben
	cbr rFlag,1<<bNote ; Flagge loeschen
	rcall NoteSpielen ; naechste Note ausgeben
	ret
;
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, 110, 0 ; a #0
.DB 1<<CS01, 151, 123, 0 ; h #1
.DB 1<<CS01, 135, 138, 0 ; cis #2
.DB 1<<CS01, 127, 146, 0 ; d #3
.DB 1<<CS01, 113, 164, 0 ; e #4
.DB 1<<CS01, 101, 184, 0 ; fis #5
.DB 1<<CS01, 90, 206, 0 ; gis #6
.DB 1<<CS01, 84, 221, 0 ; a' #7
.DB 1<<CS01, 75, 247, 0 ; h' #8
.DB 1<<CS01, 67, 20, 1 ; cis' #9
.DB 1<<CS01, 63, 37, 1 ; d' #10
.DB 1<<CS01, 56, 73, 1 ; e' #11
.DB 1<<CS01, 50, 112, 1 ; fis' #12
.DB 1<<CS01, 44, 161, 1 ; gis' #13
.DB 1<<CS01, 42, 180, 1 ; A #14
.DB 1<<CS01, 37, 237, 1 ; H #15
.DB 1<<CS01, 33, 39, 2 ; CIS #16
.DB 1<<CS01, 31, 74, 2 ; D #17
.DB 1<<CS00, 226, 149, 2 ; E #18
.DB 1<<CS00, 204, 220, 2 ; FIS #19
.DB 1<<CS00, 181, 56, 3 ; GIS #20
.DB 1<<CS00, 169, 114, 3 ; A' #21
.DB 1<<CS00, 151, 219, 3 ; H' #22
.DB 1<<CS00, 135, 79, 4 ; CIS' #23
.DB 1<<CS00, 127, 148, 4 ; D' #24
.DB 1<<CS00, 113, 36, 5 ; E' #25
.DB 1<<CS00, 101, 191, 5 ; FIS' #26
.DB 1<<CS00, 90, 112, 6 ; GIS' #27
.DB 1<<CS00, 84, 229, 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


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