Funktionsgenerator mit dem ESP32, Display und Gehäuse - [Teil 3]

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 : ");
  }
}

Sketch zum Herunterladen:

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.

Bild 8: Zusätzliche Stiftleisten am LCD Keypad Shield

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.

Bild 9: Einbau der Knöpfe im Gehäuse

Bild 10: Einbau der Potentiometer und der BNC Buchse (Signalausgang)

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.

Bild 11: Gehäuse mit LCD Keypad Shield, DC-DC- Wandler, Operationsverstärker und Verkabelung


Die Kabel können nun entsprechend dem Verdrahtungsplan eingesteckt werden.

Bild 12: Verdrahtungsplan


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.

Link zum Blogbeitrag als PDF

DisplaysEsp-32Projekte für fortgeschrittene

4 Kommentare

Gerald Lechner

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

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

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

Sven Waibel

Hallo,
wieso steht beim LCD, den ihr verlinkt habt, dass dieser nicht mit dem D1 R32 kompatibel ist?
Grüße
Sven

Einen Kommentar hinterlassen

Alle Kommentare werden vor der Veröffentlichung moderiert

Aanbevolen blog berichten

  1. Installeer ESP32 nu van de raad van bestuur
  2. Lüftersteuerung Raspberry Pi
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1
  4. ESP32 - das Multitalent
  5. OTA - Over the Air - ESP Programmeren via Wi-Fi