Raspberry Pi Pico als Software Defined Radio
Von Martin Ossmann (Deutschland)
Was den Deutschen ihr DCF77, ist den Briten der altehrwürdige Zeitzeichensender MSF, früher als „Rugby Clock“ bekannt, der seit fast 100 Jahren zunächst von Rugby und später von seinem nordenglischen Standort Anthorn aus Zeitsignale auf einem Langwellen-Träger von 60 kHz in den Äther verbreitet. In den Anfangszeiten des Zeitzeichensenders, der ursprünglich als Frequenznormal diente, wurde nur zweimal am Tag eine fünfminütige Impulsfolge gesendet. Das „Sendeprotokoll“ wechselte im Laufe der Jahrzehnte mehrfach, aber erst im Jahre 1977 wurde das Signal so kodiert gesendet, dass es auswertbare Zeit- und Datumsinformationen enthielt.
Das Projekt
Während Schaltungen von und mit DCF77-Empfängern/Dekodern seit Jahrzehnten zum Standard-Repertoire von Elektor gehören, dürfte die hier beschriebene Schaltung eines MSF-Empfängers eine Premiere für die deutsche Ausgabe von Elektor sein. In der elektronischen Steinzeit, 40 Jahre sind seitdem vergangen, gab es (nur) in der englischen Elektor-Ausgabe ein entsprechendes Projekt als Vorsatzschaltung für den guten alten 6502-Junior-Computer.
Natürlich ist die Technik des Empfängers/Dekoders, den wir hier vorstellen, „up to date“; das Ganze wird mit einem Software Defined Radio (SDR) auf einem kleinen Mikrocontroller-Board realisiert. Als geeignete, ja geradezu prädestinierte Hardware bietet sich das Raspberry-Pi-Pico-Board an, das über eine mit 125 MHz getaktete CPU RP2040 (dem hauseigenen Controller der Raspberry Pi Foundation mit zwei Kernen vom Typ Cortex-ARM M0) verfügt. Der ebenfalls vorhandene Analog-Digital-Wandler kann mit 500 kSamples pro Sekunde betrieben werden. Und das alles gibt es für geradezu lächerliche 5 € zu kaufen (siehe Passende Produkte)!
Wir zeigen hier, wie man einen Empfänger für das MSF-Zeitsignal auf 60 kHz in Hard- und Software realisieren kann. Der vollständige Empfänger, ohne Display, aber mit RS232-Ausgabe und Antennenanschluss, ist in Bild 1 zu sehen.
Hardware
Zuerst besprechen wir die Hardware unseres SDR. An unser Pico-Board werden nur wenige zusätzliche Dinge angeschlossen.
Antennenanschluss: Wir verwenden für den Antennenanschluss den Analog-Eingangspin ADC2 (GPIO28, am Pico-Board Pin 34). Der ADC nutzt die internen 3,3 V als Referenzspannung. Daher muss der Pin auf die halbe Referenzspannung als Offset gelegt werden. Dies besorgen die beiden Widerstände in Bild 2. Die Wechselspannungskopplung ist durch den 10-µF-Kondensator C1 gegeben
RS232-Ausgabe: Im einfachsten Fall (ohne LC-Display) verfügt unser Empfänger über eine serielle Schnittstelle (115.200 Bit/s) zur Ausgabe. Die Schnittstelle ist in der Schaltung nach Bild 3 realisiert. Die serielle Ausgabe über die USB-Schnittstelle können wir nicht verwenden, da ihre Interrupts später unsere Software in nicht vorhersagbarer Weise unterbrechen würden.
PWM-DACs: Wenn kein LC-Display angeschlossen ist, kann man mit DACs und der Puls-Weiten-Modulation (PWM) eine einfache Möglichkeit zum Debugging schaffen. Wir haben zwei PWM-DACs mit den dazu gehörigen Tiefpassfiltern wie in Bild 4 aufgebaut.
Mit GPIO 2 und GPIO 3 als PWM-Ausgänge lässt sich beispielsweise das demodulierte Signal zusammen mit dem Bit-Timer darstellen (Bild 5).
LC-Display: The 3.5-inch Arduino 8-bit module ILI9486 (non touch screen version SKU MAR3502 [4]) can be used as the LCD. This 3.5-inch Arduino shield has 480x320 coloured pixels and retails for around €10. Its connection to the Raspberry Pi Pico is shown in Figure 6.
Als LC-Display kommt das 3,5 inch Arduino 8 Bit Module ILI9486 zum Einsatz, und zwar in der Version ohne Touchscreen (SKU MAR3502 bei. Das 3,5-Zoll-Arduino-Shield bietet für einen Preis von etwa 10 € 480x320 farbige Pixel und wird wie in Bild 6 zu sehen an den Raspberry Pi Pico angeschlossen.
Auf dem LC-Display wird das empfangene Signal zusammen mit dem Bit-Timing in Form eines Oszillogramms dargestellt. Über dem Oszillogramm ist die empfangene Zeit-Information im Klartext dargestellt (Bild 7). Will man den Empfänger ohne LCD betreiben, kann man dieses einfach weglassen, ohne dass in der Software etwas geändert werden müsste
Aktive Antenne: Den Antennenanschluss haben wir schon vorbereitet; die Schaltung der aktiven Antenne ist in Bild 8 zu sehen. Sie basiert im Wesentlichen auf dem dualen Operationsverstärker LM6132. Dieser Opamp ist besonders gut geeignet, weil er über die folgenden wesentlichen Eigenschaften verfügt: Betriebsspannung 2,7...24 V, Bandbreite 10 MHz, Rail-to-Rail-Eingang und -Ausgang, niedrige Stromaufnahme von 360 µA/A. Will man den LM6132 durch einen anderen OP ersetzen, muss man also sorgfältig prüfen, ob dieser dann auch geeignet ist.
Programmierung des Eingangsmischers
Nach der Hardware beschäftigen wir uns mit der Programmierung. Der Analogteil des SDR ist aufgebaut wie in Bild 9. Der Raspberry Pi Pico kann in verschiedenen Sprachen programmiert werden. Wir haben uns für C entschieden und benutzen die Entwicklungsumgebung Visual Studio Code von Microsoft auf einem PC unter Windows 10. Schauen wir uns an, wie die verschiedenen Stufen arbeiten.
Die ADC-Sample-Routine wird von der PWM getriggert und 500.000 Mal pro Sekunde per Interrupt aufgerufen. Vom ADC-Wert wird der Offset ADCoffset = 2048 abgezogen und der Wert mit ADCscale = 10 multipliziert (Listing 1).
Die Phase des Lokalen Oszillators (LO-DDS) wird aktualisiert und der Eingangswert mit dem Cosinus (Inphasen- oder I-Signal) und dem Sinus (Quadraturphasen- oder Q-Signal) multipliziert. Die Produkte werden über 1250 Samples summiert (in Isum und Qsum). Die Summenwerte werden danach (in Listing 2) per FIFO an die weitere Verarbeitung weitergereicht, welche dann mit 500000/1250 = 400 Samples/s erfolgt. Diese Rate ist bereits so niedrig, dass man die komplette Weiterverarbeitung mit Double-Variablenwerten vornehmen kann
Die Werte werden aus der FIFO entnommen und jeweils an ein Butterworth-Tiefpassfilter 4. Ordnung mit einer Grenzfrequenz von 3 Hz weitergereicht. Diese niedrige Grenzfrequenz war in der Entwicklung notwendig, weil die Antennne beim Autor starke Störsignale direkt benachbart zum Nutzsignal empfing. Danach erfolgt noch einmal ein Down-Sampling, diesmal um den Faktor 4, so dass danach 100 Samples/s bearbeitet werden.
Die Routine msfSample() in Listing 3 berechnet dann aus den I/Q-Komponenten die Trägeramplitude ampl. Von ampl wird dann der Logarithmus gebildet und wiederum in ampl gespeichert, weil so die Bits besser zu dekodieren sind.
Mit einer rekursiven Filterberechnung erster Ordnung wird aus ampl die Schaltschwelle threshold abgeleitet. Das Signal ampl wird dann mit der Schaltschwelle threshold verglichen und so der logische Empfangspegel sigValue bestimmt. Damit ist der Analogteil der Verarbeitung beschrieben. Nun muss noch aus dem digitalen Empfangssignal berechnet werden, welche Bits empfangen wurden und welcher empfangenen Zeitinformation dies entspricht
Programmierung Digitalteil
Der MSF-Sender sendet Sekundenimpulse aus, wie in Bild 10 gezeigt. Bei Sekunde 0 wird der Träger für 0,5 s unterdrückt. Diesen Impuls nutzt das SDR zur Synchronisation. Die anderen Sekundenimpulse übertragen jeweils die beiden Bits A und B. Zuerst wird der Träger um 0,1 s (entsprechend 10 Samples) abgesenkt. Ist Bit A = 1, bleibt der Träger danach weitere 0,1 s abgesenkt, und ist auch Bit B = 1, erfolgt nochmal eine Absenkung um 0,1 s
In SecondTimer soll ein Timer laufen, der synchron zur Sekunde immer von 0 bis 99 zählt. Die Dekodierungssoftware arbeitet wie folgt: In Duration wird die Impulslänge des aktuellen Impulses gemessen. Wird eine 0,5-s-Absenkung erkannt, wird SecondTimer auf den Wert 50-2=48 gesetzt (die internen Verarbeitungszeiten erfordern diese Verringerung um diese zwei Impulse), damit der Timer SecondTimer jetzt synchron zur Sekunde läuft (Listing 4). Gleichzeitig erfolgt eine Synchronisation der Minute, indem in doMinuteSync() der Wert der aktuellen Sekunde auf 0 gesetzt wird.
Mit Hilfe von SecondTimer wird nun das Empfangssignal an den Mitten von Bit A und Bit B abgetastet (SecondTimer==15 beziehungsweise SecondTimer==25), um die Werte der gesendeten Bits zu bestimmen. Den digitalen Empfangswert geben wir einfach über GPIO-Pin 4 (Pico-Pin 6) aus:
gpio_put(GPIO4, sigValue); // Output sigValue at pico GPIO4=pin 6
Der Wert von SecondTimer wird später auch für das Debugging per PWM ausgegeben, ebenso die Werte von ampl und DAC. Dies geschieht durch folgende beiden Zeilen:
pwm_set_gpio_level(PWM_PIN1, ampl/5.0 ); // Output amplitude
pwm_set_gpio_level(PWM_PIN2, DAC ); // Output timing
Dekodierung der Zeitinformation
Immer wenn eine Synchronisation von SecondTimer erfolgt, ist eine Minute vergangen und wir können die aktuelle Zeitinformation berechnen. Die empfangenen Datenbits stehen in den Werten MSFbits[0..59]. Der Sender sendet die in Bild 11 aufgelisteten Informationen in diesen Bits.
Zeit- und Datumsinformation rekonstruiert man dann einfach wie in Listing 5 für die Stunden und die Minuten.
Die gleichen Informationen, die wir per serieller Schnittstelle ausgesandt haben, stellen wir auch als Text auf dem LC-Display dar. Dies geschieht durch die Befehle in Listing 6.
Die Auswertung der Paritätsprüfung erfolgt wie in Listing 7. Die überwachten Bits sind die A-Bits der übertragenen Information. Die vier Prüfbits sind B-Bits entsprechender Sekundenpulse. Es werden vier Paritätsprüfungen durchgeführt, wobei bis zu 12 Bits von einem Paritätsbit gesichert werden.
Debugsignal
Klassische Superhets mischen das Eingangssignal auf eine niedrige Zwischenfrequenz herunter. Das ZF-Signal wird dann relativ schmalbandig gefiltert. Das ZF-Signal kann man beim MSF60 auch gut auf einem Oszilloskop ansehen. Unser Empfänger mischt das Eingangssignal auf die ZF = 0 Hz herunter. Wenn man ein Wechselstrom-ZF-Signal anschauen will, kann man das 0-ZF-Signal auf die Wechselstrom-ZF heraufmischen. Die Schaltung sieht dann aus wie in Bild 12
Die Software zum Heraufmischen ist in Listing 8 zu sehen.
Als DAC wird ein PWM-Ausgang verwendet. Das mit 100 Hz AM-modulierte ZF-Signal ist in Bild 13 dargestellt.
Damit sind die Konstruktion und der Aufbau des MSF-Empfängers vollständig besprochen. Es wird dabei nur ein Kern der CPU genutzt, so dass noch viel Computing-Power für Erweiterungen übrigbleibt. Man könnte beispielsweise die Bit-Dekodierung fehlertoleranter gestalten. Einen DCF77-Empfänger könnte man weitgehend gleich aufbauen, nur die Bit-Dekodierung müsste man anpassen. Die Empfangsqualität des MSF-Signals ist hier in Aachen natürlich deutlich schlechter als der Empfang des DCF77-Signals mit einem äquivalent aufgebauten SDR. Die Parity-Bits zeigen dementsprechend oft Fehler an. Es kommen aber noch häufig genug fehlerfreie Gruppen an, so dass man ganz gut die Zeichen der Zeit erkennen kann.
Fazit
Mit minimaler Hardware-Ergänzung konnte das Raspberry-Pi-Pico-Board in ein MSF-SDR verwandelt werden. Am aufwändigsten ist der Bau der aktiven Antenne. Doch wenn man den niedrigen Preis des Boards bedenkt, sieht man, wie preisgünstig man heutzutage selbst als „Hobbyist“ solche Konzepte auf der Basis von Mikrocontrollern realisieren kann. Im Zusammenhang mit dem Pico-Board wird oft erwähnt, wie gut sich dieses in Python programmieren lässt. Für Systeme mit 500k Samplerate am Eingang ist Python allerdings nicht geeignet. Mit C aber lässt sich die Mikrocontroller-Hardware gut ansprechen und effizient programmieren. Der RP2040 ist selbst ohne Floating-Point-Unit-FPU leistungsfähig genug, um beispielsweise digitale Tiefpassfilter laufen zu lassen.
(220006-01)
Sie haben Fragen oder Kommentare?
Haben Sie technische Fragen oder Kommentare zu diesem Artikel? Schicken Sie eine E-Mail an den Autor unter ossmann@fh-aachen.de oder kontaktieren Sie Elektor unter redaktion@elektor.de.
Mehr über den Pico
Sehen Sie sich dieses Elektor-Video über den Raspberry Pi Pico an!