Ab heute wollen wir uns mit dem preisgünstigen 32-Bit Microcontroller RP2040 beschäftigen, den die Raspberry Pi Foundation im Jahr 2021 herausgebracht hat.
Die Ziele der Raspberry Pi Foundation waren ein sehr günstiger Preis bei höher Marktverfügbarkeit. Und tatsächlich ist der RP2040 für knapp 1 Euro bei den üblichen Bezugsquellen erhältlich.
Dieser Preis ist bei ATmega, STM32 oder PICs praktisch nicht erreichbar, wobei sich selbst bei den ATmega- Controllern zur Zeit wegen der Chipkrise ein akuter Mangel auftut. STM32 Controller zu bekommen, ist fast aussichtslos, hier haben selbst Industriekunden Lieferzeiten von 52+ Wochen hinzunehmen. Manche Versionen sind gar erst 2024 wieder lieferbar. Diese Verfügbarkeitssituation ist beim RP2040 zur Zeit eine ganz andere! Der Controller ist trotz SMD, für geübte Löter kein Problem auf eigenen Platinen einzulöten! Der Hobbyist kann aber auch (gegen Aufpreis natürlich) auf bereits gelötete Experimentierplatinen zurückgreifen, welche es in vielfältigen Versionen gibt.
Direkt von der Raspberry Pi Foundation als "Raspberry Pico":
In unserem "Sommerkurs 2022" wollen wir uns als Basis den preiswerten "Raspberry Pico" für Radioexperimente und digitale Signalverarbeitung mit dem RP2040 vornehmen.
Zunächst einmal ein paar technische Daten zum RP2040:
3,3 V Betriebsspannung
133 MHz Dual ARM Cortex-M0+ Cores -> laut Netz sind Übertaktungen von 200, ... teilweise 400 MHz möglich
264 KB SRAM
Für Code und Daten
Physisch partitioniert in sechs unabhängige Bänke für gleichzeitigen parallelen Zugriff durch verschiedene Bus-Master
Binary-Code kann direkt aus externem Speicher über dedizierte Interfaces ausgeführt werden (SPI, DSPI oder QSPI). Ein kleiner Cache erhöht die Performance für typische Applikationen.
QSPI Bus Controller, unterstützt bis zu 16 MB externen Flash-Speicher
DMA Controller
DMA Bus Master sind verfügbar, um sich wiederholende Datentransfers von den Prozessoren auszulagern
2 × On-Chip PLLs, um unabhängig voneinander den System-Takt und den USB/ADC-Takt zu generieren
Dedizierte Hardware für feste Peripherie-Funktionen wie SPI, I²C, UART
Interner Ringoszillator (ROSC), 1,8-12 MHz
Quarz-Oszillator (XOSC) für externe Quarze mit 1-15 MHz
64-Bit System-Timer mit Inkrement von 1 µs
RTC (Real-Time Clock)
2× 4 PIO (Programmable Input/Output) State Machines
Interner Temperatursensor
Watchdog
Brown-out Detection
Der RP2040 kann in ARM Assembler, C/C++ oder MicroPython programmiert werden, ein C/C++ SDK für das Raspberry Pi Pico Board ist verfügbar, das auch das freie Real-Time Betriebssystem FreeRTOS unterstützt.
Wir werden uns hier im Sommerkurs in der Einführung und in allen weiteren Experimenten wie immer die Sprache C/C++ aussuchen und auch das offizielle Pico-SDK nutzen. Fangen wir gleich damit an, eine Entwicklungsumgebung aufzubauen.
Wie immer nutze ich als Ausgangsbasis ein Linux oder Unix-artiges Betriebssystem.
Sollten diverse "Grundwerkzeuge" wie cmake, git, gcc, ... noch nicht aus früheren Projekten vorhanden sein, so installieren wir diese.
Die eigentlichen Entwicklungswerkzeuge für den RP2040 bekommen wir mit
Für unser erstes Testprojekt "exemple1" erstellen wir einen gleichnamigen Ordner. In diesen Ordner kopieren wir die Datei pico_sdk_import.cmake, die im pico-sdk/external - Ordner unserer SDK-Installation liegt.
Als Nächstes erstellen wir im exemple1-Prokjektordner eine Datei mit dem Namen CMakeLists.txt . Eine solche Datei kennen wir bereits aus dem iRadioMini Projekt für ESP32, hier hat Sie folgenden Inhalt:
Zitat:cmake_minimum_required(VERSION 3.13)
# initialize the SDK based on PICO_SDK_PATH
# note: this must happen before project()
include(pico_sdk_import.cmake)
project(exemple1)
# initialize the Raspberry Pi Pico SDK
pico_sdk_init()
# rest of your project
add_executable(exemple1
main.c
)
# Add pico_stdlib library which aggregates commonly used features
target_link_libraries(exemple1 pico_stdlib)
pico_enable_stdio_usb(exemple1 1)
# create map/bin/hex/uf2 file in addition to ELF.
pico_add_extra_outputs(exemple1)
Diese Datei kann als Vorlage für spätere Programme dienen, es muss lediglich der Projektname an allen Stellen abgeändert werden. Auch die Unterstützung für das Bauen der USB/Terminal-Unterstützung des RP2040 ist hier schon aktiviert.
Schreiben wir nun das kurze Testprogramm für unser Einsteigerprojekt. Wir erstellen eine Datei main.c mit folgendem Inhalt:
Wie in jedem C-Programm gibt es eine Hauptfunktion main(). In dieser legen wir mit const uint LED_PIN = PICO_DEFAULT_LED_PIN; den GPIO einer LED fest. PICO_DEFAULT_LED_PIN ist der Standard GPIO einer SMD-LED auf dem Raspberry Pi Pico Board. Mit
wird der Pin als Ausgang geschaltet und initialisiert. Weiterhin wird mit stdio_init_all(); die Terminal- bzw. Consolenausgabe des RP2040 über die USB-Buchse initialisiert. In der Endlos-while-Schleife wird jetzt jede Sekunde die LED an oder aus geschaltet + die jeweilige Textmeldung zur Console geschickt.
Wie compilieren wir jetzt das Programm?
Im exemple1-Projektordner, legen wir einen Ordner build an und wechseln anschließend in dieses Verzeichnis:
mkdir build
cd build
Danach rufen wir cmake mit dem Pfad zur unserem PICO-SDK auf:
cmake .. -DPICO_SDK_PATH=/....../pico-sdk/
Im Anschluß wird noch
make
als Befehl abgesetzt. Der Compiler übersetzt das Programm für den RP2040 jetzt und legt die Binärversion im build-Ordner ab. Wie kommt das Programm auf den RP2040?
Wir verbinden unser Raspberry Pico mit einem USB-Kabel, stecken dieses aber noch nicht in den USB-Port des Computers. Wir drücken (und halten) die Taste BOOTSEL auf dem RP2040 Board fest und stecken das USB-Kabel in die Entwicklungsmaschine. Nach dem loslassen von BOOTSEL wird nun ein USB-Laufwerk im Dateisystem des Entwicklungsrechners gemounted.
Unser kompiliertes Programm exemple1.uf2 ziehen wir jetzt einfach in das USB-Laufwerk, welches den RP2040 repräsentiert.
Unmittelbar nachdem der Kopiervorgang abgeschlossen ist, hängt sich der RP2040 automatisch aus dem Dateiverzeichnis aus und startet mit unserem Programm neu. Auf dem Raspberry Pico Board wird die SMD-LED entsprechend unserer Anweisung in main.c blinken. Den Consolenausgang können wir mit Putty oder einem anderen Terminalprogramm begutachten. Ich verwende hier das kleine Programm minicom.
Das soll als erster Schritt zur Einrichtung einer Entwicklungsumgebung für den RP2040 ausreichend sein. War doch nicht schlimm? Als Nächstes werden wir schauen, wie der preiswerte RP2040 sich in unserer Radiowelt schlagen wird.
<wird fortgesetzt>
Hier bitte keine Fragen / Diskussionen, dafür werden wir ein FAQ zum RP2040 aufmachen.
Ansprechpartner für Umbau oder Modernisierung von Röhrenradios mittels SDR,DAB+,Internetradio,Firmwareentwicklung.
Unser Open-Source Softwarebaukasten für Internetradios gibt es auf der Github-Seite! Projekt: BM45/iRadio (Google "github BM45/iRadio")
Auch in der SDR-Welt gibt es das Konzept des Superhets mit mehreren Stufen (Doppel, Einfach, ...), genauso wie das Konzept des Direktempfängers. Je leistungsfähiger der ADC und der dahinterliegende Signalprozessor, desto einfacher kann das Prinzip werden, sprich, desto weiter wandert der ADC in Richtung Antenne.
Nun haben wir mit dem RP2040 mit Sicherheit kein Rennpferd oder Ferrari unter den Signalprozessoren, dennoch wollen wir mit dem "Direct Sampling" - Prinzip anfangen. Was hat unser RP2040 denn dafür zu bieten? Wie oben schon erwähnt, besitzt der RP2040 einen ADC mit einer Sample-Rate von 500 kS/s bei 48 MHz ADC-Takt. Die maximale Samplerate ergibt sich aus dem festen 48 MHz Takt, der auch für USB benutzt wird, und der Dauer von 96 Zyklen pro Abtastung/Wandlung eines Wertes (48/96=0.5 MSPS). Mit dieser Abtastrate und nach dem Nyquist Shannon Abtasttheorem sind wir somit in der Lage, direkt und "verlustfrei", ein Signal mit einer Bandbreite von 250 kHz zu digitalisieren. Das entspricht im Radiospektrum also den VLF- bis zirka LW-Radiobereich! Später werden wir sehen, daß es mit Tricks auch noch in höhere Frequenzbereiche geht.
Kann der RP2040 aber die dabei anfallende Datenmenge überhaupt bewältigen? 250.000 * 8 bis 12 Bits macht knapp 245 kByte bis 367 kByte pro Sekunde, ... und das bei nur 264 kByte RAM für Daten und Code? Der RP2040 könnte also nicht einmal eine Sekunde des Frequenzspektrums speichern! Entwarnung! Das ist kein Problem. Erstens verarbeiten wir mit dem RP2040 zeitlich viel kleinere Stücke des Signals, der Speicherplatz ist also vorhanden und zweitens schauen wir uns die innere Struktur des Microcontrollers einmal an:
Der Analog-Digital-Wandler ist über ein sehr schnelles internes Bussystem angebunden. Nicht nur das, es gibt eine DMA (Direct Memory Access) Funktionalität! Entsprechend konfiguriert kann der ADC über DMA seine Daten direkt in den ebenfalls am Bus hängenden Speicher schreiben und das, ohne großes Zutun der CPU-Kerne. Die CPU-Kerne brauchen also nur auf ein Signal (Interrupt) zu warten, bis der ADC seine Daten in den Speicher geschrieben hat und in dieser Zwischenzeit können sich die Rechenwerke ganz anderen Aufgaben, nämlich der Verarbeitung fertiger Datenblöcke widmen. Besser geht es nicht.
Fangen wir also in unserem Hauptprogramm an, folgende Funktionalität umzusetzen:
- sample mit voller Geschwindigkeit (500 kS/s) über DMA in den Speicher
- immer wenn 1024 Abtastwerte gewonnen wurden, mache eine FFT über diese Werte
- gebe das Ergebnis der FFT (das Frequenzspektrum) über die serielle Schnittstelle aus, denn wir haben ja kein Display angeschlossen
Code:
#include <stdio.h>
#include "pico/stdlib.h"
// For ADC input:
#include "hardware/adc.h"
#include "hardware/dma.h"
#include "hardware/irq.h"
/* FFT Parameters and Variables */
#define FFT_samples 1024 //This value MUST ALWAYS be a power of 2
#define FFT_samplingFrequency 500000
//#define FFT_ampFactor 8.35f
float vReal[FFT_samples];
float vImag[FFT_samples];
// ADC Parameters and Variables
#define CAPTURE_CHANNEL 0 // Channel 0 is GPIO26
#define CAPTURE_DEPTH 1024
uint8_t capture_buffer_a[CAPTURE_DEPTH];
uint8_t capture_buffer_b[CAPTURE_DEPTH];
static void init_adc_dma_chain() {
adc_gpio_init(26 + CAPTURE_CHANNEL);
adc_init();
adc_select_input(CAPTURE_CHANNEL);
adc_fifo_setup(
true, // Write to FIFO
true, // Enable DREQ
1, // Trigger DREQ with at least one sample
false, // No ERR bit
true // Shift each sample by 8 bits
);
adc_set_clkdiv(0);
void RemoveOffset(float* array, size_t array_len)
{
// calculate the mean of vData
float mean = 0;
for (size_t i = 0; i < array_len; i++)
{
mean += array[i];
}
mean /= array_len;
// Subtract the mean from vData
for (size_t i = 0; i < array_len; i++)
{
array[i] -= mean;
}
}
void FFT_run(){
RemoveOffset(vReal, CAPTURE_DEPTH);
FFT_Windowing(FFT_WIN_TYP_BLACKMAN, FFT_FORWARD);
/*
The possible windowing options are:
// sende FFT Result an serielle Schnittstelle
printf("AA BB\n"); // neuer FFT Frame
for (int i=0; i<CAPTURE_DEPTH/2; i++) { // nur die erste Hälfe der FFT bis zur Spiegelung
printf("%f\n",vReal[i]);
}
sleep_ms(20);
} // while(true) {
stop_ADC_stream();
return 0;
}
Um die Funktionalität zu erreichen, legen wir zunächst zwei DMA-Kanäle an, über die jeweils zwei Datenpuffer abwechselnd vom ADC gefüllt werden. Wenn das passiert ist, soll sich ein Handler darum kümmern und uns über einen Flag melden, daß die nächsten 1024 Datenwerte von der "Antenne" bereitstehen:
if (dma_chan_ready == 1) {
for (int i=0; i<CAPTURE_DEPTH; i++) {
...
und füllen nach jedem Pufferwechsel (der ADC samplet im Hintergrund natürlich weiter) die Arrays (Realteil, Imaginärteil=0) für eine Fast Fourier Transformation auf. Danach wird noch schnell mit FFT_run() die FFT durchgeführt und
Code:
dma_chan_ready = -1;
// sende FFT Result an serielle Schnittstelle
printf("AA BB\n"); // neuer FFT Frame
for (int i=0; i<FFT_samples/2; i++) { // nur die erste Hälfe der FFT bis zur Spiegelung
printf("%f\n",vReal[i]);
}
das Ergebnis über die serielle Schnittstelle verschickt. In init_adc_dma_chain() wird der RP2040 mit seiner ganzen Peripherie (ADC, DMA) konfiguriert, auch das Ping-Pong zwischen den Puffern wird hier angelegt
Mit start_ADC_stream() und stop_ADC_stream() wird der ganze Prozess gestartet oder gestopt. Die Funktion RemoveOffset() entfernt noch einen vorhandenen DC-Anteil im Signal, den wir sonst in Spektrum der FFT auch sehen würden. Die Funktion status_led() ist letztentlich nur ein Timer-gesteuertes Blinklicht auf dem Pico-Board, welches uns den Betriebszustand anzeigen soll.
Was passiert nun nach der Transformation vom Zeit- in den Frequenzbereich durch die FFT und wie werden die Daten angezeigt, ein Display haben wir ja im Moment nicht? Das Ergebnis der FFT, also das Spektrum, schicken wir über die serielle Schnittstelle vom Microcontroller zum Programmiersystem. Dort habe ich ein kleines Tool geschrieben, eine Art Ersatzbildschirm, das die Daten zur Anzeige bringt.
Hier sehen wir ein paar Aufnahmen. Auf den ADC des RP2040 wurden mit einem Signalgenerator sehr lose gekoppelt folgende Signale gegeben:
Die kleinen anderen Frequenzanteile im Spektrum stammen von Einsteuungen des Entwicklungssystems (Monitor, ...).
Natürlich müssen wir vor dem Eingang des ADC eine Tiefpassfilterung vornehmen. Ich glaube dem Leserkreis muss ich nicht erklären was ein Tiefpass ist und wie Tiefpässe berechnet werden bzw. wie man an Online-Rechner für solche Filter kommt.
Ebenso rücken wir den ADC-Eingang auf so ein Potential, daß nicht nur die "positive Welle" aufgezeichnet wird, sondern auch der negative Teil. Hier reicht ein einfacher Spannungsteiler mit 2x10kOhm aus, also ungefähr so:
Da die Pegel an der Antenne natürlich viel zu klein sind, ist ein Vorverstärker nötig, damit ein guter Empfang möglich ist. Bei diesen Bandbreiten (250 kHz) sind keine besonderen HF-Anforderungen an die Transistoren gestellt. BC547 wird es genauso machen wie BF199 oder DDR-Typen SF.... , auch Verstärkerschaltungen mit Operationsverstärker sind hier natürlich möglich. Mit einem solchen zweistufigen Transistorverstärker und gut 2m Draht bekomme ich nun folgendes Bild.
Zu empfangen ist ist ganz klar DCF77 sowie Signale der Rundsteuersender von MB, DWD ist zu sehen, BBC nur zu erahnen. Aber 2m Draht sind eben keine gute LW-Antenne. Hier gibt es weiteren Optimierungsbedarf, eine Aktivantenne wie die MiniWhip wird da ein ganz anderes Bild zeigen!
Den Quellcode für den RP2040 und das FFT-Anzeigetool stelle ich hier noch rein, dann kann jeder selbst mal experimentieren.
Im nächsten Teil werden wir uns Stück für Stück mit digitalen Mischern, I/Q-Signalerzeugung, Digital-Down-Converter (DDC), digitalen Filtern und Demodulatoren beschäftigen, also die ganze Kette an Signalverarbeitung im Basisband und schauen wie sich der RP2040 auf diesem Gebiet bewähren wird.
Gruß
Bernhard
<wird fortgesetzt>
Hier bitte keine Fragen / Diskussionen, dafür werden wir ein FAQ zum RP2040 aufmachen.
Ansprechpartner für Umbau oder Modernisierung von Röhrenradios mittels SDR,DAB+,Internetradio,Firmwareentwicklung.
Unser Open-Source Softwarebaukasten für Internetradios gibt es auf der Github-Seite! Projekt: BM45/iRadio (Google "github BM45/iRadio")
13.05.2022, 13:48 (Dieser Beitrag wurde zuletzt bearbeitet: 21.05.2022, 12:30 von Bernhard45.)
Hallo zusammen, es soll ein Stück weitergehen.
Im letzten Teil hatten wir ja gesehen, wie wir mit dem ADC ein volles 250 kHz Spektrum digitalisieren und anzeigen können. Jetzt ist es natürlich praktisch, wenn wir ein Signal in diesem Spektrum so verschieben (mischen) können, daß wir es in eine andere Frequenzlage (zum Beispiel in die 0-Lage) bekommen. Zusätzlich wollen wir ja auch die I/Q-Signale aus den angetasteten Werten gewinnen. Welche Elemente brauchen wir dafür?
Nun wir brauchen als erstes mal einen Oszillator den wir auf eine Mischfrequenz einstellen können. Da wir hier im "Digitalen" agieren, wird dieser numerisch einstellbarer Oszillator in Software erstellt. Dazu gibt es verschiedenen Methoden wie den CORDIC-Algorithmus (speicherplatzeffizient) oder das DDS-Prinzip (Direct Digital Synthesis). DDS kennen wir ja schon! Das wird in ICs wie AD9833, 35 oder AD9850/1 usw. eingesetzt.
Das Herzstück der DDS ist eine große Tabelle mit vorausberechneten Sinus-Werten. Über einen Registerwert, der als Zeiger/Index auf die Tabelleneinträge dient, wird vom Signalprozessor durch diese Tabelle gesprungen und der passende vorausberechnete Wert (Amplitude/Phase) ermittelt, der dann das Signal zu diesem momentanen Zeitpunkt im Oszillator beschreibt. Die Schrittweite, also das Frequenzkontrollwort, berechnet sich aus der gewünschten Frequenz des Oszillators, der Bitbreite des o.g. Registers und der Frequenz der Masterclock (Samplerate) mit dem der Softwareoszillator getaktet wird.
Beispiel:
fo = Ausgabefrequenz des Oszillators
FCW = Frequenzkontrollwort
fMaster = Systemtakt des Oszillators
M = Bitbreite des Zeigerregisters
Für die Ausgangsfrequenz ergibt sich:
fo = (FCW/2^M)*fMaster
umgestellt nach FCW ergibt sich also:
FCW = (fo*2^M) / fMaster
Die sehr schnelle Wertverfügbarkeit des DDS-Oszillators (halt nur ein Zugriff auf eine vorausberechnete Tabelle) wird mit dem Nachteil des Speicherverbrauchs für diese Look-Up-Table erkauft. Die Frequenzauflösung und das SNR steigt mit der Bitbreite des Registers und der Tiefe der Tabelle.
Eine 8 Bit-Tabelle hat für einen normalen Fließkommatyp (float -> 4 Byte) eine Größe von 2^8 * 4 Byte, also 1024 Byte = 1 kByte.
Naja geht ja noch, erhöht man auf 12-Bit ist man aber schon bei 16 kByte, bei 16 Bit bei 256 kByte, bei 32 Bit aber schon bei 16 Gigabyte!
Sparen kann man noch, in dem man nur 1/4 der Tabelle, also nur 90° einer ganzen Sinuswelle abbildet. Durch die Periodizität des Signals lassen sich die anderen Quadranten ja praktisch aus dieser Kurztabelle ableiten.
Für unseren Test auf dem RP2040 wollen wir uns erst einmal einem 12 Bit DDS- Oszillator zuwenden. Da wir den RP2040 ja in C programmieren, können wir die Entwurfsarbeit bequem auf einem PC mit C-Compiler durchführen, zunächst mit Kommazahlberechnungen.
Erstellen wir also ein kleines PC-Programm, welches eine 12 Bit tiefe LUT (4096 Werte) vorausberechnet:
for (unsigned long long i = 0;i<M_CLK*DURATION_IN_SEC; i++) {
outfile.write((char*) &LUT[(phasen_acc >> 20)], sizeof(uint16_t));
// outfile.write((char*) &LUT[((phasen_acc >> 20)+0x400) &0xFFF], sizeof(float)); // 90° Versatz
phasen_acc+=F_CW;
}
outfile.close();
return 0;
}
Das Ergebnis (eine Sekunde lang 20 kHz) soll in eine Datei zur Überprüfung geschrieben werden. Wir sehen auch, daß wir aus der Sinus-Tabelle mit einem kleinen "Zugriffstrick" auch gleich den Cosinus ableiten können. Wir stellen den Zugriffsindex einfach um Pi/4, also 0x400=1024 Werte, gleich 90 Grad weiter. Nach jedem Durchlauf wird das Zugriffsregister um das errechnete Frequenzkontrollwort weitergesetzt.
Nach dem Compilieren mit gcc dds12.cpp -o dds12 -lm -lstdc++ rufen wird das erstelle Programm dds12 auf dem PC auf, die Datei wird erstellt.
Jetzt laden wir die Datei in ein Analysewerkzeug, ich nehme hier mein MATLAB welches ich zentral installiert habe. Alte Versionen von MATLAB bekommt man im Internet, neuere Versionen können auch als 30 Tage Demoversion von der Herstellerseite bezogen werden. Kostenfreie Open-Source-Alternativen gibt es auch einige, SCILab oder Octave sei hier stellvertretend genannt.
Nach dem Start von MATLAB sehen wir ein Arbeitsblatt auf dem wir unsere Berechnungen starten können.
Als erstes wollen wir die vom Software-Oszillator erzeugte "Wellendatei" ins MATLAB-System einladen. Das geht mit den abgebildeten Befehlen:
Als Kontrolle können wir uns auch mal einen Blick in die Datenreihe gönnen. Einfach im Workspace-Browser den Namen der Datenreihe anklicken.
Zur Kontrolle plotten wir einmal das vom Oszillator erzeugte Signal:
Ja das sieht gut aus, aber welche Frequenz hat das erzeugte Signal? Wir machen eine FFT über die Datenreihe und sehen das Ergebnis:
Im Spektrum sieht man die Antwort. Wir haben eine Frequenzkomponente bei 20000 Hz = 20 kHz. Also genau das, was wir im Programm mit
Code:
setFreqControlWord(20000)
auch erreichen wollten.
Unser Local-Oszillator, der nachher auf die Mischer auflaufen soll, funktioniert also. Machen wir uns den Spaß und mischen einfach mal ein HF-Signal gleich in MATLAB damit. Testweise erzeugen wir mal auf 77.5 kHz (also DCF77) einen Träger mit 0.7er Amplitude und genau 1 Sekunde (also 500.000 Samples) lang. Den Rechenweg und meine Eingaben seht Ihr auf dem Arbeitblatt. Wieder schaue ich mir das Signal an.
Und die FFT dazu:
Jawoll, das klappt, wir haben 77.5 kHz anliegen.
Jetzt wollen wir dieses Signal mit unserem Software-Oszillator testweise mischen, was mathematisch ja nur eine Multiplikation ist. Ihr sehr die Rechnung im Arbeitsblatt und gleich dazu die FFT des Mischerausgangs:
Wir sehen jetzt im Spektrum Frequenzanteile bei 77.5 - 20 kHz und 77.5 + 20 kHz, also das was wir erwartet haben. Jetzt können wir praktisch den oben am PC erstellten Softwareoszillator auf unser Zielsystem übertragen.
Ich habe das Modulweise (ähnlich wie beim iRadioMini für ESP32) gemacht, also es gibt eine Datei dds.h und dds.c . Zunächst rechnen wir das alles mal mit Kommazahlen, auch wenn wir kein spezielles Rechenwerk im RP2040 haben. Mal sehen ob der Controller das packt, später bei umfangreicheren Berechnungen, greifen wir dann (je nach Ausgang diesese Tests) auf Ganzzahlen zurück.
Code:
#include <math.h>
#include <stdio.h>
//************************************************
// DDS fOUT = (F_CW/2^M) * fM_CLK
//________________________________________________
// M : bitsize of phase accumulator
// F_CW : 0 - 2^(M-1)
//************************************************
#define M_CLK 500000 // Master CLK [Hz]; Samplerate
uint32_t F_CW = 1; // Frequency Control Word
Es gibt eine Funktion die eine LUT erzeugt. Mit getSinus() und getCosinus() bekommt man die vorausberechneten Werte entsprechend des aktuellen Registerwerts. Mit setFreqControlWord stellt man die Frequenz des Oszillators ein, daraus wird das Frequenzkontrollwort berechnet. incPhaseAcc() "taktet" den Softwareoszillator, was natürlich zu einer Erhöhung des Registerwerts führt.
Jetzt wollen wir natürlich auch mit dem RP2040 mal richtige HF-mischen.
Zunächst legen wir wieder ein Testsignal aus einem Signalgenerator auf unseren RP2040:
Auch hier haben wir wieder das von MATLAB gezeigte Ergebnis. Zusätzlich sehen wir im Spektrum auch den Software-Oszillator mir seiner Lo-Frequenz.
Eigentlich ganz überschau- und nachvollziehbar? Hier habt Ihr auch mal gesehen, wie ich solche Probleme zunächst einmal fern vom Zielsystem am PC durchspiele und dann, wenn die Lösung funktioniert, auf das Zielsystem übertrage.
Wir können jetzt, zum Beispiel mit einem Rotary-Encoder am RP2040 schon mal bequem im Frequenzspektrum "durchkurbeln". In den nächsten Schritten werden wir dann digitale Filter, DDC (Verringerung der Abtastrate mit Systemgewinn) und digitale Demodulatoren sehen.
Bis dahin, viel Spaß beim selbst Experimentieren.
Gruß
Bernhard
<wird fortgesetzt>
Ansprechpartner für Umbau oder Modernisierung von Röhrenradios mittels SDR,DAB+,Internetradio,Firmwareentwicklung.
Unser Open-Source Softwarebaukasten für Internetradios gibt es auf der Github-Seite! Projekt: BM45/iRadio (Google "github BM45/iRadio")
Gestern, 13:22 (Dieser Beitrag wurde zuletzt bearbeitet: Gestern, 20:28 von Bernhard45.)
Hallo zusammen,
heute wollen wir einen Schritt weitergehen. Oben haben wir ja gesehen, wie sich der RP2040 trotz fehlender FPU mit Kommawerten schlägt und uns so sogar einen ganzen 250 kHz breiten Ausschnitt auf den Bildschirm holen kann. Für die weiteren Schritte werden wir aber zurück auf Ganzzahlberechnungen kommen. Diese Zahlen sind für einen solche Microcontroller deutlich schneller zu handhaben (wir haben also noch Leistungsreserven für andere Spielereien).
Unser DDS von oben auf Ganzzahlzahlen umzustellen ist nicht schwer. Aus den Float in unser Look-Up-Table wird ein vorzeichenbehafteter Ganzzahlentyp und den Sinus in der LUT berechnen wir nicht für Werte von -1 bis 1, sondern durch Multiplikation mit (Wertebereich/2)-1 ganzzahlig. Die Funktionen getSinus und getCosinus geben jetzt kein Float, sondern ein Integer zurück, am 32-Bit breiten Phasenakkumulator ändert sich nichts.
Das DMA-gesteuerte Abtasten der HF, legt die Werte von DC-Anteilen befreit nun in einen ADC-Buffer vom Typ int16_t ab:
Code:
// ADC Parameters and Variables
#define CAPTURE_CHANNEL 0 // Channel 0 is GPIO26
#define CAPTURE_DEPTH 1024
uint8_t capture_buffer_a[CAPTURE_DEPTH];
uint8_t capture_buffer_b[CAPTURE_DEPTH];
int16_t adc_input[CAPTURE_DEPTH]; // dsk_task_buffer filled by capture_buffer_a and _b
int16_t I,Q;
Die I/Q-Signalgenerierung aus dem vorherigen Post bleibt praktisch unverändert:
Code:
// dsp tasks - signal processing every n=CAPTURE_DEPTH samples // Fs = 500 kHz
if (dma_chan_ready==3) {
dma_chan_ready = 4;
uint8_t c = 0;
for (int i=0; i<CAPTURE_DEPTH; i++) {
// I/Q generation with Losc/mixer
I=adc_input[i] * getCosinus();
Q=adc_input[i] * getSinus();
incPhaseAcc();
Da wir die Frequenz des DDS ja einstellen können, kann das im 250 kHz-breiten Spektrum befindliche signal of interest (SOI) - zum Beispiel ein Radiosender - in die Basisbandlage verschoben werden. Ab diesen Zeitpunkt, sind wir in der Lage mit einer niedrigeren Abtastrate zu arbeiten.
Ein beispielsweise 10 kHz breites SOI benötigt nach dem Abtasttheorem ja nur eine Samplerate von 20 kHz, bzw. 20 kS/s und nicht mehr die vollen 500 kHz, die der ADC in der Lage ist zu liefern. Wir sparen für die Signalverarbeitung des SOI im Basisband also eine Menge Daten und Rechenoperationen ein.
Vor der Dezimierung der Samplerate müssen wir das SOI von Frequenzanteilen befreien, die gegen das Abtasttheorem verstoßen, kurz um, wir müssen den I und Q - Signalzweig digital filtern. Der Entwurf eines solchen Filters geht heutzutage ziemlich fix. Wir können den Filter mit seinen Koeffizienten in Matlab berechnen lassen, oder eine der unzähligen Filterentwurf-Softwares nutzen.
Am Markt gibt es viele kostenpflichtige aber auch kostenlose Filter-Design Werkzeuge, die gleich auch noch den Filter als C, C++, VHDL, Verilog, ... Code ausgeben. Einfach mal eine Suchmaschine deiner Wahl betätigen. Für unser Demo-SDR möchte ich auf das kostenlose WinFilter verweisen:
Dieses Filterentwurfswerkzeug läuft entgegen seines Namens nicht nur auf Windows-Systemen, sondern kann mit wine quasi auf allen Unix, Linux und MacOS-Systemen genutzt werden. Ich packe das Werkzeug gleich noch in den Anhang.
Nach dem Start von WinFilter begrüßt uns folgendes GUI:
Nach der Auswahl des Filterprinzips (IIR oder FIR - siehe PDF der HS Schmalkalden) können wir den Filtertyp festlegen. Also ob der Filter ein Tiefpass, Hochpass , ... werden soll. Ebenso können müssen die Filtereckdaten festgelegt werden.
Nach der Berechnung des Filters sehen wir im Arbeitsplatzbereich der WinFilter-Software die Filterkennlinien.
Für unser SDR brauchen wir vor der Dezimierung natürlich einen Tiefpassfilter. Die WinFilter-Software ist in der Lage über den Menüpunk "Output" gleich den C-Code eines vorher berechneten Filters zu erzeugen.
Die Ausgabe von WinFilter könnte dann zum Beispiel so aussehen:
Code:
/**************************************************************
WinFilter version 0.8
http://www.winfilter.20m.com
akundert@hotmail.com
static __int32 y[NCoef+1]; //output samples
//Warning!!!!!! This variable should be signed (input sample width + Coefs width + 2 )-bit width to avoid saturation.
static __int16 x[NCoef+1]; //input samples
int n;
//shift the old samples
for(n=NCoef; n>0; n--) {
x[n] = x[n-1];
y[n] = y[n-1];
}
Den Code könnte (und sollte) man noch etwas aufräumen. Die Filterkoeffizienten zum Beispiel könnte man aus der Funktion iir herausnehmen, dann wären Sie für mehrere Filterinstanzen zu erreichen und bräuchten auch nur einmal im Speicher des Microcontrollers gehalten werden. Da wir sowieso 2 Filter benötigen, für den I- und den Q-Zweig, müssen natürlich auch die x und y - Wertereihen zweimal angelegt werden, zum Beispiel so:
Code:
static int32_t y_I[NCoef+1]; //output samples
//Warning!!!!!! This variable should be signed (input sample width + Coefs width + 2 )-bit width to avoid saturation.
static int16_t x_I[NCoef+1]; //input samples
static int32_t y_Q[NCoef+1]; //output samples
//Warning!!!!!! This variable should be signed (input sample width + Coefs width + 2 )-bit width to avoid saturation.
static int16_t x_Q[NCoef+1]; //input samples
Die bisherige Funktion iir wird gedoppelt und einmal iir_I und einmal iir_Q benannt. Damit wären wir auch schon fertig mit der Filterbearbeitung. In unserer Hauptsignalverarbeitungsroutine (while-Schleife in main.c) können die Filter dann so angewendet werden.
Unsere I/Q-Signalzweige sind jetzt von den unerwünschten Frequenzanteilen befreit und können in der Samplerate dezimiert werden. Ich benutze hierfür einen Dezimierungsfaktor von 32, was bedeutet, daß die am Anfang bestehende Samplerate von 500 kHz einfach durch 32 geteilt wird. Die neue Samplerate beträgt dann 15.625 kHz. Die Dezimierung selbst ist ein einfacher Schritt. Wir übernehmen aus unserer I und Q - Wertereihe einfach nur jeden 32. Wert und verwerfen den Rest. Dafür eignet sich wunderbar der Modulo-Operator ->
Code:
// Decimation by 32 -> new Fs 500.000/32 = 15.625 kHz
if (i%DECIMATION_FACTOR == 0) {
Am Rande erwähnen sollte man, daß man durch Dezimierung eines überabgetasteten Signals durch solche einfachen Sachen einen Systemgewinn erhält. Jede Verdoppelung (2,4,8,16,....) führt dabei zu 3dB mehr Dynamikumfang.
Wir haben jetzt hinten in der Basisbandverarbeitung fertig dezimierte und gefilterte I/Q Signale des SOI. In der SDR Welt gibt es den Satz "Gib mir ein I, gib mir ein Q und ich demoduliere dir die ganze Welt". Das SOI sieht zu einem bestimmten Zeitpunkt t im Konstellationsdiagramm also so aus.
Hier können wir jetzt direkt unsere Demodulatoren mathematisch betrachten. Für eine AM (Hüllkurve) interessiert uns ja der Wert der Amplitude bei SOI[t] vom Ursprung und an den kommen wir ganz einfach mit dem "Phytagoras".
SOI[t]=Wurzel(I*I+Q*Q)
Und genau das ist unsere Rechencode für einen AM-Demodulator. Wenden wir diese Rechenvorschrift auf jedes dezimiert, gefilterte I/Q-Wertepaar an, haben wir das AM-demodulierte Audiosignal des SOI mit der Samplerate von 15.625 kHz. Mit diesen Daten und dieser Samplerate füttern wir jetzt einen DAC an oder im RP2040.
Code:
dac_output[c] = sqrt(I*I+Q*Q);
c++;
Da der RP2040 keinen HW-DAC besitzt, dafür ein sehr leistungsfähiges PWM-System, schreiben wir alle 1/15.625 s, also alle 64 us ein Sample in einen PWM-Kanal.
Es gibt mehrere Wege das zu tun, einer wäre ein Timer der eine Handlerfunktion triggert und das übernimmt:
Code:
// DAC
initDAC();
add_repeating_timer_us(64, dac_output_handler, NULL, &dac_timer); // new audio sample every 1/15.625 sec.
Der PWM-DAC wird hier durch folgenden Code (DAC.c und .h) realisiert:
Wie so ein PWM-DAC an einem Microcontroller zu nutzen und zu verschalten ist, hatten wir ja als Thema in unserem Microcontrollerkurs! Hier kann das Audiosignal nun nach der Rekonstruktion direkt für einen Kopfhörer oder eine kleine Transistorendstufe am GPIO entnommen werden. Natürlich könnte man auch andere (leitungsfähigere) DACs an den RP2040 anbinden.
Einen kleinen hörbaren "Bandscan" (nicht wirklich, ich habe mit einem Encoder von Hand gekurbelt), beginnend von 153 kHz bis 235 kHz, mit AM-Demodulator habe ich mal angefügt.
Ihr müsst die Datei von bandscan.wav.zip wieder nach bandscan.wav umbenennen, da im Forum keine Dateien mit der Endung .wav erlaubt sind.
Eigentlich ganz erstaunlich was so ein 1-Euro Chip als Direktsampling-SDR auf LW schon leistet. Natürlich sind andere Demodulatoren (LSB/USB) und weitere Filterbandbreiten (man braucht ja nur mehrere Filter entwerfen und diese über eine switch-Anweisung auswählbar zu machen) möglich. DWD/RTTY, DCF77, SAQ, ... wäre alles mit so einem kleinen Microcontroller machbar. Erstaunlich....
Was fehlt denn noch? Nur so ein paar Ideen:
- vielleicht ein TFT-Display direkt am RP2040 mit Frequenzanzeige
- weitere Encoder für Audiolautstärke, Filterbandbreite, ....
- im Frontend vor dem ADC ein Mischer mit HW-DDS (Signal vielleicht aus dem RP2040) um den Frequenzbereich des SDR deutlich zu vergrößern -> KW + ...
Viel Spaß beim Experimentieren
Gruß
Bernhard
<wird fortgesetzt - Fragen/Diskussionen bitten in FAQ>
Ansprechpartner für Umbau oder Modernisierung von Röhrenradios mittels SDR,DAB+,Internetradio,Firmwareentwicklung.
Unser Open-Source Softwarebaukasten für Internetradios gibt es auf der Github-Seite! Projekt: BM45/iRadio (Google "github BM45/iRadio")