<- Bild 1: Töne erkennen und analysieren, das kann man auch mit einem Mikrocontroller
Tonerkennung: Frequenzen messen und analysieren
Wenn es darum geht, Töne bzw. NF-Frequenzen zu erkennen, kann dies wahlweise durch Hardware oder Software geschehen. Während man zur Hochzeit des RTTY-Betriebs auf den Tondekoder LM567 bzw. NE567 setzte, wird man heute eher eine Realisierung in Software bevorzugen. Wie so oft im Leben hilft dabei die Mathematik. Hier spielen Zeitgeber (Timer), die Diskrete Fouriertransformation und hier insbesondere der Goertzel-Algorithmus eine entscheidende Rolle.
In diesem Beitrag soll es weniger um die mathematische Herleitung oder das Verständnis der Algorithmen gehen, sondern vielmehr um deren Anwendung, um in der Lage zu sein, auf einfache Weise eigene Projekte zu realisieren. Viele schlaue Köpfe haben die Algorithmen bereits in Software gegossen und stellen diese Funktionen vielfach kostenfrei zur eigenen Verwendung zur Verfügung. Wer also einen RTTY-Dekoder programmiert oder an einen CW-Dekoder „bastelt“, DTMF-Töne erkennen möchte oder auch nur das Vorhandensein eines einziges Tones, kann auf diese bereits in Software verfügbaren Algorithmen zurückgreifen.
Die gestellte Aufgabe bestand darin, zu erkennen, ob ein Summer einen Ton einer bestimmten Frequenz – sagen wie 4000Hz - abgibt oder nicht. Viertausend Hertz lassen sich gut mit einem Frequenzzähler ermitteln. Möchte man diesen aus verschiedenen Gründen mit einem Mikroprozessor realisieren, sind zwei Voraussetzungen zu erfüllen: Die zu messende Frequenz muss störungsfrei sein und möglichst als Rechtecksignal vorliegen. Beides war zunächst der Fall, denn der Summer wurde von einem anderen Prozessor mit einem Rechtecksignal erzeugt. Später sollte es auch mit einem Mikrofon als Quelle funktionieren.
Frequenzen messen
Die erste Überlegung zur Frequenzmessung führt dahin, die Zeit zwischen zwei ansteigenden Flanken des Rechtecksignals zu ermitteln. Das ist die klassische Variante. Aus der verstrichenen Zeit (eine Periode oder eine halbe Periode) bestimmt man dann die Frequenz. Sollte es sich bei der zu messenden Frequenz jedoch nicht um ein konstantes Signal handeln, so wird man mit dieser Methode nur einen Momentanwert ermitteln können, denn im nächsten Moment könnte sich die Frequenz, also die Zeit zwischen zwei ansteigenden Flanken des Rechtecksignals, bereits wieder ändern. Aber es ergeben sich auch Vorteile: Die Messung ist sehr schnell, sie beträgt genau eine Periode. Lässt man in einer Endlosschleife Messung auf Messung laufen, erkennt man Frequenzänderungen in Echtzeit.
Der zweite Ansatz verspricht genauere Ergebnisse: Über einen gewissen Zeitraum, den man frei bestimmen kann, zählt man die ansteigenden (oder wahlweise absteigenden) Flanken. Anhand des Resultates und der Zeitspanne in Millisekunden wird daraus auf die Frequenz geschlossen. Damit ermittelt man für diese Zeit einen Mittelwert der Frequenz. Technisch gesehen passiert folgendes in einem Mikrocontroller: Ein Timer wird so eingestellt, dass z. B. jede Millisekunde ein Timer-Überlauf stattfindet und verstrichene Millisekunden gezählt werden. Dieser Timer ermöglicht der Software eine Torzeit (gatetime) zu definieren für den Zeitraum, in dem eine Frequenz gemessen werden soll.
Ein weiterer Timer wird so eingestellt, dass das zu messende Signal als Takt des Timers konfiguriert wird. Mit jeder steigenden Flanke wird ein Zähler um eins erhöht. Zu Beginn der Torzeit wird der Zähler auf Null gestellt. Läuft die Torzeit ab, wird der Zählerstand ausgelesen und anhand der Torzeit die (über die Torzeit) gemittelte Frequenz berechnet. Diese Messmethode eignet sich vorzüglich für konstante Frequenzen, wie in diesem Fall die des Summers. Allerdings: Je kürzer die Torzeit gewählt wird, desto näher ist die gemessene Frequenz an der Realität. Erkenntnis: Für sich schnell ändernde Frequenzen wählt man Methode 1, für eher konstante Frequenzen Methode 2, es sei denn, eine Mittelwertbildung ist gewünscht.
Ein dritter Ansatz folgt dem Konzept, dass man schaut, welche dominierenden Frequenzen ein zu analysierendes Tonsignal enthält. Das geschieht in der Regel mit einer Diskreten Fouriertransformation (DFT) oder der schnelleren Fast Fourier Transformation (FFT). Für einen in der Rechenleistung begrenzten 8-Bit-Mikrocontroller stellt das eine nur schwer zu lösende Aufgabe dar. Eine Lösung besteht darin, dass man nicht alle Frequenzen eines Signales berechnet, sondern nur diejenigen, die besonders interessant sind oder man schaut auf Frequenzen, die erwartet werden, wie etwa bei der Dekodierung von CW-Signalen, RTTY oder der Dekodierung von DTMF-Tönen. Hier kommt der Goertzel-Algorithmus ins Spiel.
<- Bild 2: Der Funktionsgenerator liefert 2000Hz, hier die Ausgabe der gemessenen Frequenz nach der Methode 1.
Frequenzmessung nach der Methode 1
Bei dieser Methode wurde der Atmel ATMega328P als Prozessor gewählt. Es ist derselbe Controller, der sich auch auf dem Arduino UNO befindet. Das Projekt wurde unter Atmel Studio 7 mit der Bezeichnung Atmega328PInputCapture_Timer1_Test angelegt. Sie finden das Programm unten zum Download. Kopieren Sie die Datei in den Ordner LW:\Benutzer\Dokumente\Atmel Studio\7.0\ ihrer Festplatte, wenn Sie das Atmel Studio 7.0 zuvor installiert haben. Nach dem Laden des Projektes ändern Sie bitte einige kleine Projekteinstellungen, die im Programm beschrieben sind. Diese beziehen sich darauf, dass man im Programm den Befehl sprintf() zur Formatierung von Strings nutzen kann und diese ohne Fehler gelinkt werden. Nun kompilieren Sie das Projekt mit F7 und übertragen es schließlich mit einem ISP-Programmer auf den Prozessor. Ein Funktionsgenerator liefert das TTL-Rechtecksignal an den PortB0/ICP1 des ATMega328P. Das kann auch ein anderer Prozessor leisten, wenn man keinen Funktionsgenerator besitzt. Bei einer Frequenz von 2 kHz erscheinen auf einem Terminalprogramm die gemessenen Frequenzen (siehe Bild 2). Über einen kleinen USB-zu-TTL-Adapter verbinden Sie den Prozessor (TxD und GND) mit dem PC.
Mit dieser Methode misst man von etwa 35 Hz bis hoch zu 15 kHz. Dabei ist die Genauigkeit im unteren Bereich noch recht hoch , nimmt aber nach oben zunehmend ab. Bei höheren Frequenzen geht die Verarbeitungszeit des Interrupt und das Speichern der Werte immer mehr in das Ergebnis ein. Das liegt daran, dass die gemessenen Zeiten bei zunehmender Frequenz kürzer werden und die Verarbeitungszeit im Interrupt mehr zu Buche schlägt. Das Programm ist so eingestellt, dass es 20 Mal die Frequenz misst und einen Mittelwert berechnet. Diesen Wert kann man leicht bei der Definition der Variable speichertiefe einstellen. Der Aufruf der Frequenzerkennung sieht im Hauptprogramm dann so aus:
Bei Bedarf schließt man an PortC0 und PortC1 je eine LED oder ein Oszilloskop an. Dann kann man das Geschehen gut mitverfolgen. Wenn Sie die entsprechenden Zeilen aktivieren (zurzeit auskommentiert), verringert sich die Genauigkeit ein wenig. Nehmen Sie das Programm als Grundlage für eigene Experimente. Sicherlich lässt es sich optimieren.
Frequenzmessung nach der Methode 2
Um die Ungenauigkeiten der Methode 1 zu vermeiden, wird das Rechtecksignal zur Messung auf den Takteingang des Timers1 des ATMega328P an Pin PortD5/T1 angelegt. Wir nutzen das Atmel Studio 7-Projekt mit der Bezeichnung FreqCounter_Test. Wie zuvor läuft der Prozessor mit einem Takt von 8MHz, die Änderungen bei den Linker-Einstellungen sind auch hier vorzunehmen und im Code dokumentiert. Das Projekt basiert auf eine C-Klasse, die Edgar Bonet, eine andere mögliche originale Quelle ist Martin Nawrath, das ist unklar, vor einiger Zeit in ein Diskussionsforum (Quelle: [2]) gepostet hat und die ich etwas angepasst habe. Das Programm misst die Frequenz, legt sie in der Variable frq ab und deren Inhalt wird über den UART über einen USB-Adapter dem PC zugeleitet und kann dort betrachtet werden. Der Messbereich bewegt sich in einem Bereich von 100Hz bis etwa 3MHz und ist systembedingt auch bei höheren Frequenzen fast auf das Hertz genau. Im Hauptprogramm wird die C-Klasse des Frequenzzählers so aufgerufen:
// Frequenz 100ms messen FreqCounter::start(100); // Dauer der Messung in ms (Torzeit) while (FreqCounter::f_ready == 0); // warten auf Ende der Messung frq = FreqCounter::f_freq; // Ergebnis übernehmen
Anschließend folgen zwei Zeilen, um das Ergebnis zum PC zu schicken. Daneben gibt es die Möglichkeit, auf das Vorhandensein einer gewünschten Frequenz abzufragen:
Die Funktion Frequenz_messen misst mehrfach die Frequenz und liefert die Zahl der Treffer zurück. Dabei wird eine Toleranz von +-5% berücksichtigt. Letztere kann man den eigenen Erfordernissen leicht anpassen. Im Programm existiert zudem eine Routine, die ein 5-kHz-Rechtecksignal mit dem Timer0 erzeugt, welche jedoch auskommentiert ist. Verwenden Sie diese für eigene Experimente. Methode zwei funktioniert gut, solange das Rechtecksignal ungestört ist. Störgeräusche würden die Messung verfälschen – in den meisten Fällen würden zu hohe Messwerte gezählt.
<- Bild 3: Liefert der Goertzel-Algorithmus eine Amplitude größer als der vorgegebene Schwellwert, wertet die Software diese Frequenz als „erkannt“.
Frequenzanalyse nach der Methode 3
Statt rechenintensiv auf alle Spektralanteile zu schauen, wie es die Diskrete Fouriertransformation (DFT) oder die Schnelle Fouriertransformation (FFT) macht, betrachtet der Goertzel-Algorithmus nur gewünschte Frequenzen. Der Algorithmus wurde schon 1958 von Gerald Goertzel entwickelt. Eine für Mikrocontroller verwendbare Implementierung wurde von Kevin Banks für alle Interessenten bereitgestellt und von Jacob Rosenthal in eine Arduino-Bibliothek mitsamt Beispielsketch gegossen. Die Bibliothek von [3] gibt es auf der virtuellen Heft-DVD. Kopieren Sie diese in das Verzeichnis LW:\Benutzer\Arduino\libraries. Nach einem Start der Arduino-Entwicklungsumgebung steht diese sofort zur Verfügung. Laden Sie das Beispielsketch decode.ino, kompilieren es und übertragen es auf einen Arduino UNO. Das Rechteck-Signal wird am Arduino-Pin A0 angelegt und muss TTL-Pegel aufweisen. Größere Spannungen als 5Vss verträgt der Eingang nicht. Wenn Sie die Rechteckfrequenz am Funktionsgenerator verändern, sollte bei etwa 700Hz die grüne LED auf dem Arduino flackern. Möchten Sie die zu dekodierende Frequenz ändern, schauen Sie im Sketch auf diese Zeile:
const float TARGET_FREQUENCY = 700;
und ändern den Wert. Haben Sie ein Problem bei der Installation der Goertzel-Lib, hilft Ihnen sicherlich [4] weiter. In der Hauptschleife des Arduino-Beispielprogramms benötigt die Abfrage der Frequenz und das Steuern der LED bei guter Amplitude nur wenige Zeilen:
goertzel.sample(sensorPin); // Daten per AD-Wandler einlesen float magnitude = goertzel.detect(); //Amplitude der gewünschten Frequenz
// bei erkanntem Signal ist die Amplitude im Spektrum hoch if(magnitude>THRESHOLD) // Amplitude > Schwellwert? digitalWrite(led, HIGH); //LED ein else digitalWrite(led, LOW); //nicht erkannt, LED aus
Für den Zweck, die Arduino-Lib mit dem Atmel Studio nutzen zu können, habe ich sie vom Arduino-Ballast befreit und das beschriebene Beispielprogramm auf dem Atmel Studio für den ATMega328P neu programmiert, mit LED und einer seriellen UART-Ausgabe mit 9600 Baud (wie auch bei den zuvor besprochenen Programmen). Die Ausgabe zum PC-Terminalprogramm listet die Amplitude des gefundenen Signals (magnitude) auf und wie oft die gewünschte Frequenz in diesem Durchgang erkannt wurde.
Das Projekt trägt den Namen ATMega328P_Goertzel_Tonerkennung. In diesem Projekt wird auf eine 800Hz-Frequenz abgefragt. Es gibt, wie auch der der Arduino-Version, einige Parameter, an die man „drehen“ kann, um das Resultat zu optimieren:
uint8_t sensorPin = 0; Das bedeutet, dass das Rechtecksignal am Pin ADC0/PC0 des Controllers anliegt. Ändern Sie den Wert, um einen andere Kanal des Analog-Digital-Wandlers zu verwenden.
const float TARGET_FREQUENCY = 800; Hier wird die gewünschte Frequenz in Hz festgelegt. Die max. mögliche Frequenz beträgt 8900/2, also etwas unter 4500Hz.
const int N = 200; Anzahl der ADC-Samples (Abtastungen des Analog-Digital-Wandlers). Mit diesem Wert sollten Sie experimentieren. Bei tiefen oder hohen Frequenzen können Sie testen, ob sich die Erkennung verbessert oder verschlechtert. Beachten Sie bitte: Je größer die Zahl der Abtastungen, desto länger dauert die Berechnung.
<- Bild 4: Die Software nach dem Goertzel-Algorithmus listet die Amplituden der Messungen bei erkannter Frequenz.
const float THRESHOLD = 4000; Das ist der Schwellwert, der darüber entscheidet, ob eine Messung als „Frequenz erkannt“ oder „Frequenz nicht erkannt“ bewertet wird. Verfolgen Sie die Arbeit des Programms in einem Terminalprogramm, wird die Amplitude zu jeder Messung angezeigt (Bild 4). Bei hohen Amplituden der gewünschten Frequenz können Sie diesen Schwellwert nach oben anpassen.
const float SAMPLING_FREQUENCY = 8900; Dieser Wert ergibt sich aus der Berechnung der Taktfrequenz des ADC geteilt durch die Dauer eines Samples (14 Taktzyklen) = ca. 8900 Abtastungen des ADC je Sekunde. Teilt man diesen Wert durch zwei, ergibt sich die maximale, zu erkennende Frequenz. Diesen Wert sollten Sie nur ändern, wenn Sie die Taktfrequenz des ADC verändern. Beim ATMega328 sind 125 kHz jedoch ein praktikabler und empfohlener Wert. Weiteres: siehe Datenblatt des Controllers.
Bisher sind wir davon ausgegangen, dass ein Funktionsgenerator den ATMega328P über einen analogen Eingang mit dem Rechtecksignal bedient. Interessant wird es erst, wenn man ein Elektret-Mikrofon an den Controller anschließt und in einem SSB-Empfänger ein CW-Signal einstellt. Bei richtiger Einstellung des CW-Tons sollte – im besten Fall – die LED im Takt des CW-Signals aufleuchten. Störungen anderer Frequenzen sollten weitgehend ignoriert werden.
<- Bild 5: Verdrahtungsplan des CW-dekoders rund um einen Arduino UNO. Quelle: Skovholm.
CW-Dekoder
Einen Schritt weiter führt der CW-Dekoder von OM Hjalmar Skovholm Hansen, OZ1JHM. Er stellte seine Software decoder11.ino ins Internet [5] und bietet es unter der GNU-Lizenz kostenfrei an. Der Dekoder basiert auf den Goertzel-Algorithmus und erwartet das CW-Signal an Arduino Pin# A1. Die Ausgabe der dekodierten Zeichen erfolgt über ein LC-Display mit 16 bzw. 20 Buchstaben je Zeile mit zwei oder vier Zeilen. Das muss man im Sketch einstellen. Liegt die Amplitude des Signals höher als ein Schwellwert, wird die Länge des Dit bzw. Dah sowie deren zeitliche Abstände zueinander bewertet. Mehrere Dit‘s und Dah‘s puzzelt die Software anhand der Pausenzeiten zu Buchstaben zusammen und ordnet mittels einer Tabelle die Buchstaben zu, welche dann auf dem LCD abgebildet werden..
Ein Video auf Youtube [6] zeigt die Schaltung in Funktion. Wie aus dem Verdrahtungsplan ersichtlich, wird das CW-Signal über einen Koppelkondensator und einem Spannungsteiler direkt den analogen Eingang der Arduino UNO zugeführt.
Bild 6: Ein Arduino UNO, zehn LED und ein Kondensator bilden den DTMF-Dekoder. Der Opamp ganz links kann bei ausreichend starkem Eingangssignal entfallen. Quelle: Arduino Projecthub.
DTMF-Dekoder
Im Arduino Projecthub findet man den auf den Goertzel-Algorithmus basierenden DTMF-Dekoder. Die Schaltung verwendet 10 LEDs, die an den digitalen Pins 2 bis 12 angeschlossen sind und leuchten, wenn ein beliebiger Ton für die Zahlen 0 - 9 empfangen wurde. Die auf dem Arduino UNO an Pin# 13 auf der Platine bereits enthaltene grüne LED blinkt kurz auf, sobald ein Impuls oder ein Signal erkannt wird. Zahlen größer als 9 (die Sonderzeichen A - F) werden über Serial.print() auf dem Monitor in der Arduino IDE angezeigt. Die NF wird über einen 1uF-Kondensator in Reihe mit dem analogen Pin #A0 verbunden, der die niedrigen Frequenzen und den DC-Offset herausfiltert. Vor dem Kompilieren des DTMF-Dekoders muss die Goertzel-Lib installiert worden sein.
Alle beschriebenen Arduino-Sketche bzw. Atmel-Studio-7-Softwareprojekte finden Sie unten zum Download.
Exkurs: Der LM567
<- Bild 6: Blockschaltbild desd LM567, der auch heute noch gern zur Tonerkennung genutzt wird. Quelle: Datenblatt.
So funktioniert der Tondekoder-IC: Der LM567 ist ein universell einsetzbarer Decoder, die einen Transistor auf Masse schaltet, wenn ein Eingangssignal innerhalb des Durchlassbereichs vorhanden ist. Die Schaltung besteht aus einem I- und Q-Detektor, gesteuert von einem spannungsgesteuerten Oszillator, der die Mittenfrequenz festlegt. Externe Komponenten (R, C) werden verwendet, um unabhängig voneinander die Mittenfrequenz, Bandbreite und Ausgabeverzögerung (delay) zu definieren.