RPi Pico – I2S DIY made easy      

von Martin Müller                    

Elektronik-Labor  Projekte  Mikrocontroller  Raspberry     







Man findet im Netz, die eine oder andere Library, die dem RPi Pico eine I2S-Schnittstelle zur Verfügung stellt. Dazu wird üblicherweise eine PIO-Einheit des RPi-Pico verwendet. Man kann eine solche Schnittstelle auch selber programmieren. Das hat den Vorteil, dass man die I2S-Schnittstelle relativ einfach an die eigenen Bedürfnisse anpassen kann. Hat man verstanden, wie die PIO-Einheit mit ihren Statemachines arbeitet, ist es leicht auch ähnliche Schnittstellen (z.B. DSP oder right- und left justified mode) zu implementieren.

Beispielhaft soll hier eine Möglichkeit gezeigt werden, eine I2S-Schnittstelle mit 2 x 16 Bit Auflösung, 48 kHz Sampling-Rate (fs) und einer Master-Clock-Frequenz mit dem 256-fachen der Sampling-Rate (256fs) zu programmieren.

Voraussetzungen:

Die Schnittstelle wird mit dem von der  Raspberry Pi Fundation zur Verfügung gestellten C/C++ SDK programmiert. Dazu ist es erforderlich das genannte SDK samt Toolchain zu installieren. Eine Anleitung findet sich hier
https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf

Optimalerweise hat man es auch schon einmal geschafft, ein Pi-Pico-Projekt selbstständig zu erstellen und dieses auf dem Prozessor laufen zu lassen. Um dem Code des .c-files nicht unnötig aufzublähen, wurden nur so viele Funtionen, Variablen und Konstanten definiert, wie unbedingt erforderlich. Deshalb mag es so erscheinen , als ob an der einen oder anderen Stelle „magic numbers“ verwendet werden. Falls erforderlich, kann ein Blick in die Online-Dokumentation des SDK für Klarheit sorgen:
https://raspberrypi.github.io/pico-sdk-doxygen/

I2S


Die I2S-Schnittstelle besteht aus mindestens 3 Datenleitungen.
LRCLK: hat in diesem Fall 48 kHz. Ist der Pegel dieser Leitung LOW, werden die über über DATA und BCLK eingelesenen Daten dem linken Audiokanal zugeordnet. Ist der Pegel HIGH, so handelt es sich um Informationen für den rechten Kanal.
BLCK: Die aufsteigende Flanke, liest den Zustand der Datenleitung (DATA) ein. Die Übertragung beginnt mit dem höhstwertigsten Datenbit (MSB). Dieses wird mit der zweiten aufsteigenden BLCK-Flanke nach einem Potentialwechsel von LRCLK eingelesen.
DATA: stellt die einzulesenden Daten zur Verfügung.

MCLK: Zum Betrieb digitaler Filter benötigen manche I2S-Bausteine noch einen zusätzlichen Masterclock, der üblicherweise ein Vielfaches (hier das 256 fache) des LCRK beträgt.

Wichtig ist, dass die 4 Signalleitungen sauber miteinander synchronisiert sind.

PIO und State Machines



Es trifft sich daher gut, dass eine PIO-Einheit über vier sog. Statemachines verfügt. Jede Statemachine kann ein eigenes kurzes Programm abarbeiten. Günstigerweise kann man die Statemachines miteinander synchronisieren. Wie schon geschildert, stehen die Takte der Signalleitungen LRCLK, BCLK und MCLK in festen Verhältnissen zueinander. Auf DATA muss dann zum richtigen Zeitpunkt das entsprechende Datenbit erscheinen.

Programmdateien

 

Man benötigt vier .pio-Dateien. Sie enthalten den Assemblercode für die vier Statemachines. Sie heißen hier MCLK.pio, LRCLK.pio, BCLK.pio und DATA.pio.
i2sdiy.c enthält das eigentliche Programm.
CMakeLists.txt enthält unter anderem Anweisungen, wie die .pio-Dateien in das Hauptprogramm eingebunden werden.
Im Projektverzeichnis befindet sich auch noch die Datei pico_sdk_import.cmake. Sie enthält Informationen zum SDK und muss in der Regel nicht projektspezifisch angepasst werden.

 

CMakeLists.txt

 

 

pico_generate_pio_header(${PROJECT_NAME}
${CMAKE_CURRENT_LIST_DIR}/LRCLK.pio)
pico_generate_pio_header(${PROJECT_NAME}
${CMAKE_CURRENT_LIST_DIR}/DATA.pio)
pico_generate_pio_header(${PROJECT_NAME}
${CMAKE_CURRENT_LIST_DIR}/BCLK.pio)
pico_generate_pio_header(${PROJECT_NAME}
${CMAKE_CURRENT_LIST_DIR}/MCLK.pio)

Hier werden die .pio-Dateien eingebunden.


set(PICO_SDK_PATH "/home/USERNAME/pico/pico-sdk")

Diese Zeile enthält den Pfad zum SDK und muss rechnerspezifisch angepasst werden.
 

MCLK.pio, LRCLK.pio, BCLK.pio
 

 

 

MCLK.pio, LRCLK.pio, BCLK.pio machen nichts anderes als jeweils einen GPIO-Pin des RPi-Pico ein- und auszuschalten. .wrap sorgt dafür, dass die Programmschleife (von .wrap_target aus) immer wieder abgearbeitet wird ohne, dass für diesen Sprung ein weiterer Taktzyklus erforderlich ist.
Die .pio-Dateien in den mitgelieferten Beispielen des SDK enthalten immer etwas Code in C/C++. Dieser dient dazu, die zur jeweiligen .pio-Datei gehörende Statmachine zu initialisieren. Bei diesem Projekt passiert das alles i2sdiy.c.

i2sdiy.c

 

 

Der C/C++ Code erweckt die I2S-Schnittstelle zum Leben und versorgt sie mit den Daten, die ausgegeben werden sollen.

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"

#include "MCLK.pio.h"
#include "LRCLK.pio.h"
#include "BCLK.pio.h"
#include "DATA.pio.h"

Hier werden die notwendigen Libraries sowie die assemblierten .pio-Dateien eingebunden.


#define PIO0_CTRL ((volatile uint32_t*) 0x50200000

Auf die Adresse des Pio0-Kontrollregisters kann später mit *PIO0_CTRL direkt zugeriffen werden.

set_sys_clock_pll(1548000000,7,3);                               

 
Der Systemclock wird auf 73.714 MHz gesetzt. Ausgehend vom LRCL (48 kHz) wird ein MCLK mit der 256fachen Frequenz erzeugt.

48 kHz x 256 = 12.288 MHz

Das 6 fache davon sind 73.728 MHz. Der Systemtakt des RPi-Picos kann nur in bestimmten Intervallen verändert werden. Die einstellbare Taktfrequenz von 73.714 MHz liegt sehr dicht am errechneten Wert. Mehr Informationen zum Ändern der Taktfrequenz sind hier zu finden:

https://www.youtube.com/watch?v=G2BuoFNLoDM

pio_gpio_init(pio0, 12); //GPIO 12 = MCLK
pio_gpio_init(pio0, 13); //GPIO 13 = LRCLK
pio_gpio_init(pio0, 14);      //GPIO 14 = BCLK
pio_gpio_init(pio0, 15);      //GPIO 15 = DATA

GPIO 12-15 werden PIO0 zugewiesen.

uint offsetMCLK = pio_add_program(pio0, &MCLK_program); //Offset Startadresse MCLK
uint offsetLRCLK = pio_add_program(pio0, &LRCLK_program); //Offset Startadresse LRCLK
uint offsetBCLK = pio_add_program(pio0, &BCLK_program); //Offset Startadresse BCLK
uint offsetDATA = pio_add_program(pio0, &DATA_program); //Offset Startadresse DATA

Hier werden die Startadressen der .pio-Dateien festgelegt.


float MCLKdiv = 3; //gültiger Bereich: 1 <= MCLKdiv <= 255

MCLK ist die höchste für die I2S-Schnittstelle zu erzeugende Frequenz. Sie beträgt 12.288 MHz.

73.714 MHz / 12.288 MHz ~ 6

Um den MCLK zu erzeugen, schaltet die Statemachine GPIO 12 ein und wieder aus. Das Einschalten benötigt einen Taktzyklus, das Ausschalten einen weiteren. Deshalb muss die Systemfrequenz nicht durch 6, sondern durch 3 geteilt werden.


//init MCLK

pio_sm_set_consecutive_pindirs(pio0, 0, 12, 1, true); //(pio 0, StateMachine 0, GPIO 12, 1 Pin, true=OUT)
pio_sm_config cMCLK = MCLK_program_get_default_config(offsetMCLK); //Standartkonfiguration für StateMachine 0 übernehmen
sm_config_set_set_pins(&cMCLK, 12, 1); //set pin = GPIO 12, 1 Pin
sm_config_set_clkdiv(&cMCLK,MCLKdiv); //Clockdivider StateMachine 0
pio_sm_init(pio0, 0, offsetMCLK, &cMCLK); //(pio 0, StateMachine 0, Startadresse, Konfiguration)

Hier wird Statemachine 0 für den MCLK an GPIO12 konfiguriert.
Der Clockdivider wird mit der Variablen MCLKdiv auf 3 gesetzt.

Analog werden danach LRCLK und BCLK vorbereitet.

sm_config_set_clkdiv(&cLRCLK,(MCLKdiv * 256)); //Clockdivider StateMachine 1
sm_config_set_clkdiv(&cBCLK,(MCLKdiv * 8)); //Clockdivider StateMachine 2

Die Clockdivider-Werte werden entsprechend angepasst.

Bei Statemachine 3, die die DATA-Leitung bedient, gibt es folgendes zu beachten:

sm_config_set_out_pins(&cDATA, 15, 1); //out pin = GPIO 15, 1 Pin

sorgt dafür, dass GPIO 15 seine Daten aus dem Output-Shift-Register (OSR) der Statemachine erhält.


sm_config_set_clkdiv(&cDATA,(MCLKdiv * 4)); //Clockdivider StateMachine 3

Die Taktfrequenz von Statemachine 3 beträgt das Doppelte der Taktfrequenz von Statemachine 2. Durch entsprechende Programmierung in DATA.pio wird sichergestellt, dass das jeweilige Datenbit sicher auf der Leitung vorhanden ist, wenn es mit der aufsteigenden Flanke des BCLK eingelesen wird.


sm_config_set_out_shift(&cDATA, 0, 1, 32); // FIFO => FILO , autopull enabled, 32 Bit

Mit dieser Anweisung wird erreicht, dass das OSR automatisch neue Daten aus dem TX-FIFO-Register bekommt, wenn es 32 Bit ausgegeben hat. Weiterhin wird die Ausgaberichtung des OSR so geändert, dass die I2S-Schnittstelle das höchstwertige Datenbit (MSB) als erstes erhält.



DATA.pio wartet zunächst einen Moment, damit das MSB pünktlich zur zweiten ansteigenden Flanke von BCLK nach einem Potentialwechsel von LRCLK an GPIO 15 vorhanden ist. Danach werden mit der Dauerscheife die restlichen Datenbits des OSR ausgegeben. Ist das OSR vollständig entleert, wird es automatisch nachgeladen.

pio_sm_put(pio0,3,0xa000a000); //beschreibe TX-FIFO StateMachine 3

1010000010100000 wird ins TX-FIFO von Statemachine 3 geschrieben, damit sicher ein Wert vorhanden ist, wenn die Statemachines gestartet werden.

*PIO0_CTRL = 0x00000fff; //pio 0, Statemachines 0, 1, 2 und 3 synchron starten

Mit diesem Befehl werden die Statemachines synchron gestartet. Möchte man genau wissen, was 0x00000fff in diesem Register bewirkt, hilft ein Blick auf Seite 391 des RP2040 Datenblatts:

https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf

while (true) {
pio_sm_put(pio0,3,0xa000a000);
}

Hier wird in einer Endlosschleife immer wieder der gleiche Wert ins TX-FIFO der Statemachine 3 geschrieben. Der Schreibversuch ist nur dann erfolgreich, wenn das Register leer ist, ansonsten bleibt er ohne Folgen für den Programmablauf.
Überträgt man den compilierten Code auf den RPi-Pico, kann man mit einem Logikanalysator sehen, dass an GPIO 12 bis 15 genau das passiert, was man beabsichtigt hat.


Projekt-Download: i2sdiy.zip



RPi Pico – DDS mit I2S  von Martin Müller



Hier wird gezeigt wie ein 1 kHz-Sinussignal mittels Direct Digital Synthesis (DDS) erzeugt wird und über einen I2S-Audioverstärker ausgegeben wird.

Voraussetzungen:
Als I2S-Audioverstärker wird ein  Adafruit MAX98357 I2S Class-D Mono Amp-Modul verwendet. Es wird gemeinsam mit dem RPi-Pico auf einem Steckbrett platziert. Seine Versorgungsspannung von 3,3 V erhält es vom RPi-Pico. LRC wird mit GPIO 13, BCLK mit GPIO 14 und DIN mit GPIO 15 des RPi-Pico verbunden. Die Pins GAIN und SD werden so konfiguriert, dass das Modul bei einer internen Verstärkung von 6 dB den linken Audiokanal wiedergibt.
Alle Informationen zu dem Modul sind hier zu finden:

https://cdn-learn.adafruit.com/downloads/pdf/adafruit-max98357-i2s-class-d-mono-amp.pdf


DDS:
Wenn man nicht weiß, wie DDS funktioniert, so wird das in dieser .pdf-Datei ab Seite 85 sehr verständlich beschrieben:

https://www.weigu.lu/tutorials/avr_assembler/pdf/MICEL_MODUL_C.pdf


Programmdateien
Die .pio-Dateien sind identisch zu denen des obigen. Artikels. CMakeLists.txt muss an den neuen Projektnamen angepasst werden. i2sdds.c wird um die DDS-Funktionalität ergänzt.





uint32_t sinus [256] = {                //Sinus-Array
8192, 8393, 8593, 8794, 8994, …

Zunächst wird ein Wavetable, also ein Array mit 256 Zahlenwerten hinzugefügt, das eine Sinuskurve beschreibt.


uint32_t akku = 0;
uint32_t idex = 0;
uint32_t freq = 2731;                                                       
uint32_t wert = 8192;

Diese Variablen werden für die zur DDS gehörenden Rechenoperationen benötigt.

Die Taktfrequenz der DDS entspricht dem I2S-LRCLK. In diesem Fall sind das 48 kHz.
Für den Phasenakku wurde eine Breite von ungewöhnlichen 17 Bit (131072) gewählt.

48000 / 131072  =  0,3662

Das bedeutet, dass mit dieser DDS eine Auflösung von maximal 0,3662 Hz erreicht wird.

Nun soll ein Sinussignal mit einer Frequenz von 1000 Hz erzeugt werden.

1000 / 0,3662   =  2730,748

Gerundet sind das 2731. Dieser Wert ist in „freq“ hinterlegt.


while(true){                        //Endlosschleife                                                               
    pio_sm_put_blocking(pio0,3,wert);    //beschreibe TX-FIFO StateMachine 3, wenn TX-FIFO leer
    gpio_put(8, 1);
    akku = akku + freq;
    akku = akku & 0x0001ffff;        //Akku 17 Bit => 131072
    idex = akku >> 9;                //Die obersten 8 Bit zur Indizierung des Arraywertes benutzen
    wert = sinus[idex];                                                       
    wert = wert << 16;                //Zahlenwert für li. Audiokanal um 16 Bits nach links schieben
    gpio_put(8, 0);
    }

Die Endlosschleife versorgt die I2S-Schnittstelle mit den auszugebenden Werten.

pio_sm_put_blocking(pio0,3,wert);

wartet darauf, dass das TX-FIFO von Statemachine 3 leer ist, bevor es die aktuelle Variable „wert“ übergibt.


akku = akku + freq;

„freq“ (2731) wird bei jedem Durchlauf zum aktuellen Wert des Phasenakkus hinzuaddiert.


akku = akku & 0x0001ffff;

setzt die 15 am höchsten gewerteten Bits nach jeder Addition auf Null. Damit wird die Breite des Phasenakkus auf 17 Bit begrenzt.


idex = akku >> 9;

Benutzt die oberen 8 Bit von den 17 Bit des Phasenakkus, um den nächsten zu übertragenden Zahlenwert aus dem Wavetable auszuwählen. Der Wavetable hat 256 Einträge, deshalb braucht man 8 Bit um alle Einträge erreichen zu können.


wert = sinus[idex];
wert = wert << 16;

Hier wird der Zahlenwert aus dem Wavetable in die zum TX-FIFO-Register zu übertragende Variable geschrieben. Damit das Signal auf dem linken Audiokanal erscheint, muss der Zahlenwert noch um 16 Bit nach links verschoben werden.


gpio_put(8, 1);
...
gpio_put(8, 0);

GPIO 8 wird als eine Art Indikator verwendet. Er wird HIGH, wenn das TX-FIFO-Register beschrieben wurde, und wird wieder LOW, nachdem die Berechnungen zur DDS abgeschlossen sind.





Wie mit dem Logikanalysator leicht zu sehen ist, wird dafür nur ein sehr kleiner Teil des LRCLK-Zyklus benötigt. Man könnte man auch einen Wavetable mit mehr Einträgen verwenden, damit das auszugebende Signal „genauer“ dargestellt wird . Auch kann man die Breite des Phasenakkus auf 24 oder 32 Bit erhöhen um eine höhere Auflösung der Ausgabefrequenz zu erreichen.

Projekt-Download: i2sdds.zip



RPi Pico – Regelbare DDS mit I2S   von Martin Müller



Dieser Beitrag beschreibt, wie man Frequenz und Amplitude des DDS-Sinusgenerators regeln kann.

Voraussetzungen:
An den RPi-Pico werden ein Drehimpulsgeber mit Taster und 2 LEDs (mit integrierten Vorwiderständen) angeschlossen. Mit dem Taster kann man auswählen, ob man die Frequenz oder die Amplitude des erzeugten Signals verändern möchte. Die Auswahl wird mit den beiden LEDs angezeigt. 

Drehimpulsgeber mit Taster:
Mechanische Schalter neigen grundsätzlich dazu beim Betätigen zu prellen.



Dies ist beim Taster des Drehimpulsgebers, der an GPIO 6 angeschlossen wird, auch nicht anders und muss in der Software entsprechend berücksichtigt werden.



GPIO 1 und GPIO 2 sind mit den Kontakten des Drehimpulsgebers verbunden. Dreht man die Achse des Drehimpulsgebers gegen den Uhrzeigersinn, liegt an GPIO 1 (blau) HIGH, wenn an GPIO 2 (gelb) eine negative Flanke auftritt. Dreht man im Uhrzeigersinn, so liegt beim Erscheinen der negativen Flanke an GPIO 2, LOW an GPIO 1. Dies muss vom RPi-Pico ausgewertet werden.


Programmdateien
Entsprechend der neuen Funktionen muss die bestehende .c-Datei ergänzt werden.



#define INTR0 ((volatile uint32_t*) 0x400140f0)    //Adresse Raw-Interrupts GPIO 0 – 7

macht das INTR0-Register zugänglich.


uint32_t stat = 0;                            //Variablen für Regelung
uint32_t tdel = 0;
uint32_t laut = 32;
uint32_t ldel = 0;
uint32_t fdel = 0;

Diese Variablen werden für die Regelung benötigt. Die Lautstärke soll in 65 Stufen (0 – 64) regelbar sein. Als Vorgabe wird ein Zahlenwert von 32 eingestellt.


uint32_t freq = 1360;

Der Initialwert für die Frequenz liegt bei ca. 500 Hz ( 1360 * 48000 / 131072).


gpio_init(1);                                //GPIO 1 Drehimpulsgeber Potential
gpio_set_dir(1, GPIO_IN);
gpio_pull_up(1);

gpio_init(2);                                //GPIO 2 Drehimpulsgeber neg. Flanke
gpio_set_dir(2, GPIO_IN);
gpio_pull_up(2);

gpio_init(6);                                //GPIO 6 Drehimpulsgeber Taster
gpio_set_dir(6, GPIO_IN);
gpio_pull_up(6);

gpio_init(3);                                //GPIO 3 LED Lautstärke an
gpio_set_dir(3, GPIO_OUT);
gpio_put(3, 1);

gpio_init(5);                                //GPIO 5 LED Frequenz aus
gpio_set_dir(5, GPIO_OUT);
gpio_put(5, 0);

Hier werden der Drehimpulsgeber mit Taster und die beiden LEDs den entsprechenden GPIO-Pins zugeordnet.


*INTR0 = 0x04000400;                        //neg. Flanken GPIO 2 und 6 löschen

löscht bislang an GPIO 6 und GPIO 2 aufgetretene negative Flanken.



Betrachtet man das INTR0-Register kann man erkennen, welche Bits welchen Potentialen bzw. Ereignissen an den einzelnen GPIOs zugeordnet sind. Bits, die mit „RO“ gekennzeichnet sind können nur ausgelesen, aber nicht verändert werden. Ist ein Bit mit „WC“ gekennzeichnet, so kann es durch Überschreiben mit 1 zurückgesetzt werden. Schaut man sich exemplarisch die Bits 27 – 24 an, so zeigen diese, dass an GPIO 6 ein Potentialwechsel von HIGH auf LOW stattgefunden hat. Alle anderen GPIOs sind ohne stattgefundenen Potentialwechsel auf HIGH.

 
stat = *INTR0;                                //GPIO 0-7 Status speichern

In der Endlosschleife wird der Inhalt des INTR0-Registers in die Variable „stat“ übergeben. Damit wird sichergestellt, dass eine Änderung des Inhalts des INTR0-Registers während der Auswertung folgenlos bleibt.


//Auswahl Lautstärke/Frequenz
if(tdel == 0){                    //Prüft ob Variable zum Entprellen = 0
       
Das Signal vom Taster des Drehimpulsgebers wird nur dann ausgewertet, wenn „tdel“ = 0 ist. Das bedeutet, dass seit dem letzten Tastendruck mindestens 100 ms vergangen sind.


if((stat & 0x04000000) != 0){        //Prüft GPIO 6 auf neg. Flanke
           
Durch die logische UND-Verknüpfung mit  0x04000000 wird überprüft, ob Bit 26 in „stat“ gesetzt ist. Nur in diesen Fall ist das Ergebnis der Verknüpfung ungleich 0.

 
gpio_xor_mask(0x28);            //Schaltet GPIO 3 und 5 um
tdel = 4800;                    //Setzt Variable zum Entprellen auf 100 ms

War Bit 26 in „stat“ gesetzt, werden GPIO 3 und GPIO 5 durch die EXCLUSIV-ODER-Verknüpfung mit 0x28 umgeschaltet. „tdel“ wird zum Entprellen des Tasters auf 4800 gesetzt. Da die Endlosschleife 48000 Mal pro Sekunde abgearbeitet wird, dauern 4800 Zyklen genau 100 ms.


if(tdel > 0){                    //Prüft ob Variable zum Entprellen > 0
*INTR0 = 0x04000000;            //Löscht neg. Flanke an GPIO 6
tdel --;}                        //Dekrementiert Variable zum Entprellen um 1

Hier wird bei jedem Schleifendurchlauf „tdel“ um 1 verringert, bis der Zahlenwert 0 erreicht ist. Weiterhin wird jedes Mal Bit 26 des INTR0-Registers zurückgesetzt.       


//Frequzenz   
if(gpio_get(5) == 1){            //Prüft ob GPIO 5 = 1

Die Frequenz der DDS kann nur verändert werden, wenn GPIO 5 (rote LED) HIGH ist.


if(fdel == 0){                    //Prüft ob Variable zum Verzögern = 0

Auch hier gibt es eine Variable, die dazu dient den Kontakt des Drehimpulsgebers zu entprellen. Ob man sie überhaupt benötigt, und auf welchen Wert Sie gesetzt werden muss, ist von der Ausführung des jeweiligen Drehimpulsgebers abhängig.


If((stat & 0x00000400) != 0){                    //Prüft GPIO 2 auf neg. Flanke
            if((stat & 0x00000010) != 0){        //Prüft GPIO 1 auf LOW
                if(freq < 2730){            //Prüft ob Maximalfrequenz noch nicht erreicht
                freq = freq + 10;            //Inkrementiert Frequenz um 10
                }
            }
            if((stat & 0x00000020) != 0){        //Prüft GPIO 1 auf HIGH
                if(freq > 540){                //Prüft ob Minimalfrequenz noch nicht unterschritten
                freq = freq - 10;            //Dekrementiert Frequenz um 10
                }
            }
            fdel = 48;                        //Setzt Variable zum Verzögern auf 1 ms
            }
        }
        }
   
        if(fdel > 0){                        //Prüft ob Variable zum Verzögern > 0
        *INTR0 = 0x00000400;                //Löscht neg. Flanke an GPIO 2
        fdel --;                            //Dekrementiert Variable zum Verzögern um 1
        }

Es wird überprüft, ob eine negative Flanke an GPIO 2 erkannt wurde. In Abhängigkeit vom Potential (HIGH oder LOW), das zu diesem Zeitpunkt an GPIO 1 anliegt, wird „freq“ in Zehnerschritten (ca. 3,66 Hz) erhöht oder vermindert. Dabei wird sichergestellt, dass vorgegebene Minimal- und Maximalwerte eingehalten werden.
Die Verzögerungsvariable („fdel“) wird analog zum Vorgehen beim Tastendruck des Drehimpulsgebers behandelt.


//Lautstärke   
    if(gpio_get(3) == 1){                    //Prüft ob GPIO 3 = 1
    if(ldel == 0){                            //Prüft ob Variable zum Verzögern = 0
        if((stat & 0x00000400) != 0){            //Prüft GPIO 2 auf neg. Flanke
            if((stat & 0x00000010) != 0){        //Prüft GPIO 1 auf LOW
                if(laut < 64){                //Prüft ob Maximallautstärke noch nicht erreicht
                laut ++;                    //Inkrementiert Lautstärke um 1
                }
            }
            if((stat & 0x00000020) != 0){        //Prüft GPIO 1 auf HIGH
                if(laut > 0){                //Prüft ob Minimallautstärke noch nicht unterschritten
                laut --;                    //Dekrementiert Lautstärke um 1
                }
            }
            ldel = 48;                        //Setzt Variable zum Verzögern auf 1 ms
            }
        }
        }

Die Lautstärke kann in Einerschritten im Wertebereich von 0 bis 64 nur dann geändert werden, wenn GPIO 3 (blaue LED) HIGH ist. Auch hier gibt es wieder eine Verzögerungsvariabel.


wert = wert * laut;                            //wert mit Lautstärke multiplizieren 0 - 64
wert = wert >> 6;                            //wert durch 64 dividieren

Der aus dem Wavetable der DDS ausgelesene Zahlenwert, wird mit dem Lautstärkewert (0 – 64) multipliziert. Durch Rechtsverschiebung um 6 Bits wird das Ergebnis durch 64 dividiert. Somit wird der ursprüngliche Zahlenwert mit einem Lautstärkewert zwischen 64/64 und 0/64 multipliziert. Man kann natürlich hier mit der Floatfunktion des C-Compilers arbeiten. Da man letztlich einen ganzzahligen Wert benötigt, kann man sich diesen Umweg wie gezeigt auch sparen.


wert = wert << 16;                            //Zahlenwert für li. Audiokanal um 16 Bits nach links schieben

Damit das Endergebnis auf dem linken Audiokanal erscheint, muss es abschließend noch um 16 Bits nach links verschoben werden.

Probiert man den auf diese Weise konstruierten regelbaren Sinusgenerator aus, so wird man feststellen, dass es  hinsichtlich der „Klangqualität“ durchaus Optimierungspotential gibt. Das hängt im wesentlichen mit der recht einfachen und überschaubaren Konfiguration der DDS zusammen.

Ziel dieses Artikels ist es nicht den perfekten regelbaren Sinusgenerator zu konstruieren, sondern zu zeigen, wie man mit dem RPi-Pico, dem einen oder anderen Blick ins Datenblatt und etwas Kreativität eigene etwas komplexere Projekte erstellen kann, ohne dabei auf vorgefertigte Libraries zurückgreifen zu müssen.





Elektronik-Labor  Projekte  Mikrocontroller  Raspberry