Melodiegenerator mit Highspeed-PWM-Ausgabe mit Tiny15...
Aufgrund dieses Threads im Mikrocontroller-Forum habe ich mir Gedanken gemacht, wie man die Soundausgabe und die anderen Eigenschaften dieses Melodiegenerators verbessern kann. Ziel war ein sinusähnlicher Ton mit abklingender Amplitude. Erreicht wurden folgende Eigenschaften:
4 Taster für 4 unterschiedliche Melodien,
laute Soundausgabe durch Anschluss des Lautsprechers über komplementäre Emitterfolger,
abklingender sinusähnlicher Ton, über Highspeed-PWM ausgegeben,
Power-Down-Sleep außerhalb der Melodieausgabe,
aufwecken aus Power-Down durch Tastendruck an einem von 4 Tastern,
Überwachung der Betriebsspannung während der Melodieausgabe und Auslösen eines Alarmsignals danach, falls das Limit unterschritten wurde.
Schaltung
Es sind 4 Taster am Tiny15 angeschlossen, mit denen 4 verschiedene Melodien aufgerufen werden können. Damit der Tiny15 beim Betätigen eines Tasters über den Low-Level-Interrupt an Int0 aus dem Power-Down-Mode geweckt werden kann, sind drei Dioden eingebaut, die bei jedem Tastendruck den Int-Eingang mit auf GND ziehen. Bei längeren Leitungen zu den Tastern empfiehlt es sich, NPN-Transistoren an den Eingängen vorzusehen, die den AVR-Pin auf GND ziehen (siehe rechtes Bild). Die Klingeltaster werden dann gegen +4,8V geschaltet, nicht mehr gegen GND.
Der Ausgang wird mit einem komplementären Emitterfolger (als Impedanzwandler) verstärkt, da dieser einen geringen Ruhestrom aufweist. Der Lautsprecher ist gleichstromfrei über einen Elko 470µF angeschlossen. An PB3 (ADC2) ist über einen (im Vergleich zum internen PullUp) niederohmigen Wiederstand eine Reihenschaltung von 2 LEDs gegen GND angeschlossen, die als Z-Diode missbraucht werden. Bei eingeschaltetem internen PullUp fällt an ihnen eine konstante Spannung von 3,22V ab. Diese beeinflusst das Einlesen des Tasters nicht und erlaubt einen Vergleich der Betriebsspannung (NiCd-Akku 4,8V) als ADC-Referenzspannung mit dem an ADC2 fest anliegenden Spannungsabfall über den LEDs. Dies ermöglicht eine einfache Akku-Überwachung mit Alarm-Sequenz, die an darauf hinweist, den Akku mal wieder zu laden.
Das Programm wurde mittels AVR-Studio in AVR-Assembler geschrieben. Dabei gliedert sich das Programm in mehrere Teile, die teils in Interrupts abgearbeitet werden. Zwischen den Interrupts wird der Tiny15 bei laufender Soundausgabe in den Sleepmode IDLE versetzt, außerhalb der Soundausgabe wird der Sleepmode Power-Down aufgerufen, was den Standby-Stromverbrauch reduziert.
Gliederung des Programms:
Deklarationen Hier werden Konstanten definiert und Register mit Namen versehen.
Interrupt-Sprungtabelle Hier sind die Sprünge zur Reset-Routine und den Interrupt-Routinen aufgelistet. Anzahl und Reihenfolge der Vektoren ist bei den einzelnen AVRs unterschiedlich. Nicht benutzte Interrupts werden durch RETI als Platzhalter ersetzt.
Reset-Routine Hier werden einige Teile der Hardware initialisiert und einige Register mit Startwerten belegt.
Mainloop (Hauptschleife) Hier werden die entprellten Tastenflags abgefragt und evtl. angeforderte Melodien zur Ausgabe ausgewählt. Während eine Melodie läuft, ist der Aufruf neuer Melodien gesperrt. Zur Mainloop gehören die Routinen
tiefschlaf: Diese Routine wird aufgerufen, wenn ein von der Mainloop gemanagter TimeOut-Zähler abläuft. Dann wird der Tiny15 in den Power-Down-Mode versetzt und Int0 aktiviert, damit ein Tastendruck den AVR wieder wecken kann.
taste1:..taste4: Diese Routinen werden aufgerufen, wenn das zugehörige Tastenflag gesetzt ist und momentan keine Melodieausgabe erfolgt. Die Routine setzt den Z-Pointer auf den Anfang der jeweiligen Melodiedatenliste, löscht das entsprechende Tastenflag (und das, welches über die Diode den Int0 ausgelöst hat) und springt zur Routine 'start'.
start: Wird von den Tastenroutinen aufgerufen und schaltet den AVR in den Betriebsmodus (Busy-Flag setzen, Int0 deaktivieren, Sleepmode Idle auswählen, EEPROM-Adresse 0 einstellen, ersten Sinuswert in Timer1-PWM-Register schreiben). Danach wird mittels RCALL der Teil der ADC-ISR aufgerufen, der den nächsten Datensatz aus der Tabelle liest und Tonhöhe (Frequenz) und Dauer setzt (oder Dämpfung, Pause usw...). Nach dem Rücksprung aus der ADC-ISR wird zur Mainloop geprungen (um zu schlafen).
Externer Interrupt INT0 Hier wird der Sleepmode auf IDLE umgeschaltet und der externe Low-Level-Interrupt deaktiviert, damit der Tiny15 mit Takt versorgt wird und die anderen Komponenten arbeiten können.
ADC-complete-Interrupt Dieser Interrupt wird neben der Erfassung des Akkuzustandes noch als Timer für mehrere Aufgaben missbraucht:
LED-Spannungsabfall (Strom über internen PullUp) an ADC2/PB3 einlesen und Mittelwert über die 256 letzten Messungen ermitteln.
Runterzählen der Lautstärke gemäß des variablen Parameters für Dämpfung.
Einlesen und Entprellen der Taster (nach Peter Dannegger).
Auslesen eines Datensatzes aus der Melodieliste und einstellen der Parameter Tonfrequenz und Tondauer (Note), oder Pausenlänge, Dänpfung bzw. Ende-Kennung (andere vereinbarte Daten).
Steuerung der Tondauer, Holen der nächsten Daten bei Ablauf der Tondauer.
Nach letztem Ton der Melodie:
Akkuzustand mit Limit vergleichen, ggf. Ausgabe der Alarm-Sequenz einleiten,
Busy-Flag löschen,
TimeOut auf '5 vor 12' (1) setzen, damit die Mainloop über 'tiefschlaf' in den Standby-Modus schaltet, falls kein weiteres Tastenflag gesetzt ist.
Timer0-Überlauf-Interrupt Nach Reload des Timers mit dem variablen Wert der Tonhöhe (Frequenz) wird der nächste Wert aus der im EEPROM liegenden Sinus-Tabelle geholt und mit dem aktuellen Lautstärkewert multipliziert. Das Ergebnis wird dem Timer1 als Tastgrad für die PWM geschickt.
Timer1 Timer1 ruft keinen Interrupt auf und hat daher keinen zyklisch aufgerufenen eigenen Programmcode. Er wird in Highspeed-PWM (100kHz Ausgangsfrequenz) mit PWM-Umfang 256 betrieben und gibt über den Tastgrad der PWM die von Timer0 erzeugte Tonfrequenz aus.
Sinus-Tabelle im EEPROM Der Einfachheit halber wurde die gesamte Periode (64 Werte) einer Sinus-Schwingung ins EEPROM gelegt. 16 Werte und anschließende Berechnung der übrigen Werte hätte zwar gereicht, aber der EEPROM hat nunmal Platz für 64 Werte und die Rechnerei in der Timer0-ISR wird einfacher.
Der Ablauf beim Betätigen eines Tasters ist folgender:
Der Tiny15 befindet sich im Power-Down-Sleep und hat den Systemtakt abgeschaltet. Alle Timer (auch ADC) sind dadurch funktionsunfähig, nur ein L-Pegel an PB2 (INT0) kann den Tiny15 wecken.
Ein Tastendruck zieht (ggf. über eine der Dioden) PB2 (INT0) auf GND und löst damit den INT0 aus, worauf die INT0-ISR aufgerufen wird.
INT0-ISR
deaktiviert INT0,
aktiviert die PullUps aller Taster und
schaltet Sleep-Mode auf IDLE um, worauf der Systemtakt aktiv bleibt.
ADC-ISR (als Timer missbraucht) liest und entprellt die Taster und setzt die den Tastern entsprechenden Bits im Register tfl. Nebenher wird noch der Akku-Zustand eingelesen, aber das ist jetzt nicht relevant.
Erkennt die Mainloop ein gesetztes (entprelltes) Tastenflag, so wird
das Tastenflag dieser Melodie und das des Interrupteingangs gelöscht, damit die Melodieausgabe nur einmal erfolgt,
das Busy-Flag gesetzt, damit weitere Tastendücke erst nach Ablauf der Melodieausgabe abgefragt werden,
der EEPROM-Adresspointer auf Beginn der Sinustabelle gesetzt und der erste Sinuswert ausgelesen und als Tastgrad für die PWM-Erzeugung an Timer1 gesendet,
der Z-Pointer auf den Beginn der Datenliste der zugehörigen Melodie positioniert,
der Timer1 im Highspeed-PWM-Modus gestartet, wobei der erste Sinuswert der Sinustabelle als Tastgrad bereits eingestellt ist,
der Sleep-Mode auf IDLE umgeschaltet (könnte durch vorheriges Soundende schon auf IDLE gestellt worden sein, neuer Soundaufruf erfolgt aber, weil noch ein Tastenflag gesetzt war), damit Timer und ADC-Timer arbeiten können,
Int0 gesperrt, falls er schon beim vorherigen Soundende aktiviert war und noch ein Tastenflag aktiv war,
als Unterprogramm (mit RCALL) der Teil der ADC-ISR aufgerufen, der (bei Tonende) für das Holen des nächsten Datensatzes zuständig ist.
Die Routine "tonende" der ADC-ISR
holt nun den ersten Datensatz der Sounddaten und verteilt sie an die zuständigen Register
Der erste Datensatz enthält normalerweise den Dämpfungswert der Tonerzeugung, der ins Register "leis" übernommen wird.
Der zweite Datensatz enthält einen Notenwert und eine Notendauer. Der Notenwert wird als Reload-Wert für Timer0 (tsw) gesichert, der Tonlängenwert in den Zähler "dauer". War der Datensatz eine gültige Note, dann wird Timer0 mit Vorteiler 1:1 gestartet, der die Tonerzeugung steuert.
Die weiteren Datensätze enthalten Noten, Pausen, Dämpfungswerte oder die Ende-Kennung, die werden aber erst später aufgerufen...
Die ADC-ISR wird über RETI beendet und die Programmabarbeitung fällt in die Mainloop. Da das Busy-Flag gesetzt ist, werden bis zum Ende der laufenden Melodie keine weiteren Tastenflags abgefragt. Der Tiny15 fällt zum Ende der Mainloop in den Sleep-Zustand IDLE, aus dem ihn jeder Interrupt wecken kann.
Nach einer tonhöhenabhängigen Verzögerung weckt Timer0 nun den AVR und ruft die Timer0-ISR auf. In dieser wird
der nächste Interrupt-Termin (tonhöhenabhängig) "angemeldet",
der Sinuswert aus der Tabelle im EEPROM gelesen, die Adresse des EEPROMs erhöht und der Lesebefehl für den nächsten Zugriff erteilt,
dann wird der Sinuswert mit der aktuellen (langsam sinkenden) Lautstärke multipliziert und das Ergebnis in OCR1A geschrieben, worauf sich der Tastgrad der von Timer1 erzeugten PWM (100kHz) ändert.
Nach Verlassen der Timer0-ISR fällt der Tiny15 in die Mainloop und landet im Sleep.
Timer0 ruft nun peroidisch die Timer0-ISR auf, die den nächsten Sinuswert aufruft. Eine komplette Periode benötigt 64 Aufrufe.
Inzwischen läuft aber auch der ADC-Takt, und irgendwann ist der ADC fertig und löst die ADC-ISR aus. In dieser wird
der ADC für den nächsten Interrupt vorbereitet (eingeschaltet),
der ADC-Wert (Akkuzustand) eingelesen und in den Mittelwert übernommen, falls er größer als 1/4 des Limits ist, also falls der Taster an PB2 nicht betätigt ist,
in Abhängigkeit vom Wert der Dämpfung die Lautstärke (Multiplikator für den Sinuswert) vermindert, was ein Abklingen des Tons bewirkt,
bei jede zehnten Durchlauf
die Tasten eingelesen und entprellt,
der Tondauerzähler vermindert, falls er nicht schon auf 0 ist,
der nächste Datensatz aus der Liste geholt, falls die Tondauer abgelaufen ist,
Timer0-Reload, Tondauer und Startlautstärke gesetzt, falls es Notendaten waren,
Timer0 deaktiviert, aber Tondauer gesetzt, falls es Pausendaten waren,
Akkuzustand überprüft und ggf. Alarmsound ausgewählt, falls es die Ende-Kennung war,
Soundausgabe beendet, falls es die Alarm-Endekennung war oder die Endekennung bei vollem Akku. Dazu wird
das Busy-Flag gelöscht, damit die Mainloop einen weiteren Tastendruck erkennen kann,
Timeout auf "5 vor 12" gestellt, damit die Mainloop sofort in den Standby-Modus umschaltet, falls kein weiteres Tastenflag gesetzt ist.
mit RETI die ISR verlassen und in die Mainloop gefallen.
Falls die Sondaudgabe fertig ist, werden die (entprellten) Tastenflags abgefragt und beim Auffinden eines gesetzten Tastenflags die zugehörige Melodie zur Ausgabe vorbereitet.
War nach Ende der Soundausgabe kein weiteres Tastenflag gesetzt, dann erreicht der Timeout-Zähler 0, worauf die Mainloop die Routine 'tiefschlaf' aufruft, in der der Sleepmode Power-Down vorbereitet wird, Int0 aktiviert wird, alle PullUps außer für Int0 deaktiviert werden...
Nun fällt der AVR wieder in den Sleep-Zustand. Ist die Melodieausgabe noch oder wieder aktiv, so ist Sleepmode IDLE aktiviert, aus dem Timer0 und ADC-Interrupt den AVR wecken können. Ist die Melodieausgabe abgeschlossen, so ist Sleepmode Power-Down aktiv und der externe Interrupt freigeschaltet. Nun kann nur ein Tastendruck den AVR wecken.
Ermittlung des Akkuzustandes
Bei jedem Melodieaufruf werden die internen PullUp-Widerstände aller Taster eingeschaltet. An PB3 (ADC2) ist aber neben dem Taster auch noch eine Reihenschaltung aus 2 LEDs angeschlossen, an denen bei aktivem PullUp eine von der Betriebsspannung des AVRs unabhängige Spannung von etwa 3,22V abfällt.
Diese feste Spannung wird nun jede Millisekunde eingelesen, wobei als Referenzspannung die Betriebsspannung des Tiny15 genutzt wird, die ja die Akkuspannung ist und bei leerer werdendem Akku geringer wird. Es wird also nicht die übliche Messmethode angewendet, bei der eine variable Spannung gegen eine konstante Referenzspannung gemessen wird, sondern umgekehrt, es wird eine feste Spannung gegen eine variable Referenzspannung gemessen. Das ergibt
Der eingelesene ADC-Wert wird nun auf Plausiblität geprüft und verworfen, falls er zu klein ist, weil der Taster an PB3 betätigt ist. Ist der Wert plausibel, dann wird daraus der Mittelwert über 256 Messungen ermittelt. Die Mittelwertberechnung erfolgt, indem bei jedem neuen Wert 1/256 des momentanen Mittelwertes vom Mittelwert subtrahiert wird und danach 1/256 des neuen Wertes zum Mittelwert addiert wird. Das liest sich komplizierter als es ist, den der Bruch '1/256' wird durch einfaches Byteshifting erreicht. Als ASM-Code sieht das dann so aus:
sub volt0,volt ;1/256 vom Mittelwert
sbc volt,null ;incl. Übertrag subtrahieren
add volt0,wl ;1/256 des eingelesenen Wertes
adc volt,null ;mit Übertrag addieren
Diese Methode ist sehr ressourcensparend und benötigt zur Ausführung nur 4 Takte. Als Startwert für den Mittelwert wird in der Reset-Routine der Grenzwert eingetragen, der den Alarm auslöst. Somit bewegt sich der Mittelwert von Anfang an in die entsprechende Richtung (Akku voll oder Akku leer). Es werden etwa 1000 Messungen pro Sekunde gemacht, von denen aber nur die letzten 256 relevant sind.
Ist die Melodie zu Ende (Ende-Kennung 0), wird der Mittelwert mit dem Grenzwert verglichen und ggf. die Alarm-Melodie ausgegeben. Damit keine Endlosschleife entsteht, hat die Alarm-Melodie eine eigene Ende-Kennung, bei deren Auftreten die Tonausgabe ohne Akkutest beendet wird.
Leider ist die Ruhestromaufnahme immernoch etwas hoch, da über den PullUp für Int0 und die Diode etwas Strom über die LEDs fließt. Abhilfe schafft der Verzicht auf den Taster an PB3 und das Weglassen der Diode. Das bedeutet aber auch Verzicht auf eine Melodie, es können dann nur mit 3 Tastern 3 Melodien aufgerufen werden. Aber wer braucht schon 4 Klingeltaster an seiner Türklingel, meist reicht ja einer oder zwei.
Vereinbarte Werte in den Daten...
Jeder Datensatz besteht aus 2 Bytes. Das erste Byte ist der Timer-Reload-Wert und bestimmt die Frequenz, das zweite Byte bestimmt die Dauer (in Zehntelsekunden). Sehr kleine Timer-Reload-Werte sind unbrauchbar, da benachbarte Frequenzen zu weit auseinander liegen. Daher können die kleinen Werte als Steuerkommandos interpretiert werden. Vereinbart zur Steuerung sind:
0 Ende-Kennung einer Melodie, löst den Akkutest aus. Wert 2 wird ignoriert.
1 Kennung für Pause, Wert 2 bestimmt die Pausendauer.
2 Ende-Kennung der Alarm-Sequenz. Wert 2 wird ignoriert.
3 Kennung für Dämpfung, Wert 2 repräsentiert den Dämpfungswert (0..9).
>50 Timer-Reload für Tonhöhe, Wert 2 repräsentiert die Tondauer.
Jede Melodie sollte mit dem individuellen Dämpfungswert beginnen, auch innerhalb einer Melodie kann die Dämpfung verändert werden, indem man einfach einen neuen Dämpfungswert einträgt.