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.
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.
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
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.