Bei einem Projekt wollte ich dem Anwender auf das Ende einer Aktion durch ein ansprechendes Tonsignal aufmerksam machen. Ein einfaches Piepsen war mit zu wenig. Ein Jingle wäre schön. Am einfachsten kommt man an eine Melodie, indem man sie aus dem Netz herunterlädt. Viele kleine Musikstücke stehen als Klingelton für Mobiltelefone kostenfrei zur Verfügung. Diese liegen häufig im RTTTL-Format vor.
Eine weitere Bedingung war, dass das Ganze interrupt-gesteuert erfolgen muss. Das Hauptprogramm muss für weitere Steuerungsaufgaben zur Verfügung stehen. Zur Verfügung steht der Timer2 eines ATmega48, der mit 8 MHz betrieben wird.
Es gibt im Netzt eine Reihe von RTTTL-Interpreter zu finden. Die meisten sind jedoch nicht komplett per Interrupt getrieben. Des Weiteren war nicht vorgesehen, Melodien dynamisch nachzuladen, d.h. es ist nicht unbedingt notwendig, mit den Original-RTTTL-Sequenzen zu arbeiten. Die Erzeugung der Töne soll später über einen Timer des AVR erfolgen. Es liegt also nahe, die RTTTL-Sequenzen so aufzubereiten, dass die ISR des Timer-Interrupts damit möglichst einfach umgehen kann.
Genaue Frequenzen mit einem AVR-Timer erhält man, indem man den Timer in einem Modus betreibt, in dem man die Zeitdauer zwischen zwei Interrupts möglichst präzise einstellen kann. Dies ist der "Clear Timer on Compare Match (CTC) Mode". Über den Prescaler wird festgelegt, mit welcher Takt-Frequenz der Timer angesteuert wird, mit den OCR-Registern wird festgelegt, wie viele der heruntergeteilten Takte eine Periode ausmachen. Die Zeitdauer zwischen zwei Auslösungen der zugehörigen "Output Compare Match Interrupt" beträgt somit t = 1/F_CPU * Prescaler * OCR und die zugehörige Frequenz ist f = 1/t.
Die Zeitdauer des Tons lässt sich am besten durch Zählen der notwendigen Schwingungen einstellen.
Aus dem Netzt geladene RTTTL-Sequenzen, werden von einem Visual-Basic-Programm in Sequenzen von Prescaler- und OCR-Anzahl-Werten übersetzt. Die Folgen werden als Arrays im Programmspeicher angelegt, nacheinander abgerufen und zur direkten Steuerung des Timers verwandt. Spezielle Kodierungen gibt es für die Pausen und das Melodie-Ende.
Die
Hardware ist recht einfach beschrieben.
Ein aus einem alten PC ausgebauter Piezo-Signalgeber
generiert die Töne.
Damit es schön laut wird, wird er im Gegentakt betrieben, d.h. beide Anschlüsse
werden an einen Pin des µC angeschlossen. Beim Abspielen wird dafür gesorgt, dass beide Pins immer
unterschiedliche Polarität haben.
Der Code zur Erzeugung des Gegentakts sieht dann so aus:
// Die beiden Ports für den Lautsprecher (Gegentakt)
#define TonePort1 PORTB
#define TonePin1 PB2
#define TonePort2 PORTD
#define TonePin2 PD7
// Pins umschalten
TonePort1 ^= _BV(TonePin1);
TonePort2 = (TonePort1 & _BV(TonePin1)) ? TonePort2 & ~_BV(TonePin2) : TonePort2 | _BV(TonePin2); // Gegentakt
Die Ansteuerung des zweiten Pins mag etwas umständlich erscheinen, stellt aber sicher, dass -unabhängig von der Ausgangssituation- stets beide Pins auf unterschiedlichen Potentialen liegen.
Das Umschalten der Pins erfolgt innerhalb der TIMER2_COMPA-ISR des Timer2. Timer2 wurde so eingestellt, dass die ISR mit der doppelten Frequenz der zu erzeugenden Tonfrequenz angesteuert wird.
Eine Melodie ist eine Abfolge von Tönen mit definierter Frequenz und Zeitdauer, ggf. unterbrochen von Pausen mit definierter Dauer. Wie oben beschrieben, wird sie als Folge von Prescaler-, OCR- und Schwingungsanzahl-Werten abgelegt. Dies ermöglicht eine einfache Steuerung des Timers und ist geeignet in einer ISR genutzt zu werden.
Der Datentyp zur Ablage der einzelnen Melodie-Komponenten ist:
typedef struct
{ uint8_t Ocr; // einzustellender Wert für das OCR-Register
uint8_t Prescaler; // einzustellender Wert für den Prescaler
uint16_t ToggleCount; // Anzahl Schaltvorgänge (Tondauer)
} note_t;
Spezielle Kodierungen gibt es für Pausen und das Melodie-Ende. Bei Pausen ist der OCR-Wert 0 und das Feld ToggleCount anzählt die Anzahl Millisekunden, die die Pause dauern soll. Das Ende der Melodie ist erreicht, wenn alle drei Elemente der Struktur 0 sind, also {0, 0, 0}.
Eine gespeicherte Melodie sind dann wie folgt aus:
note_t const Muppets[] PROGMEM =
{ {118, 0b011, 503}, {118, 0b011, 503}, {141, 0b011, 422}, {125, 0b011, 474}, {141, 0b011, 211}, {125, 0b011, 474}, {158, 0b011, 376}, {0, 0, 240}, {118, 0b011, 503},
{118, 0b011, 503}, {141, 0b011, 422}, {125, 0b011, 237}, {141, 0b011, 211}, {0, 0, 120}, {158, 0b011, 564}, {0, 0, 240}, {188, 0b011, 316}, {188, 0b011, 316},
{158, 0b011, 376}, {178, 0b011, 335}, {188, 0b011, 158}, {178, 0b011, 335}, {118, 0b011, 251}, {238, 0b011, 126}, {211, 0b011, 141}, {188, 0b011, 316}, {188, 0b011, 158},
{188, 0b011, 158}, {0, 0, 120}, {188, 0b011, 158}, {158, 0b011, 376}, {0, 0, 480}, {118, 0b011, 503}, {118, 0b011, 503}, {141, 0b011, 422}, {125, 0b011, 474},
{141, 0b011, 211}, {125, 0b011, 474}, {158, 0b011, 376}, {0, 0, 240}, {118, 0b011, 503}, {118, 0b011, 503}, {141, 0b011, 422}, {125, 0b011, 237}, {141, 0b011, 422},
{158, 0b011, 564}, {0, 0, 240}, {188, 0b011, 316}, {188, 0b011, 316}, {158, 0b011, 376}, {178, 0b011, 335}, {188, 0b011, 158}, {178, 0b011, 335}, {118, 0b011, 251},
{238, 0b011, 126}, {211, 0b011, 141}, {188, 0b011, 316}, {188, 0b011, 158}, {211, 0b011, 282}, {211, 0b011, 141}, {238, 0b011, 251}, {0, 0, 0} };
Die gespeicherten Melodien werden über Pointer publiziert.
extern note_t const Scottland[] PROGMEM;
extern note_t const PinkPanther[] PROGMEM;
extern note_t const Muppets[] PROGMEM;
...
Ein einzelnes Melodie-Element ist vier Byte lang. Es liegt also nahe, die vorhandene AVR-lib-Biblioteksfunktion pgm_read_dword zum Einlesen zu nutzen. Das Zwischenschalten einer union dient zur Konvertierung von DWORD nach note_t.
static union // Zum Einlesen aus PROGMEM
{ note_t note; // Noten-Daten
uint32_t x; // DWORD
} TempNote;
TempNote.x = pgm_read_dword(...);
note_t n = TempNote.note;
Die Erzeugung der Melodie erfolgt innerhalb der ISR zum Output Compare A Match Interrupt. Sie muss bei der Ausgabe von Tönen die Pins umschalten und die Schwingungen zählen, bei der Ausführung der Pausen die Pausendauer in Millisekunden nachhalten und nach Beendigung einer Aktion die nächste anstoßen. Zusätzlich muss sie nach jedem gespieltem Ton eine kurze Pause von 30 ms einfügen, weil dies den Klang der Melodie deutlich verbessert. Die direkte Folgen der Töne ohne diese Pause hört sich nicht gut an.
static void StartNote(note_t Note); // Startet das Abspielen der nächsten Note
static void StartPause(uint16_t ms); // Startet die Ausgabe einer Pause
void StopMelody(); // Unterbricht das Abspielen einer Melodie
static volatile uint16_t ToggleCount; // Anzahl noch verbleibender Umschalt-Vorgänge.
static volatile uint8_t Pause; // 0 falls aktuell eine Pause ausgegeben werden muss.
static volatile bool NoteCompletion; // false, wenn kurze Pause nach Noten-Ende noch ansteht.
static note_t *ActNote; // Pointer auf die nächste Note
// Timer-ISR erledigt das Abspielen der Melodie
ISR(TIMER2_COMPA_vect)
{ if (Pause != 0) // Wird aktuell ein Ton ausgegeben, dann Pins umschalten.
{ TonePort1 ^= _BV(TonePin1);
TonePort2 = (TonePort1 & _BV(TonePin1)) ? TonePort2 & ~_BV(TonePin2) : TonePort2 | _BV(TonePin2); // Gegentakt
}
if (ToggleCount-- == 0) // Ende eines Auftrags erreicht?
{ if (NoteCompletion) // Noten-Komplettierung durchgeführt?
{ TempNote.x = pgm_read_dword(++ActNote); // Nächste Note einlesen
if (TempNote.note.ToggleCount) // Gültige Note (Ende-Kennung ist 0)?
StartNote(TempNote.note);
else
StopMelody();
}
else
{ NoteCompletion = true; // Noten-Komplettierung starten
StartPause(30);
}
}
}
Der Timer ist so angesteuert, dass er bei der Ton-Ausgabe mit der doppelten Tonfrequenz einen Interrupt auslöst und bei Pausen jede Millisekunde. ToggleCount enthält demzufolge die notwendige Anzahl von Pin-Umschaltungen bei der Tonausgabe und Anzahl Millisekunden während einer Pause.
Zunächst wird geprüft, ob aktuell ein Ton erzeugt werden soll. Ist dies der Fall, müssen die Pins für den Piezo-Signalgeber umgeschaltet werden.
Der Aktionszähler ToggleCount wird heruntergezählt. Ist das Ende der aktuellen Aktion erreicht, wird über NoteCompletion geprüft, ob zum Abschluss des Tons eine kurze Pause eingefügt werden soll.
Ist dies nicht der Fall, wird die nächste Note geladen und geprüft, ob dies die Ende-Kennung ist.
Die Hilfsfunktionen StartNote, StartPause, StopMelodie machen das, was der Name sagt.
// Startet das Abspielen der nächsten Note
static void StartNote(note_t Note)
{ ToggleCount = Note.ToggleCount;
NoteCompletion = false;
TCCR2A = _BV(WGM21); // CTC-Modus festlegen TOP = OCRA
TIMSK2 |= _BV(OCIE2A); // Interrupt freigeben
if ((Pause = Note.Ocr) != 0)
{ // Pins in den Output-Modus versetzen
DDR(TonePort1) |= _BV(TonePin1); // Pin auf Output
DDR(TonePort2) |= _BV(TonePin2); // Pin auf Output
OCR2A = Note.Ocr; // OCR setzen
TCCR2B = Note.Prescaler; // Timer starten
}
else
StartPause(Note.ToggleCount);
}
Zunächst werden generelle Einstellungen gemacht, die sowohl für die Ausgabe eines Tons als auch einer Pause notwendig sind. Dann prüft StartNote , ob die abzuspielende Note eine Pause-Kodierung enthält. Ist dies der Fall wird über StartPause die Ausgabe einer Pause gestartet. Steht die Ausgabe eines Tons an, werden die Pins des Piezo-Signalgebers auf Otutput geschaltet und der Timer zur Erzeugung der passenden Frequenz eingestellt.
// Startet die Ausgabe einer Pause
static void StartPause(uint16_t ms)
{ ToggleCount = ms;
// Timer auf Taktdauer von ca. 1ms
OCR2A = 8; // OCR setzen
TCCR2B = 7; // Timer starten, Prescaler 1024
Pause = 0;
NoteCompletion = true;
}
Der Timer wird auf einen Takt von etwa 1 ms eingestellt. NoteCompletion wird auf true gesetzt um zu verhindern, dass an das Ende der (Noten-)Pause noch einmal eine kurze Pause eingefügt wird, wie sie bei der Ausgabe von Tönen notwendig ist.
// Unterbricht das Abspielen einer Melodie
void StopMelody()
{ TIMSK2 &= ~_BV(OCIE2A); // Interrupt sperren
TCCR2B = 0; // Von der Taktquelle trennen
DDR(TonePort1) &= ~_BV(TonePin1); // Pin auf Input
DDR(TonePort2) &= ~_BV(TonePin2); // Pin auf Input
}
Der Timer-Interrupt wird unterbunden und die Pins des Piezo-Signalgebers auf Input gelegt.
Das API besteht aus dem Datentyp note_t (s.o.) den Zeigern zu den gespeicherten Melodien (s.o.) und den drei Funktion PlayMelody, IsPlayingMelody und StopMelody.
// Startet das Abspielen eioner Melodie
void PlayMelody(note_t *Melody)
{ ActNote = Melody;
TempNote.x = pgm_read_dword(Melody);
StartNote(TempNote.note);
}
Es wird einfach die erste Note geladen und an StartNote übergeben.
// Zeigt an, ob aktuell eine Melodie gespielt wird.
bool IsPlayingMelody(void)
{ return (TIMSK2 & _BV(OCIE2A)); // Ist der Interrupt freigegeben?
}
Hier wird einfach geprüft, ob der Timer-Interrupt noch freigegeben ist.
PlayMelody(Muppets); // Startet die Ausgabe einer Melodie
while (IsPlayingMelody())
{ // Mache irgendetwas
...
// Vorzeitiger Abbruch
if (StopButtonPressed())
StopMelody();
}
Zum Code gibt es nicht viel zu sagen. Der Analyzer für den RTL-Code habe ich von hier: Musik-Box von flickerfly und die Noten-Definitionen stammen von rogue-code Tone Library. Beides C-Programme für den Arduino, die ich nach Basic übertragen habe (wie schon an anderer Stelle beschrieben: Vielen geschweiften Klammen von C etc. machen mich nervös!).
Die Bedienung des Programms ist recht einfach. In das linke Fenster kopiert man den zu transformierenden RTTTL-Code, drückt die Schaltfläche "Abspielen". Das Stück wird vorgespielt und dabei ins rechte Fenster übertragen. Zum Schluss wird das Ergebnis in die Zwischenablage kopiert und kann dann einfach in das AVR-Programm einkopiert werden.