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.

In­halts­ver­zeich­nis

1. Prinzip

2. Tonerzeugung

3. Melodie

4. API

5. RTTTL-Compiler

6. Download


Prinzip

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.

Tonerzeugung

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

Gegentakt-Schaltung

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.

Melodie

Speicherung

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

Abruf

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;

Melodie-Erzeugung

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.

ISR

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.

StartNote

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

StartPause

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

StopMelodie

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

API

Funktionen

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.

PlayMelody

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

IsPlayingMelody

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

StopMelody

s.o.

Beispiel

PlayMelody(Muppets);  // Startet die Ausgabe einer Melodie

while (IsPlayingMelody())
{ // Mache irgendetwas
  ...

  // Vorzeitiger Abbruch
  if (StopButtonPressed())
     StopMelody();
}

RTTTL-Compiler

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.

RTTTL-Compiler

Download

AVR-Modul Melody (Quell-Code)

RTTTL-Konverter Binary

RTTTL-Konverter Quell-Code