Ein einfacher Melodiegenerator mit Tiny15 in Assembler...


Im Mikrocontroller-Forum wurde die Frage nach der Realisierung eines Türklingel-Melodiegenerators gestellt. Dies sollte zwar in BASCOM und mit dem Tiny12 geschehen, aber in Assembler und mit dem Tiny15 macht sich das besser. Klar, es handelt sich um einen Melodiegenerator, der lediglich eine Folge von Piepstönen erzeugt, also kein MP3 oder WAV oder Hüllkurven, ja nichtmal unterschiedliche Lautstärke, nur Pieps in definierter Tonhöhe, Pause oder nix...

Natürlich kann man auch einen Soundrecorder-Chip oder gar einen MP3-Player als Türklingel benutzen, die sind inzwischen recht billig zu haben. Aber hier geht es um die Programmierung, nicht um das Endergebnis.


Wie geht man nun an die Lösung des Problems heran?
Was braucht man eigentlich für einen solchen Melodiegenerator?

Zuerst zum Tongenerator:

Der Tiny15 bietet die Möglichkeit, mittels der Output-Compare-Funktionalität des Timer1 einen Portpin toggeln zu lassen bzw. eine PWM zu erzeugen. Die PWM nützt hier nicht viel, aber das Pin-Toggeln ist hier sehr brauchbar.
Um dieses zu erreichen, müssen bestimmte Steuerbits im Steuerregister TCCR1 gesetzt werden.
Datenblatt Tiny15 Seite 29
Nun sind alle Bits in TCCR1 geklärt.
Um nun die Tonerzeugung einzuschalten, ist das oben ermittelte Bitmuster in das Steuerregister TCCR1 zu schreiben. Um den Ton wieder auszuschalten, schreibt man 0 in das Steuerregister. Um nun nicht immer die Konstanten in ein Hilfsregister laden zu müssen, werden dafür zwei Register reserviert. Da mit diesen Register keine weitere Operationen durchgeführt werden müssen, genügen "untere Register", also soche, die keine Operationen mit Konstanten können.
Als Programmcode sieht das erstmal so aus:
.equ sndein=(1<<ctc1)|(1<<com1a0)+7 ;siehe Datenblatt Seite 29..30)
.equ sndaus=0               ;Bitmuster: Timer1 ausschalten

.def tonein=r7              ;Wert zum Timer1 einschalten
.def tonaus=r8              ;Wert zum Timer1 ausschalten

 ldi wl,sndein              ;Einschalt-Bitmuster Timer1 (für tccr1)
 mov tonein,wl              ;zuweisen
 ldi wl,sndaus              ;Ausschalt-Bitmuster
 mov tonaus,wl              ;zuweisen
Der Timer kann nun an jeder Stelle des Programms durch
 out tccr1,tonein
eingeschaltet werden und durch
 out tccr1,tonaus
wieder aus. Dabei wird am Pin OC1A die Tonfrequenz erzeugt, die dem Inhalt des Registers OCR1A entspricht. Da das Löschen des Timers (TCNT1) und das Toggeln des Pins OCR1 automatisch durch Hardware geschieht, muss nichtmal ein Interrupt ausgelöst werden. Demzufolge wird für Timer1 auch keine ISR (Interrupt-Service-Routine) erforderlich.

Timer0

Timer0 soll einen Interrupt erzeugen, der alle 10ms (also mit 100Hz) die ISR aufruft. 10ms deshalb, weil in diesem Zeitraster die Tastenabfrage und Tondauersteuerung optimal funktioniert.

Wie überredet man nun Timer0 dazu, alle 10ms einen Interrupt austzulösen? Timer0 ist recht einfach gehalten und hat nur den Überlauf-Interrupt, der dann auftritt, wenn der Timer von 255 auf 0 springt. Da der Tiny15 mit 1,6MHz taktet, die ISR des Timer0 mit 100Hz takten soll, muss der Timer0 alle 16000 Takte (1600000Hz / 100Hz) überlaufen. Da der eigentliche Zählumfang nur 256 (von 0 bis 255) beträgt, muss ein Vorteiler vorgeschaltet werden. Dies wird erreicht, indem man in das Steuerregister des Timer0 TCCR0:

TCCR0 (Datenblatt Seite 27)

den Wert hineinschreibt, den man der Tabelle 9 des Datenblatts:

Vorteiler Timer0 (Datenblatt Seite 27)

entnehmen kann. Teilt man nun die benötigten 16000 Takte durch den max. Zählumfang von 256, dann erhält man 62,5. Der nächstliegende Vorteiler in Tabelle 9 beträgt 64. Teilt man die 16000 Takte nun durch den Vorteiler von 64, dann erhält man den erforderlichen Zählumfang des Timer0 von 250 (vorgeteilten) "Ticks".

Um nun den Vorteiler 64 einzustellen, ist der Wert 3 in das Register TCCR0 zu schreiben. Dies wird in der Initialisierung erledigt, denn der Timer0 soll ja ständig durchlaufen und die ISR zyklisch aufrufen. Da nun Timer0 nur aufwärts zählen kann, muss der Startwert des Timers manipuliert werden. Für der Umfang von 250 beträgt dieser 6, nämlich 256-250. Dieser Wert muss in jedem Interrupt neu gesetzt werden. Um dieses zu erleichtern (ISRs sollen möglichst kurz sein), wird ein (unteres) Register mit diesem Wert vorbelegt. Somit braucht man in der ISR nur noch dieses Register nach TCNT0 auszugeben.

Bei den Deklarationen:

.equ zaehlumfang=250    ;Timer0-Zählumfang
In der Reset-Routine:
 ldi wl,256-zaehlumfang     ;Startwert (Reload) für Timer0
 mov tsw,wl                 ;einrichten und
 out tcnt0,tsw              ;setzen
 ldi wl,3                   ;Vorteiler 64 (DB Seite 27)
 out tccr0,wl               ;für Timer0
Damit nun der Timer auch einen Interrupt auslöst, muss noch das Bit TOIE0 im Timer-Interrupt-Maskenregister TIMSK gesetzt werden:
 ldi wl,1<<toie0            ;Timer0-Überlauf-Int
 out timsk,wl               ;aktivieren
Und damit Interrupts überhaupt zugelassen werden, muss noch das I-Flag im Statusregister gesetzt werden:
 sei                        ;Interrupts global freigeben
Damit nun im Interrupt in die richtige Routine verzweigt wird, müssen die Interrupt-Vektoren eingerichtet werden. Dies sind Sprungbefehle, die zu Beginn des Adressbereiches im Programmspeicher (Flash) angeordnet sind. Anzahl und Zuordnung der Sprungbefehle zu den Adressen ist bei jedem AVR-Typ in Abhängigkeit von der Hardwareausstattung anders und muss dem jeweiligen Datenblatt entnommen werden. Beim Tiny15 sind das:

Interrupt-Vektoren (Datenblatt Seite 12)

Um nun die Adressen nicht einzeln definieren zu müssen, fügt man für unbenutzte Interrupts Dummy-Befehle als Platzhalter ein. Nimmt man dafür den Befehl RETI (RETurn from Interrupt), dann wird auch ein versehentlich (durch Programmierfehler) aufgerufener Interrupt sauber und ohne Stackfehler beendet. Die Interrupt-Sprungtabelle für dieses Programm auf dem Tiny15 sieht dann so aus:

.cseg               ;Codesegment (Programmcode, Flash)
.org 0              ;Startadresse 0
 rjmp RESET         ;Reset handler
 reti;rjmp EXT_INT0      ;IRQ0 handler
 reti;rjmp PIN_CHANGE    ;Pin change handler
 reti;rjmp TIM1_CMP      ;Timer1 compare match
 reti;rjmp TIM1_OVF      ;Timer1 overflow handler
 rjmp TIM0_OVF      ;Timer0 overflow handler
 reti;rjmp EE_RDY        ;EEPROM Ready handler
 reti;rjmp ANA_COMP      ;Analog Comparator handler
 reti;rjmp ADC           ;ADC Conversion Handler

RESET:
Da nur Reset und Timer0-Überlauf genutzt werden, sind alle anderen Sprünge auskommentiert und durch "reti" ersetzt.

In der ISR ist nun der Timer0 wieder auf Startwert zu setzen. Ganz wichtig ist in der ISR auch, dass das SREG (Statusregister mit den Bedingungsflags) gesichert wird und am Ende der ISR wiederhergestellt wird. Beginn und Ende der Timer0-ISR sehen also so aus:

TIM0_OVF:           ;ISR Timer0-Überlauf
 in srsk,sreg   ;Statusregister sichern
 out tcnt0,tsw  ;Timer Reload (Startwert setzen)

 ;...hier folgt die eigentliche Arbeit der ISR,
 ;   also Tasten-Entprellung und Tondauer- und Melodiesteuerung...

 out sreg,srsk              ;Statusregister wiederherstellen
 reti                       ;fertig...

Tastenabfrage

Der Tiny15 hat (wenn man sich ISP nicht verbauen möchte) 5 nutzbare I/O-Pins. PB1 ist mit der Spezialfunktion OC1A belegt und wird daher als NF-Ausgang benutzt. Also sind noch 4 I/O-Pins frei, die als Eingänge genutzt werden können. Wenn sie nunmal da sind, sollten sie auch benutzt werden. Der Melodiegenerator bekommt also 4 Anschlüsse für "Klingeltaster". Sie werden gegen GND geschaltet, somit lassen sich beim Testaufbau die internen Pull-Up-Widerstände des Tiny15 verwenden. Für eine praktische Nutzung als Türklingel müssen aber zwecks Störunterdrückung niederohmige externe Pull-Up-Widerstände eingesetzt werden.

Da mechanische Taster sehr stark prellen, ist es erforderlich, diese per Software zu entprellen. Dies geht am einfachsten, indem man die Tasten-Eingänge zyklisch abfragt und neue Werte nur dann übernimmt, wenn sie mehrere Abfragen hintereinander stabil angelegen haben. In der Codesammlung im Mikrocontroller-Forum hat Peter Dannegger eine kugelsichere Tastenentprellung für 8 Taster (ein kompletter Port) mit Vierfach-Abfrage veröffentlicht, die sehr effizient arbeitet. Diese Routine läuft in einem Timer-Interrupt im Abstand von 4...20ms. Sie stellt dem Hauptprogramm zwei Register mit Einzelbits für jeden Taster zur Verfügung, von denen das eine den entprellten Status jedes Tasters darstellt, das andere ein "Betätigungsflag" für jeden Taster. Also ein Flag, dass von der Entprellroutine gesetzt wird, wenn ein Taster neu betätigt wurde. Gelöscht wird das Flag erst, wenn auf den Tastendruck reagiert wurde.
Damit die Tastenentprellung zyklisch ausgeführt wird, wird ein Timer-Interrupt benötigt. Damit der Timer-Interrupt auch zur Tondauersteuerung genutzt werden kann, wird sein Abstand auf 10ms (Begründung im nächsten Absatz) festgelegt. Die Erklärung der Entprellroutine findet man hier . Die Entprellung erfordert zwei untere Register als Prellzähler (pz0 und pz1), von denen das Eine die Bit0 aller Bits umfasst und das Andere die Bit1 aller Bits. Ein weiteres unteres Register (tas) enthält den (entprellten) Tastenstatus aller Taster. Die Flags für neu betätigte Tastendrücke liegen in einem oberen Register (tfl), um sie einzeln rücksetzen zu können (Operation mit Konstanten). Da beim Tiny15 nicht alle 8 Bits am Port existieren, bietet es sich an, im Register tfl noch das Busy-Flag unterzubringen, welches gesetzt wird, solange eine Melodieausgabe erfolgt und deswegen keine neue Melodieausgabe begonnen werden darf.

Die (entprellten) Tasten werden in der Hauptschleife des Programms abgefragt, falls das Busy-Flag nicht gesetzt ist. Ist ein Tastendruck erkannt worden, dann wird


Tondauersteuerung

Damit die einzelnen Töne der Melodie unterschiedlich lang sein können (Dauer des Tons) benötigt jeder Ton zwei Parameter: Damit mit dem Wertevorrat eines Bytes ein sinnvoller Tondauerbereich eingestellt werden kann, muss ein Kompromiss für den Timer gefunden werden, der die Tondauer herunterzählt. Dies ist Timer0, der schon die Entprellung der Taster erledigt. Bei einem ISR-Abstand von 10ms (100Hz) kann man Tondauern bis 2,55s im Hundertstelsekundenraster realisieren, das dürfte ein guter Kompromiss zwischen Auflösung und maximaler Dauer sein. Der Timer0 zählt in seiner ISR das Register "dauer" herunter, falls es einen anderen Wert als 0 hat, also falls ein Ton aktiv ist. Erreicht "dauer" dabei den Wert 0, dann wird das nächste Parameterpaar aus der Melodieliste gelesen. Die Tonhöhe (Frequenz) wird in das Timer-Referenzregister OCR1A geschrieben, die Tondauer in das Register "dauer". Danach wird der Timer1 aktiviert, falls die Tonhöhe erlaubt war. War die Tonhöhe 0, dann wird die Melodie gestoppt und das Busy-Flag gelöscht, denn 0 gilt als Ende-Kennung. War die Tonhöhe 1, dann wird der Tongenerator nicht aktiviert, aber die Tondauer abgearbeitet. Dies ermöglicht das Einfügen von Pausen.

Beim Herunterzählen der Tondauer wird zusätzlich auf Übereinstimmung mit der Konstante "stumm" geprüft und bei Übereinstimmung die Tonausgabe deaktiviert. Dies sorgt dafür, dass jeder Ton etwas eher ("stumm" hundertstel Sekunden) abgeschaltet wird, als der nächste Ton aufgerufen wird. Damit wird eine kleine Pause zwischen den Tönen realisiert, die es ermöglicht, innerhalb einer Melodie mehrmals hintereinander den gleichen Ton zu spielen. Ohne diese Pause würde man nur einen langen Ton statt mehrerer gleicher kurzen Tönen hören.


Parameterlisten für Melodien

Mit den 4 Tastern können 4 verschiedene Melodien aufgerufen werden. Dies erfordert 4 Parameterlisten. Für jeden Ton wird die Tonhöhe (Frequenz) und die Tondauer benötigt. Die Tonhöhe (Byte) enthält den Referenzwert für das Register OCR1A des Timer1. Damit nun nicht die direkten numerischen Werte eingegeben werden müssen (was sehr kryptisch wäre), werden diese Werte als Konstanten mit aussagekräftigen Namen definiert. Somit kann man statt der Werte einfach die Noten-Namen eintippen. Auch die Pause ist definiert, als Ende-Kennung gibt man eine 0 ein.
Die Tondauer wird dann in Zahlen eingegeben, die Auflösung entspricht 10ms je Wert. Da der Flash-Speicher der AVRs wordorientiert adressiert wird muss auch die Ende-Kennung einen Eintrag für die Tondauer haben, also ".db 0,0".

Download Assembler-Quelltext

Der komplette Quelltext in AVR-Assembler zum Download ist sehr üppig kommentiert. Er eignet sich daher besonders für die ersten Schritte in AVR-Assembler.

Calibration des internen RC-Oszillators

Der Tiny15 hat einen RC-Oszillator, dessen Fertigungstoleranzen durch eine Calibration kompensiert werden sollten, da er sonst mit einer undefinierten Frequenz um die 1MHz taktet. Dazu ermittelt der Hersteller das erforderliche Calibrationsbyte und schreibt es dauerhaft in den Signature-Bereich des AVRs und zwar in das High-Byte der Adresse 0. Diesen Bereich kann nur eine Programmer-Software (ISP oder HV) auslesen, das im AVR laufende User-Programm kommt da nicht ran. Zusätzlich schreibt der Hersteller das Calibrationsbyte auch in das Low-Byte und High-Byte der letzten Zelle des Flash-Speichers. Hier wird es aber beim ersten Löschen gelöscht.
Die von mir verwendete Calibrations-Routine (erster Teil der Reset-Routine) setzt voraus, dass sich das Calibrationsbyte im Low-Byte der letzten Flash-Zelle befindet. Es ist also je nach ISP-Programm von Hand aus dem Signature-Bereich auszulesen und in das vorletzte Byte der Programmdatei einzutragen. Nur dann kann die Reset-Routine den Oszillator ordentlich kalibrieren. Mein Eigenbau-ISP-Programm macht das bei jedem Löschen eines zu calibrierenden AVRs automatisch, falls die letzte Zelle nicht vom Programm belegt ist.

Hardware

Es wurde erstmal nur eine Versuchsanordnung auf einem Steckbrett aufgebaut.
Foto vom Steckbrett
Ob jemals ein finales Gerät aufgebaut wird, wissen derzeit nur die Götter...

Die Anschlussbelegung des Tiny15 ergibt sich aus den Sonderfunktionen der Pins.


   Widerstand zu Vcc   Reset  Pin1   Pin8  Vcc   Versorgungsspannung +5V
            Taster 4     PB4  Pin2   Pin7  PB2   Taster 2
            Taster 3     PB3  Pin3   Pin6  PB1 / OC1A Anschluss Lautsprecher über 10µF
                 GND     GND  Pin4   Pin5  PB0   Taster 1

   Alle Taster und der Lautsprecher werden gegen GND geschaltet...

Die Versorgungsspannung ist selbstverständlich mit einem Keramik-Kondensator 100nF abzublocken.

Das Programm hat noch den entscheidenden Nachteil, dass die Ruhestromaufnahme um die 4mA liegt und die Schaltung daher nicht für Batteriebetrieb geeignet ist. Deshalb habe ich eine weitere Variante entwickelt, die zwar nur einen Klingeltaster unterstützt, aber für Batteriebetrieb geeignet ist und über 4 Jumper oder DIP-Schalter einige Einstellungen zulässt.