Nachdem wir im zweiten Teil einen Verstärker hinzugefügt haben mit dem man sowohl die Amplitude als auch den Offset einstellen kann, wollen wir jetzt die Bedienung nicht mehr über den seriellen Monitor, sondern direkt am Gerät vornehmen.
Dazu eignet sich zum Beispiel das LCD1602 Display Keypad Shield, das ein LCD-Display mit zwei Zeilen a 16 Zeichen sowie Taster zur Bedienung besitzt. Dieses Shield wurde für den Microkontroller Arduino entworfen. Damit es mit dem ESP32 einfach genutzt werden kann, ersetzen wir den ESP32 DevKit CV4 vom ersten Teil durch das Board ESP32 D1 R32, das seine Anschlusspins gleich wie ein Arduino-Uno angelegt hat. Wir können also das Shield direkt draufstecken. Allerdings werden Beispiele nicht funktionieren, da der ESP32 andere IO-Pins als der Arduino hat. Zusätzlich ist zu beachten, dass die Eingänge des ESP32 nur 3.3 V vertragen.
Die Abbildungen zeigen die Pinbelegung des ESP32 D1 R32 Boards und die Belegung des LCD-Keypad Shield. Bei diesem sind nur jene Pins beschriftet, die auch genutzt werden.
Da alle LCD- Pins als Eingänge des LCD-Controllers benutzt werden, brauchen wir uns keine Sorgen wegen der Spannung machen, da die Eingänge auch mit 3.3 V sicher funktionieren.
Etwas anders sieht es mit dem Taster-Anschluss aus. Der liefert je nach gedrückter Taste einen Wert zwischen 0 und 5V. Am Shield ist der Ausgang über einen 2 kOhm Widerstand mit dem +5V Anschluss verbunden. Wenn wir nun einen Widerstand von 3.9 kOhm vom Taster-Anschluss und dem 0V Anschluss löten, beträgt die maximale Spannung an diesem Anschluss nur noch 5V * 3900 / (2000 + 3900) = 3.3 V. Auf der Abbildung ist dieser Widerstand zu sehen.
Ein weiteres Problem haben wir mit dem GPIO12 Anschluss. Der muss während des Bootvorgangs auf 0V liegen, da sonst die Betriebsspannung für den Flash-Speicher auf 1.8V geschaltet wird. Hier hilft wieder ein Widerstand von 10 kOhm zwischen GPIO12 und GND. Diesen Widerstand löten wir aber besser an das ESP32 D1 R32 Board, da dieses Problem auch bei anderen Arduino Shields auftreten kann.
![]() |
Bild 1: Pinbelegung des ESP32 D1 R32
![]() |
Bild 2: Pinbelegung des LCD Keypad Shield (nur verwendete Anschlüsse)
![]() |
Bild 3: Rückseite ESP32 D1 R32 mit 10 kOhm-Widerstand zwischen GPIO12 und GND
Benötigte Hardware
Hier sind nochmal alle Teile, auch jene aus dem zweiten Teil, aufgeführt.
Anzahl | Bauteil | Anmerkung |
---|---|---|
1 | ESP32 D1 R32 Board | |
2 | LCD Keypad Shield | |
1 | Widerstand 3,9 kOhm | |
1 | Widerstand 10 kOhm | |
1 | LM358 Dual Operationsverstärker | Aus Teil2 |
2 | Potentiometer 10 kOhm mit 4mm Achse | Aus Teil2 |
1 | Widerstand 1 kOhm R3 | Aus Teil2 |
1 | Widerstand 1,5 kOhm R5 | Aus Teil2 |
1 | Widerstand 2,2 kOhm R2 | Aus Teil2 |
2 | Widerstand 100 kOhm R1 und R4 | Aus Teil2 |
1 | Stiftleiste 7-polig | |
1 | Stiftleiste 6-polig | |
3 | Stiftleiste 3-polig | Aus Teil2 |
1 | Stiftleiste 2-polig | Aus Teil2 |
3 | Jumper Wire Kabel weiblich/weiblich 3-polig | |
2 | Jumper Wire Kabel weiblich/weiblich 2-polig | |
1 | Leiterplatte oder Lochrasterplatte 30 x 40 mm | Aus Teil2 |
1 | DC-DC-Boost-Buck-Wandler mit positiver und negativer Spannung Eingang 5V, Ausgang +/- 5V | Aus Teil2 |
1 | BNC-Einbaubuchse | |
2 | Knöpfe für Potentiometer | |
1 | Knöpfe aus dem 3D Drucker mit TPU Filament | |
4 | Gehäuse und Distanzstücke aus dem 3D-Drucker mit PLA Filament |
|
8 | Blechschrauben 2,2 x 6,5 mm | |
4 | Blechschrauben 2,2 x 9,5 mm |
Die Software
/* * Funktionsgenerator für Sinus, Dreieck und Rechteck Signale * Einstellbare Frequenz 20 Hz bis 20 KHz * Für Dreieck und Rechteck einstellbares Tastverhältnis 0 bis 100% */ //Bibliotheken zum direkten Zugriff auf Steuerregister des ESP32 #include "soc/rtc_cntl_reg.h" #include "soc/sens_reg.h" #include "soc/rtc.h" //Bibliotheken zur Verwendung des Digital zu Analog Konverters und für den I2S-Bus #include "driver/dac.h" #include "driver/i2s.h" //Bibliothek für das LCD Display #include <LiquidCrystal.h> #define SINFAKT 127.0 //gemessen für Schrittweite = 1 und kein Vorteiler (8.3MHz) #define SIGNALOUT 26 //Pin für die Signalausgabe //LCD Pins #define PIN_RS 12 //Registerselect 0=Befehle 1=Daten #define PIN_EN 13 //Enable Takt zum Schreiben #define PIN_D4 17 //Datenbit #define PIN_D5 16 //Datenbit #define PIN_D6 27 //Datenbit #define PIN_D7 14 //Datenbit #define PIN_BL 5 //Hintergrundbeleuchtung 0=aus //Analog PIN für Tasten #define KEYS A12 //Spannungsteiler für Tasten #define Rv 2000 //Vorwiderstand #define R3 3900 //Schutzwiderstand für maximal 3.3 V #define Rp 1000 //Widerstand GPIO2 gegen Masse #define Rr 0 //Spannungsteiler bei gedrückter RIGHT Taste #define Ru 330 //Spannungsteiler bei gedrückter UP Taste #define Rd 950 //Spannungsteiler bei gedrückter DOWN Taste #define Rl 1950 //Spannungsteiler bei gedrückter LEFT Taste #define Rs 5250 //Spannungsteiler bei gedrückter SELECT Taste //Tasten Codes #define NONE 0 #define LEFT 1 #define RIGHT 2 #define UP 3 #define DOWN 4 #define SELECT 5 //Betriebsarten #define MSINUS 0 #define MRECTANGLE 1 #define MTRIANGLE 2 //Änderungstypen #define EMODE 0 #define EFREQUENCY 1 #define ERATIO 2 // Init I2C LCD LiquidCrystal lcd(PIN_RS, PIN_EN, PIN_D4, PIN_D5, PIN_D6, PIN_D7); //Variablen zum Speichern der Schwellwerte für Taster uint16_t Ur, Uu, Ud, Ul, Us; //Buffer zum Erstellen der Dreieckfunktion uint32_t buf[128]; //Einstellwerte für Kurvenform, Frequenz und Tastverhältnis int8_t mode = MSINUS; //0=Sinus, 1=Rechteck, 2=Dreieck float frequency = 1000; //20 bis 200000 Hz int8_t ratio = 50; //Tastverhältnis 0 bis 100% int8_t edit = EMODE; //was wird verändert 0=Mode 1=Frequenz 2=Tastverhältnis uint32_t tic; //Für Wartezeit int8_t lastKey = 0; //zuletzt ermittelte Taste oder 0 wenn keine uint16_t step = 0; //Schrittweite für Frequenzerhöhung float ftmp; //Variable zum Speichern der Frequenz während der Einstellung int16_t rtmp; //Variable zum Speichern des Tastverhältnis während der Einstellung int8_t mtmp; //Variable zum Speichern der Betriebsart während der Einstellung //Flag Ist wahr, wenn die Initialisierung bereits erfolgte bool initDone = false; //Konfiguration für den I2S Bus i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN), //Betriebsart .sample_rate = 100000, //Abtastrate .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // der DAC verwendet nur 8 Bit des MSB .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // Kanalformat ESP32 unterstützt nur Stereo .communication_format = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S_MSB, //Standard Format für I2S .intr_alloc_flags = 0, // Standard Interrupt .dma_buf_count = 2, //Anzahl der FIFO Buffer .dma_buf_len = 32, //Größe der FIFO Buffer .use_apll = 0 //Taktquelle }; //Buffer für Dreieck Wellenform füllen //Parameter up ist die Dauer für den Anstieg in Prozent //Parameter sz gibt die Buffergröße für eine Periode an //es werden die Werte für eine Periode in den Buffer geschrieben void fillBuffer(uint8_t up, uint8_t sz) { uint8_t down; //Zeit für die fallende Flanke in % uint32_t sample; //32Bit Datenwort (I2S benötigt zwei Kanäle mit je 16 Bit float du,dd,val; //Hilfsvariablen down=100-up; //Anzahl der Schritte für Anstieg und Abfall berechnen uint16_t stup = round(1.0*sz/100 * up); uint16_t stdwn = round(1.0*sz/100*down); uint16_t i; if ((stup + stdwn) < sz) stup++;//Ausgleich eventueller Rundungsfehler //Amplitudenänderung pro Schritt für Anstieg und Abfall du = 256.0/stup; dd = 256.0/stdwn; //füllen des Buffers val = 0; //Anstieg beginnt mit 0 for (i=0; i<stup; i++) { sample = val; sample = sample << 8; //Byte in das höherwertige Byte verschieben buf[i]=sample; val = val+du; //Wert erhöhen } val=255; //Abfallende Flanke beginnt mit Maximalwert //Rest wie bei der ansteigenden Flanke for (i=0; i<stdwn; i++) { sample = val; sample = sample << 8; buf[i+stup]=sample; val = val-dd; } } //Alle Ausgänge stoppen void stopAll(){ ledcDetachPin(SIGNALOUT); i2s_driver_uninstall((i2s_port_t)0); dac_output_disable(DAC_CHANNEL_2); dac_i2s_disable(); initDone=false; } //Kurvenform Rechteck starten //Pin für Signalausgang zuweisen void startRectangle(){ ledcAttachPin(SIGNALOUT,1 ); initDone=true; } //Frequenz für Rechteck setzen mit entsprechendem Tastverhältnis void rectangleSetFrequency(double frequency,uint8_t ratio) { ledcSetup(1,frequency,7); //Wir nutzen die LEDC Funktion mit 7 bit Auflösung ledcWrite(1,127.0*ratio/100); //Berechnung der Schrittanzahl für Zustand = 1 } //Dreiecksignal starten void startTriangle(){ i2s_set_pin((i2s_port_t)0, NULL); //I2S wird mit dem DAC genutzt initDone=true; } //Frequenz für Dreieck setzen mit entsprechendem Tastverhältnis double triangleSetFrequency(double frequency,uint8_t ratio) { int size=64; //zuerst wird die geeignete Buffergröße ermittelt //damit die Ausgabe funktionier muss die I2S Abtastrate zwischen //5200 und 650000 liegen if (frequency<5000) { size = 64; } else if (frequency<10000) { size = 32; } else if (frequency<20000) { size = 16; } else { size = 8; } //Abtastrate muss in einer Periode beide Buffer ausgeben uint32_t rate = frequency * 2 * size; //Die Abtastrate darf nur innerhalb der Grenzwerte liegen if (rate < 5200) rate = 5200; if (rate > 650000) rate = 650000; //wirklichen Frequenzwert setzen frequency = rate / 2 / size; //I2S Treiber entfernen i2s_driver_uninstall((i2s_port_t)0); //Konfiguration anpassen i2s_config.sample_rate = rate; i2s_config.dma_buf_len = size; //und mit der neuen Konfiguration installieren i2s_driver_install((i2s_port_t)0, &i2s_config, 0, NULL); //Abtastrate einstellen i2s_set_sample_rates((i2s_port_t)0, rate); //Buffer füllen fillBuffer(ratio,size*2); //und einmal ausgeben i2s_write_bytes((i2s_port_t)0, (const char *)&buf, size*8, 100); return frequency; } //Sinusausgabe vorbereiten void startSinus(){ //Ausgang für Signalausgang freigeben dac_output_enable(DAC_CHANNEL_2); // Sinusgenerator aktivieren SET_PERI_REG_MASK(SENS_SAR_DAC_CTRL1_REG, SENS_SW_TONE_EN); // Ausgabe auf Kanal 1 starten SET_PERI_REG_MASK(SENS_SAR_DAC_CTRL2_REG, SENS_DAC_CW_EN2_M); // Vorzeichenbit umkehren SET_PERI_REG_BITS(SENS_SAR_DAC_CTRL2_REG, SENS_DAC_INV2, 2, SENS_DAC_INV2_S); initDone=true; } //Frequenz für Sinus setzen double sinusSetFrequency(double frequency) { //Formel f = s * SINFAKT / v //s sind die Schritte pro Taktimpuls //v ist der Vorteiler für den 8MHz Takt //Es gibt 8 Vorteiler von 1 bis 1/8 um die Kombination Vorteiler und //Schrittanzahl zu finden, testen wir alle acht Vorteiler Varianten //Die Kombination mit der geringsten Frequenzabweichung wird gewählt double f,delta,delta_min = 999999999.0; uint16_t divi=0, step=1, s; uint8_t clk_8m_div = 0;//0 bis 7 for (uint8_t div = 1; div<9; div++){ s=round(frequency * div/SINFAKT); if ((s>0) && ((div == 1) || (s<1024))) { f= SINFAKT*s/div; /* Serial.print(f); Serial.print(" "); Serial.print(div); Serial.print(" "); Serial.println(s); */ delta = abs(f-frequency); if (delta < delta_min) { //Abweichung geringer -> aktuelle Werte merken step = s; divi = div-1; delta_min = delta; } } } //wirklichen Frequenzwert setzen frequency = SINFAKT * step / (divi+1); // Vorteiler einstellen REG_SET_FIELD(RTC_CNTL_CLK_CONF_REG, RTC_CNTL_CK8M_DIV_SEL, divi); // Schritte pro Taktimpuls einstellen SET_PERI_REG_BITS(SENS_SAR_DAC_CTRL1_REG, SENS_SW_FSTEP, step, SENS_SW_FSTEP_S); return frequency; } //Einstellungsänderungen durchführen void controlGenerator() { switch (mode) { case MSINUS: if (!initDone) startSinus(); frequency = sinusSetFrequency(frequency); break; case MRECTANGLE : if (!initDone) startRectangle(); rectangleSetFrequency(frequency,ratio); break; case MTRIANGLE : if (!initDone) startTriangle(); frequency = triangleSetFrequency(frequency,ratio); break; } } //Anzeige aktualisieren //Wenn monitor wahr ist, erfolgt die Ausgabe //auch auf die serielle Schnittstelle void displayValues(boolean monitor) { char buf[15]; //aktuelle Werte ausgeben String ba; switch (mode) { case MSINUS: ba="Sinus "; break; case MRECTANGLE: ba="Rechteck "; break; case MTRIANGLE: ba="Dreieck "; break; } //Betriebsart ausgeben lcd.setCursor(0,0); lcd.print(" "); lcd.print(ba); if (monitor) { Serial.println("**************** Eingestellte Werte *************************"); Serial.print("Betriebsart = "); Serial.println(ba); } //Frequenz je nach Wert als Hz oder kHz if (frequency < 1000){ sprintf(buf,"%6.2f Hz",frequency); } else { sprintf(buf,"%6.2fkHz",frequency/1000); } //Frequenz ausgeben lcd.setCursor(0,1); lcd.print(" F"); lcd.print(buf); if (monitor) { Serial.print("Frequenz = "); Serial.println(buf); } sprintf(buf,"%2i%%",ratio); //Tastverhältnis ausgeben lcd.setCursor(11,1); lcd.print(" T"); lcd.print(buf); if (monitor) { Serial.print("Tastverhältnis = "); Serial.println(buf); Serial.println(); } //je nach Edit-Mode Pfeilzeichen ausgeben switch (edit) { case EMODE: lcd.setCursor(0,0); break; case EFREQUENCY: lcd.setCursor(0,1); break; case ERATIO: lcd.setCursor(11,1); break; } lcd.print(char(126)); } //Edit Mode ändern mit UP und DOWN Taste void changeEdit(boolean up) { //je nach Richtung Schritte positiv oder negativ int s = up?1:-1; edit += s; //am Ende wieder auf Anfang springen if (edit < 0) edit = 2; if (edit > 2) edit = 0; //die aktuellen Werte in die temporären Werte //für die Änderung kopieren ftmp = frequency; rtmp = ratio; mtmp = mode; //Geänderte Editmode ausgeben Serial.print("Mode = ");Serial.println(mode); //Anzeige aktualisieren ohne Ausgabe auf serielle //Schnittstelle displayValues(false); } //Betriebsart ändern mit RIGHT und LEFT Taste void changeMode(boolean up) { //je nach Richtung Schritte positiv oder negativ int s = up?1:-1; //temporäre Betriebsart ändern mtmp += s; //Wenn das Ende erreicht wird wieder auf Anfang springen if (mtmp < 0) mtmp = 2; if (mtmp > 2) mtmp = 0; //Geänderte Betriebsart am Display anzeigen lcd.setCursor(1,0); switch (mtmp) { case 0: lcd.print("Sinus "); break; case 1: lcd.print("Rechteck "); break; case 2: lcd.print("Dreieck "); break; } } //Frequenz ändern mit RIGHT und LEFT Taste void changeFrequency(boolean up) { //war die Taste vorher nicht gedrückt, wird die Schrittweite auf 1 gesetzt //während die Taste gedrückt bleibt, wird die Schrittweite laufend //verdoppelt bis eine maximale Schrittweite erreicht wurde step = (lastKey == NONE)?1:step*2; if (step > 1024) step = 1024; //Richtungsfaktor bestimmen int16_t s = up?1:-1; //temporäre Frequenz ändern ftmp = ftmp+s*step; //auf Minimal- und Maximalwerte prüfen if (ftmp < 20) ftmp=20; if (ftmp > 20000) ftmp = 20000; char buf[15]; //Für die Anzeige Hz oder kHz if (ftmp > 999) { sprintf(buf,"%6.2fkHz",ftmp/1000.0); } else { sprintf(buf,"%6.2f Hz",ftmp*1.0); } //Geänderte Frequenz am Display anzeigen lcd.setCursor(2,1); lcd.print(buf); } //Tastverhältnis ändern mit RIGHT und LEFT Taste void changeRatio(boolean up) { //Richtung festlegen int8_t jx = up?1:-1; //Temporäres Tastverhältnis ändern rtmp = rtmp+jx; //auf Minimal- und Maximalwerte prüfen if (rtmp < 0) rtmp=0; if (rtmp > 100) rtmp = 100; char buf[15]; //Geändertes Tastverhältnis am Display anzeigen sprintf(buf,"%2i%%",rtmp); lcd.setCursor(13,1); lcd.print(buf); } //der Funktionsgenerator wird auf die geänderte Einstellung gesetzt //die temporären Werte werden in die Aktuellen Werte übernommen void setValues() { Serial.print("Set values edit = "); Serial.println(edit); switch (edit) { case EMODE: stopAll(); mode = mtmp; break; case EFREQUENCY: frequency = ftmp; break; case ERATIO: ratio = rtmp; break; } //Funktionsgenerator selber ändern controlGenerator(); displayValues(true); } //Tastaturspannung einlesen und auswerten void handleKeys() { //Tastaturspannung einlesen int x=analogRead(KEYS); uint8_t key = NONE; if (x < Ur) { key = RIGHT; } else if (x < Uu) { key = UP; } else if (x < Ud){ key = DOWN; } else if (x < Ul){ key = LEFT; } else if (x < Us){ key = SELECT; } else {key = NONE;} if (((key == UP) || (key == DOWN)) && (lastKey == NONE)) changeEdit(key == DOWN); if ((key == LEFT) || (key == RIGHT)) { switch (edit) { case EMODE: if (lastKey == NONE) changeMode(key == RIGHT); break; case EFREQUENCY: changeFrequency(key == RIGHT); break; case ERATIO: changeRatio(key == RIGHT); break; } } if ((key == SELECT) && (lastKey == NONE)) setValues(); lastKey = key; tic = millis(); } //Serielle Schnittstelle aktivieren und //Defaulteinstellungen 1kHz Sinus setzen //Schwellwerte für Tastatur festlegen void setup() { Serial.begin(115200); controlGenerator(); lcd.begin(16,2); // initialisiere LCD I2C Anzeige lcd.clear(); displayValues(true); tic = millis(); Serial.print("Kommando M,F,A,T,O : "); pinMode(2,INPUT); //Schwellwerte für Taster berechnen //Diese Berechnung ist notwendig, da die Toleranzen sehr //gering sind, und die Schwellwerte von der Betriebsspannung //abhängen. //Versorgungsspannung ohne Taste ermitteln int x=analogRead(A12); float Rin = R3 * Rp / (R3 + Rp); float Ub = x / Rin * (Rin + Rv); //Schwellspannungen ermitteln float Uup,Udn,Ulf,Usl,Rtmp; //Mittelwert für UP Taste Rtmp = Rin * Ru / (Rin + Ru); Uup = Ub * Rtmp / (Rtmp + Rv); //Mittelwert für DOWN Taste Rtmp = Rin * Rd / (Rin + Rd); Udn = Ub * Rtmp / (Rtmp + Rv); //Mittelwert für LEFT Taste Rtmp = Rin * Rl / (Rin + Rl); Ulf = Ub * Rtmp / (Rtmp + Rv); //Mittelwert für Select Taste Rtmp = Rin * Rs / (Rin + Rs); Usl = Ub * Rtmp / (Rtmp + Rv); //eigentliche Schwellwerte berechnen //immer in die Mitte zwischen zwei Mittelwerten Ur = Uup/2; Uu = Uup + (Udn - Uup) / 2; Ud = Udn + (Ulf - Udn) / 2; Ul = Ulf + (Usl - Ulf) / 2; Us = Usl + (x-Usl) /2; //Schwellwerte auf die serielle Schnittstelle ausgeben Serial.printf("Schwellwerte: right %i, up %i, down %i, left %i, select %i\n",Ur,Uu,Ud,Ul,Us); } void loop(){ if ((millis()-tic) > 200) handleKeys(); //Serielle Schnittstelle abfragen if (Serial.available() > 0) { //Befehl von der Schnittstelle einlesen String inp = Serial.readStringUntil('\n'); //und zur Kontrolle ausgeben Serial.println(inp); char cmd = inp[0]; //erstes Zeichen ist das Kommando if ((cmd == 'M') || (cmd == 'm')) { //war das Zeichen 'M' wird die Betriebsart eingestellt char newMode = inp[1]; //zweites Zeichen ist die Betriebsart uint8_t nm=0; switch (newMode) { case 's': case 'S': nm=0; break; case 'r': case 'R': nm=1; break; case 't': case 'T': nm=2; break; } if (nm != mode) { //Nur wenn eine Änderung vorliegt, muss was getan werden stopAll(); mode=nm; controlGenerator(); } } else { //bei den anderen Befehlen folgt ein Zahlenwert String dat = inp.substring(1); //je nach Befehl, werden die Daten geändert switch (cmd) { case 'F' : case 'f' :frequency = dat.toDouble(); break; //Frequenz case 'T' : case 't' :ratio = dat.toInt(); break; //Tastverhältnis } //Grenzwerte werden überprüft if (ratio > 100) ratio = 100; if (frequency < 20) frequency = 20; if (frequency > 20000) frequency = 20000; controlGenerator(); } //aktuelle Werte ausgeben displayValues(true); Serial.print("Kommando M,F,T : "); } }
Der Teil für den Funktionsgenerator und für die Bedienung über die serielle Schnittstelle ist identisch mit dem Sketch aus Teil 1. Neu ist die Bedienung über die Taster am Display Keypad Shield. Insbesondere die Auswertung der Tasten erfordert eine besondere Beachtung.
![]() |
Bild 4: Spannungsteiler für die Tasten am LCD Keypad Shield und links das ESP32 D1 R32 Board mit dem Parallelwiderstand
Der Spannungsteiler ist so ausgelegt, dass bei 5V der Unterschied zwischen den einzelnen Spannungen bei etwa 1V liegt. Da wir aber den 3900 Ohm Schutzwiderstand eingebaut haben, damit der Ausgang nie mehr als 3.3 V erreicht, reduziert sich dieser Unterschied. Aber das Problem wird noch größer. Das ESP32 D1 R32 Board hat einen 1kOhm Widerstand parallel zum GPIO2 Eingang, damit zum Flashen nur der GPIO0 auf GND gelegt werden muss. Durch diesen Widerstand reduziert sich der Spannungsunterschied weiter. Die Tabelle zeigt die Spannungen die man von den Tastern erhält (ohne Parallelwiderstand, mit 3900 Ohm parallel und mit 3900 Ohm und 1000 Ohm parallel.
Taster | Ohne Parallelwiderstand | 3900 Ohm | 3900 Ohm und 1000 Ohm |
---|---|---|---|
Kein Taster | 5,00 V | 3,30 V | 2,20 V |
SELECT | 3,60 V | 2,60 V | 2,04 V |
LEFT | 2,50 V | 1,97 V | 1,10 V |
DOWN | 1,60 V | 1,4 V | 0,89 V |
UP | 0,70 V | 0,66 V | 0,52 V |
RIGHT | 0,00 V | 0,00 V | 0,00 V |
Bei Tests hat sich gezeigt, dass die 5V Spannung nicht sehr genau ist. Mit einem externen Netzteil war die Spannung 4,4V, mit der Versorgung über USB zwischen 5,0V an einem Computeranschluss und 5,2V an einem USB Ladegerät.
Würde man nun mit festen Schwellwerten für jeden Taster arbeiten, würde das Gerät je nach Betriebsspannung funktionieren oder auch nicht. Um eine sichere Funktion zu gewährleisten, wird daher beim Einschalten die Betriebsspannung ohne gedrückte Taste ermittelt. Daraus lassen sich dann die mittleren Spannungen je Tasten berechnen. Da die Abfrage bei der Tastenauswertung auf kleiner lautet, erhält man das beste Ergebnis, wenn man zur mittleren Spannung die Hälfte des Abstandes zum nächst höheren Mittelwert addiert um den Schwellwert zu erhalten.
Das Gehäuse
Um die Nutzbarkeit zu erhöhen, haben wir ein Gehäuse entworfen, das mit einem 3D Drucker hergestellt werden kann. Das Gehäuse ist so aufgebaut, dass alle Teile darin Platz finden.
![]() |
Bild 5: Gehäuse aus dem 3D-Drucker
Am Boden sieht man die rechteckige Ausnehmung für das Display und die Öffnungen für die Knöpfe. Rechts daneben die zwei Bohrungen für die Potentiometer. In der rechten Wand ist eine Bohrung für die BNC-Buchse und an der linken Wand eine rechteckige Ausnehmung für den USB-Stecker und für den Stromversorgungs-Stecker.
Zur Befestigung der Leiterplatten sind jeweils vier Distanz-Zylinder mit Bohrungen vorhanden.
Der passende Deckel hat keine Ausnehmungen und kann ganz einfach aufgesteckt werden.
Die Knöpfe sind zur einfacheren Montage auf einer gemeinsamen Grundplatte angeordnet. Damit sie einzeln gedrückt werden können, müssen sie mit einem elastischen Filament, zum Beispiel TPU, gedruckt werden. Sollte das nicht möglich sein, müssen die einzelnen Knöpfe nach dem Drucken von der Grundplatte getrennt werden, damit sie einzeln betätigt werden können.
![]() |
![]() |
Bild 6: Knöpfe aus dem 3D-Drucker
Schließlich gibt es noch zwei Distanzstücke, die zwischen Shield und Display eingefügt werden sollen, um beim Festschrauben eine Verspannung des Displays zu verhindern.
![]() |
Bild 7: LCD Keypad Shield, Einbau der Distanzstücke
Links zum Herunterladen der 3D Druckdateien:
Gehäuse Unterteil, Deckel, Knöpfe, Distanzstück 1, Distanzstück 2
Zusammenbau
Das LCD Keypad Shield hat neben den Stiftleisten, die für die Verbindung zum ESP32 D1 R32 Board gebraucht werden, noch freie Lötpunkte, die parallel mit den Stiftleisten verbunden sind. Diese verwenden wir, um die externen Module mit dem Board zu verbinden. Dazu müssen wir sie ebenfalls mit Stiftleisten bestücken.
![]() |
Ebenso sollte der 3.9 kOhm Schutzwiderstand nicht vergessen werden.
Den Zusammenbau beginnen wir mit den Knöpfen Sie werden in die entsprechenden Ausnehmungen im Gehäuse eingelegt. Es folgen die beiden Potentiometer mit Anschlusskabel. Für die Anschlusskabel halbieren wir ein 3-poliges Jumper Wire Kabel und verlöten diese mit den Potentiometern. Eine Zugentlastung mit einem Kabelbinder ist von Vorteil.
![]() |
![]() |
Ein 2-poliges Jumper Wire Kabel wird ebenfalls halbiert und mit der BNC-Buchse verlötet.
Wenn die beiden Potentiometer und die BNC-Buchse eingebaut sind, kann das LCD-Keypad Shield im Gehäuse mit vier Schrauben befestigt werden. Nicht vergessen, die beiden Distanzstücke zwischen Display und Platine einzulegen. Auch die das Spannungswandler-Modul und das Verstärkermodul können mit je vier Schrauben befestigt werden.
![]() |
Die Kabel können nun entsprechend dem Verdrahtungsplan eingesteckt werden.
![]() |
Als Letztes wird das ESP32 D1 R32 Board auf das LCD-Keypad Shield aufgesteckt.
![]() |
Bild 13: Gehäuseunterteil mit dem kompletten Aufbau
Nun noch den Deckel drauf, und Knöpfe für die Potentiometer montieren, dann ist unser Funktionsgenerator einsatzbereit. Zur Stromversorgung können wir wahlweise ein USB-Ladegerät oder ein Netzteil mit 6 bis 12 V verwenden.
![]() |
Bild 14: Fertiger Funktionsgenerator am Oszilloskop
Viel Erfolg beim Nachbau.
6 Reacties
Andreas Wolter
@Bastlersiggi: wahrscheinlich wurde die Bibliothek verändert. Es gibt die Funktion i2s_write(). Damit könnte es wieder funktionieren. Oder zur älteren Version zurückkehren.
https://github.com/earlephilhower/ESP8266Audio/issues/273
Mit freundlichen Grüßen,
Andreas Wolter
AZ-Delivery Blog
Bastlersiggi
Der obige Sketch ergibt bei mir folgende Fehlermeldung:
FunktionsgeneratorESP32.ino:
In function ‘double triangleSetFrequency(double, uint8_t)’:
146:2: error: ‘i2s_write_bytes’ was not declared in this scope
i2s_write_bytes((i2s_port_t)0, (const char )&buf, size8, 100);
^~~~~~~~~~~~~~~
i2s_write_expand
exit status 1
Compilation error: ‘i2s_write_bytes’ was not declared in this scope
Verwendete IDE: 2.3.2 Board: ESP32 WROOM
Welchen Code kann man stattdessen verwenden bzw. den Sketch aktualisieren?
Gerald Lechner
Hallo Claus, GPIO12 ist schon richtig. Es ist nicht GPIO02 gemeint. Der Pin GPIO12 dient beim ESP32 dazu, die Versorgungsspannung für den Flash-Speicher zu steuern. Ist GPIO12 beim Bootvorgang auf High, beträgt die Versorgungsspannung 1.8 V, bei LOW 3.3V. Daher muss GPIO12 beim Booten auf LOW sein.
Das mit dem Widerstand ist richtig. Beim ESP32 Board könnte er weggelassen werden. Ich habe ihn auf das Display gelötet, da ich dieses auch mit der ESP8266 Version genutzt habe. Bei diesem Board ist dieser Anschluss mit A0 verbunden. A0 ist aber ein hochohmiger Eingang, der dann mit 5V beschädigt würde. Mit dem 3.9 kOhm Widerstand ist man auf der sicheren Seite und es funktionieren die Tasten trotzdem problemlos. Der Funktionsgenerator funktioniert natürlich nur mit dem ESP32!
Claus Teubner
Hallo,
wie immer, sehr schöner Artikel.
Heute habe ich den versteckten Tipfehler :-) gefunden.
‘…Ein weiteres Problem haben wir mit dem GPIO12 Anschluss. Der muss während des Bootvorgangs auf 0V liegen…’
Sollte doch GPIO2 statt GPIO12 Anschluss sein.
Außerdem sollte doch der 3,9kOhm Widerstand direkt am ESP32 reichen, dann bleibt das Display unverändert und wenn beides zusammengesteckt ist, ist sichergestellt, dass nur max. 3,3V am ESP anliegen. Falls die 3,9kOhm zum booten nicht reicht, tut es auch der 1kOhm alleine, dann sind die Taster-Spannungen etwas größer.
Bernd Albrecht
@Sven: Der D1 R32 ist “baulich” am Uno angelehnt. Die Uno Shields können also aufgesteckt werden. Aber es gibt deutliche Unterschiede bei der Pinbelegung, der gravierendste ist die zulässige Spannung von 3,3V beim D1 R32. Um das LCD Keypad Shield zu verwenden, müssen deshalb wie beschrieben die zusätzlichen Widerstände angelötet werden.
Sven Waibel
Hallo,
wieso steht beim LCD, den ihr verlinkt habt, dass dieser nicht mit dem D1 R32 kompatibel ist?
Grüße
Sven