Themabewertung:
  • 0 Bewertung(en) - 0 im Durchschnitt
  • 1
  • 2
  • 3
  • 4
  • 5
Einstieg in die RP2040 Controllerreihe ...
#1
Hallo zusammen.

Im Forum haben wir bereits einige Mikrocontroller kennengelernt. Wir kennen die 8-Bit ATmega Familie aus unserem "Einsteigerkurs Microcontrollerprogrammierung" (https://radio-bastler.de/forum/showthread.php?tid=13864) , wir kennen die ebenfalls 8-Bit PIC Familie (PIC10...18) (https://radio-bastler.de/forum/showthread.php?tid=14096), die 32-Bit Controller ESP8266 und ESP32 mit WiFi- Funktionalität kennen wir durch zahlreiche Internetradioprojekte und unser iRadioMini für ESP32 (https://radio-bastler.de/forum/showthread.php?tid=18559).

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.


.jpg   RP2040_Microcontroller.jpg (Größe: 17,83 KB / Downloads: 369)

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.


.jpg   berrybase.jpg (Größe: 10,58 KB / Downloads: 369)
.jpg   reichelt.jpg (Größe: 19,09 KB / Downloads: 369)

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":

.jpg   Raspberry_pi_pico_oben.jpg (Größe: 10,4 KB / Downloads: 369)

Auch einen Arduino Nano gibt es mit RP2040 Bestückung:

.jpg   Arduino_Nano_RP2040_.jpg (Größe: 11,61 KB / Downloads: 369)

Diverse andere Boardhersteller haben auch den RP2040 für sich entdeckt, hier die Marke "SparkFun":

.jpg   SparkFun_Pro_Micro_-_RP2040.jpg (Größe: 14,46 KB / Downloads: 369)


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

    AHB Crossbar für Chip-internes Daten-Routing
        Max. Bus-Bandbreite: 2 GB/s @ 125 MHz Systemtakt

    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


Peripherie:
    30 × GPIO (General Purpose Inputs/Outputs)
    2 × UART (Universal Asynchronous Receiver Transmitter)
    2 × SPI (Serial Peripheral Interface)
    2 × I²C
    16 × PWM (Pulse Width Modulation)
    USB 1.1 (Host- und Device-Modus)
    4-Kanal 12-Bit Analog-Digital-Umsetzer (ADC)
        Nach dem SAR-Prinzip
        Sample-Rate: 500 kS/s @ 48 MHz ADC-Takt

    3-Pin SWD (Serial Wire Debug) Interface


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

Zitat:sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib

, danach checken wir das oben angesprochene PICO-SDK von https://github.com/raspberrypi/pico-sdk oder von meiner Seite https://github.com/BM45/pico-sdk aus:

Zitat:  git clone -b master https://github.com/raspberrypi/pico-sdk.git
  cd pico-sdk
  git submodule update --init


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:

Zitat:#include <stdio.h>
#include "pico/stdlib.h"

int main() {
    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

    stdio_init_all();

    while(true) {
      gpio_put(LED_PIN,1);
      printf("LED AN\n");
      sleep_ms(1000);
      gpio_put(LED_PIN,0);
      printf("LED AUS\n");
      sleep_ms(1000);
    }
    return 0;
}

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 
 
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

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.


.jpg   rp2040_connect_usb.jpg (Größe: 7,35 KB / Downloads: 369)

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.

sudo minicom -b 115200 -o -D /dev/ttyACM0

Das Ergebnis:


.jpg   terminal_out.jpg (Größe: 11,42 KB / Downloads: 367)


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")
Zitieren
#2
Hallo zusammen, weiter soll es gehen.

Betrachten wir einmal die verschiedenen Konzepte von Software Defined Radios (SDR), sehen wir eine große Ähnlichkeit zu analogen Geräten:


.jpg   radio_architectures1.jpg (Größe: 17,98 KB / Downloads: 169)

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"



// for FFT calc
#include "microFFT.h"
#include <math.h>
#include <stdint.h>
#include <string.h>

// for status LED
struct repeating_timer timer;

/* 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];

uint dma_chan_a, dma_chan_b;
int8_t dma_chan_ready;

static void dma_handler_a() {
    dma_chan_ready = 0;
    dma_channel_set_write_addr(dma_chan_a, capture_buffer_a, false);
    dma_hw->ints0 = 1u << dma_chan_a;
}

static void dma_handler_b() {
    dma_chan_ready = 1;
    dma_channel_set_write_addr(dma_chan_b, capture_buffer_b, false);
    dma_hw->ints1 = 1u << dma_chan_b;
}

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

    dma_channel_config dma_cfg_a, dma_cfg_b;

    dma_chan_a = dma_claim_unused_channel(true);
    dma_chan_b = dma_claim_unused_channel(true);

    dma_cfg_a = dma_channel_get_default_config(dma_chan_a);
    dma_cfg_b = dma_channel_get_default_config(dma_chan_b);

    channel_config_set_transfer_data_size(&dma_cfg_a, DMA_SIZE_8);
    channel_config_set_transfer_data_size(&dma_cfg_b, DMA_SIZE_8);

    channel_config_set_read_increment(&dma_cfg_a, false);
    channel_config_set_read_increment(&dma_cfg_b, false);

    channel_config_set_write_increment(&dma_cfg_a, true);
    channel_config_set_write_increment(&dma_cfg_b, true);

    channel_config_set_dreq(&dma_cfg_a, DREQ_ADC);
    channel_config_set_dreq(&dma_cfg_b, DREQ_ADC);

    channel_config_set_chain_to(&dma_cfg_a, dma_chan_b);
    channel_config_set_chain_to(&dma_cfg_b, dma_chan_a);

    dma_channel_configure(dma_chan_a, &dma_cfg_a,
        capture_buffer_a,// dst
        &adc_hw->fifo,  // src
        CAPTURE_DEPTH,  // transfer count
        true            // start now
    );

    dma_channel_configure(dma_chan_b, &dma_cfg_b,
        capture_buffer_b, // dst
        &adc_hw->fifo,  // src
        CAPTURE_DEPTH,  // transfer count
        false           // start now
    );

    dma_channel_set_irq0_enabled(dma_chan_a, true);
    irq_set_exclusive_handler(DMA_IRQ_0, dma_handler_a);
    irq_set_priority(DMA_IRQ_0, 0xFF);
    irq_set_enabled(DMA_IRQ_0, true);

    dma_channel_set_irq1_enabled(dma_chan_b, true);
    irq_set_exclusive_handler(DMA_IRQ_1, dma_handler_b);
    irq_set_priority(DMA_IRQ_1, 0xFF);
    irq_set_enabled(DMA_IRQ_1, true);

    adc_run(false);
}



static void start_ADC_stream() {
    printf("start streaming");
    dma_channel_set_write_addr(dma_chan_a, capture_buffer_a, true);
    dma_channel_set_write_addr(dma_chan_b, capture_buffer_b, false);
    adc_run(true);
}

static void stop_ADC_stream() {
    printf("stop streaming");
    adc_run(false);
    adc_fifo_drain();
    dma_channel_set_write_addr(dma_chan_a, capture_buffer_a, false);
    dma_channel_set_write_addr(dma_chan_b, capture_buffer_b, false);
}

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:

        FFT_WIN_TYP_RECTANGLE
        FFT_WIN_TYP_HAMMING
        FFT_WIN_TYP_HANN
        FFT_WIN_TYP_TRIANGLE
        FFT_WIN_TYP_NUTTALL
        FFT_WIN_TYP_BLACKMAN
        FFT_WIN_TYP_BLACKMAN_NUTTALL
        FFT_WIN_TYP_BLACKMAN_HARRIS
        FFT_WIN_TYP_FLT_TOP
        FFT_WIN_TYP_WELCH
  */
  FFT_Compute(FFT_FORWARD);
  FFT_ComplexToMagnitude();
}

static bool status_led(struct repeating_timer *t) {
    gpio_put(PICO_DEFAULT_LED_PIN, !gpio_get(PICO_DEFAULT_LED_PIN));
    return true;
}


int main() {
    stdio_init_all();
    init_adc_dma_chain();
 
    // status LED
    gpio_init(PICO_DEFAULT_LED_PIN);
    gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
    add_repeating_timer_ms(500, status_led, NULL, &timer);

    start_ADC_stream();

    // FFT Initialization
    FFT_Init(vReal, vImag, FFT_samples, FFT_samplingFrequency);

    while(true) {

         if (dma_chan_ready == 0) {
           for (int i=0; i<CAPTURE_DEPTH; i++) {
         vReal[i] = (float) capture_buffer_a[i] ;
             vReal[i] = vReal[i]/255;
             vImag[i] = 0;
         //printf("%f\n",vReal[i]);
           }
    
           FFT_run();
     }

         if (dma_chan_ready == 1) {
           for (int i=0; i<CAPTURE_DEPTH; i++) {
         vReal[i] = (float) capture_buffer_b[i];
             vReal[i] = vReal[i]/255;
             vImag[i] = 0;
         //printf("%f\n",vReal[i]);
           }
          
           FFT_run();
         }
       
         dma_chan_ready = -1;

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

Code:
uint dma_chan_a, dma_chan_b;
int8_t dma_chan_ready;

static void dma_handler_a() {
    dma_chan_ready = 0;
    dma_channel_set_write_addr(dma_chan_a, capture_buffer_a, false);
    dma_hw->ints0 = 1u << dma_chan_a;
}

static void dma_handler_b() {
    dma_chan_ready = 1;
    dma_channel_set_write_addr(dma_chan_b, capture_buffer_b, false);
    dma_hw->ints1 = 1u << dma_chan_b;
}

Die Daten werden dabei vom ADC-Kanal 0 -> GPIO 26 des Pico erfasst und in die Datenpuffer capture_buffer_a und _b abgelegt:

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

Die CPUs warten nun in einer Endlosschleife bis das Flag dma_chan_ready gesetzt wurde

Code:
   while(true) {

         if (dma_chan_ready == 0) {
           for (int i=0; i<CAPTURE_DEPTH; i++) {
         vReal[i] = (float) capture_buffer_a[i] ;
             vReal[i] = vReal[i]/255;
             vImag[i] = 0;
         //printf("%f\n",vReal[i]);
           }
    
           FFT_run();
     }

         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

PHP-Code:
    channel_config_set_chain_to(&dma_cfg_adma_chan_b);
    channel_config_set_chain_to(&dma_cfg_bdma_chan_a); 

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:

75 kHz

.jpg   75kHzFFT.jpg (Größe: 14,41 KB / Downloads: 165)

125 kHz

.jpg   125kHzFFT.jpg (Größe: 10,13 KB / Downloads: 165)

225 kHz

.jpg   225kHzFFT.jpg (Größe: 9,46 KB / Downloads: 164)

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:


                               R1 (10kOhm) -> +3.3V
                                |
 Antenne -> TPF ->   o  -------------> ADC_Pin
                                |
                               R2 (10kOhm) -> GND


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.


.jpg   LW_FFT_an_Antenne.jpg (Größe: 14,7 KB / Downloads: 163)

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")
Zitieren
#3
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?


.jpg   IQgen.jpg (Größe: 28,56 KB / Downloads: 99)

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:

Code:
#include <math.h>
#include <stdio.h>
#include <fstream>
#include <iostream>

//************************************************
// DDS fOUT = (F_CW/2^M) * fM_CLK
//________________________________________________
// M : bitsize of phase accumulator
// F_CW : 0 - 2^(M-1)
//************************************************

#define DURATION_IN_SEC 1
#define M_CLK 500000 // Master CLK [Hz]; Samplerate

using namespace std;

float LUT[4095];
uint32_t F_CW = 0;

void SinusLUT() {
   double angle = 0.0;
   double step = (2*M_PI)/4095.0;

   for (uint16_t i=0; i<4095; i++) {
      LUT[i]= (sin(angle));
      angle+=step;
   }
}

Da wir den Oszillator ja in seiner Frequenz einstellen wollen, berechnen wir das Frequenzkontrollwort nach obigen Beispiel

Code:
uint32_t setFreqControlWord(uint32_t freq) {
  F_CW = (freq * pow(2,32)) / M_CLK;
  return F_CW;
}

In unserem Hauptteil (main()) erstellen wir jetzt solchen Oszillator der mit 20 kHz schwingen soll:

Code:
int main() {
  uint32_t phasen_acc = 0;
  SinusLUT();
  printf("FCW=%i\n",setFreqControlWord(20000));

  std::ofstream outfile;
  outfile.open("data.dat", std::ofstream::out | std::ofstream::trunc | ios::binary);

  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

float LUT[4095];
uint32_t phasen_acc = 0;

void createLUT() {
   double angle = 0.0;
   double step = (2*M_PI)/4095.0;

   for (uint16_t i=0; i<4095; i++) {
      LUT[i]= (sin(angle));
      angle+=step;
   }
}

float getSinus() {
    float val = LUT[(phasen_acc >> 20)];
    return val;
}

float getCosinus() {
    float val = LUT[((phasen_acc >> 20)+0x400) & 0xFFF];
    return val;
}

void incPhaseAcc() {
  phasen_acc+=F_CW;
}

uint32_t setFreqControlWord(uint32_t freq) {
  F_CW = (freq * pow(2,32)) / M_CLK;
  return F_CW;
}

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:


.jpg   Sig_Test1.jpg (Größe: 9,74 KB / Downloads: 96)

Im oben gezeigten Code fügen wir nach dem die DMA-Kanäle neue Daten geliefert haben unsere Mischung genau nach diesem Prinzip ein:


.jpg   IQgen.jpg (Größe: 28,56 KB / Downloads: 99)


.png   iqmix.png (Größe: 3,55 KB / Downloads: 96)

Code:
   if (dma_chan_ready!=-1) {
           for (int i=0; i<CAPTURE_DEPTH; i++) {
              I=vReal[i] * getCosinus();
              Q=vReal[i] * getSinus();
              vReal[i] = I+Q;
              incPhaseAcc();
           }
       
           FFT_run();
     }

Das Ergbnis sehen wir wieder auf unserem "Behelfsbildschirm" an der Console des RP2040:


.jpg   Sig_Test2.jpg (Größe: 11,24 KB / Downloads: 96)

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")
Zitieren
#4
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.


.png   IQgen.png (Größe: 53,18 KB / Downloads: 45)


.jpg   DDC.jpg (Größe: 29 KB / Downloads: 45)

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.

Eine nähere Betrachtung des Themas "digitale Filter" findet man hier gut aufbereitet: https://www.hs-schmalkalden.de/fileadmin...r_in_C.pdf

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:

http://www.winfilter.20m.com/

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

Filter type: Low Pass
Filter model: Butterworth
Filter order: 2
Sampling Frequency: 500 KHz
Cut Frequency: 5.000000 KHz
Coefficents Quantization: 16-bit

Z domain Zeros
z = -1.000000 + j 0.000000
z = -1.000000 + j 0.000000

Z domain Poles
z = 0.955597 + j -0.041851
z = 0.955597 + j 0.041851
***************************************************************/
#define NCoef 2
#define DCgain 1024

__int16 iir(__int16 NewSample) {
    __int16 ACoef[NCoef+1] = {
        15862,
        31724,
        15862
    };

    __int16 BCoef[NCoef+1] = {
        16384,
        -31313,
        14990
    };

    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];
    }

    //Calculate the new output
    x[0] = NewSample;
    y[0] = ACoef[0] * x[0];
    for(n=1; n<=NCoef; n++)
        y[0] += ACoef[n] * x[n] - BCoef[n] * y[n];

    y[0] /= BCoef[0];
   
    return y[0] / DCgain;
}


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.

Code:
            // I/Q generation with Losc/mixer
              I=adc_input[i] * getCosinus();
              Q=adc_input[i] * getSinus();
              incPhaseAcc();            
            
              // LPF
              I=iir_I(I);
              Q=iir_Q(Q);


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.

So, wo sind wir jetzt?


.jpg   DDC2.jpg (Größe: 31,36 KB / Downloads: 44)

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.
    

              Q
              |
              |         *SOI[t]
              |
              |
------------------------  I
              |
              |
              |
              |


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:

Code:
#include "hardware/pwm.h"
#include "pico/stdlib.h"
#include "hardware/irq.h"


#define DAC_RANGE    256
#define DAC_BIAS    (DAC_RANGE/2)

uint dac_audio;

void initDAC() {
    // Tell GPIO 0 and 1 they are allocated to the PWM
    gpio_set_function(0, GPIO_FUNC_PWM);
    gpio_set_function(1, GPIO_FUNC_PWM);

    dac_audio = pwm_gpio_to_slice_num(0);
    pwm_set_wrap(dac_audio, DAC_RANGE-1);
 
    // Set the PWM running
    pwm_set_enabled(dac_audio, true);
 
}


void sample2DAC(int16_t sample) {
    int a_sample = sample ; // DAC_BIAS;                // Add bias level

    if (a_sample > DAC_RANGE)                        // Clip to DAC range
    a_sample = DAC_RANGE;
    else if (a_sample<0)
    a_sample = 0;

    pwm_set_chan_level(dac_audio,PWM_CHAN_A,sample);
}

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.


.zip   bandscan.wav.zip (Größe: 323,29 KB / Downloads: 2)

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>


Angehängte Dateien
.zip   tools.zip (Größe: 207,31 KB / Downloads: 1)
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")
Zitieren


Gehe zu: