Seite 1 von 1

DIY: digitales aber analoges VU-Meter

Verfasst: So 12 Apr, 2026 09:20
von ruessel
Im Netz gefunden:



Was haben wir hier, ein scheinbar analoges VU Meter und eine Peakled bei Übersteuerungen. Ich habe mal geschaut was so ein analoges Instrument kostet: ab 25,- aufwärts, dann kommt noch die analoge Ansteuerung dazu, nochmal 10,- Teile. Hier sind wir bei einen ESP32 ab 6,- Euro eine LED für wenige Cents und das runde LCD für 6,- EUR. Also deutlich günstiger. Außerdem wären noch weitere Möglichkeiten erstellbar, ein kleines z.B. rotes Dreieck was den Spitzenpegel für 1 Sekunde anzeigt, oder eine Tonkanalanzeige.... was auch immer, wird ja in der Software festgelegt und ist damit jederzeit änderbar.
Leider gibt er nur ein kleines Demoskript mit ruckelnden Zeiger frei, zumindest ist dieses Demo wie oben von mir nicht zu finden. Grund genug es selber mal versuchen und noch weiter zu verbessern. Ich könnte mir noch einen Drehschalter vorstellen, wo man noch andere Skalen und Farben wählen kann. Der Zeiger wird vom ESP32 definiert, die Skala ist nur ein Hintergrundbild. Dies über einen Drehschalter frei auszutauschen sehe ich nicht als großes Problem an. Noch nicht.

Re: DIY: digitales aber analoges VU-Meter

Verfasst: So 12 Apr, 2026 10:08
von ruessel
Meine Idee wäre Stereo beizubehalten aber nur einen Prozessor zu benutzen. Ich habe mal die KI gefragt:

Ein ESP32‑S3 WROOM‑1 N16R8 (16 MB Flash, 8 MB PSRAM) ist eine sehr gute, preiswerte Wahl für ein Stereo‑VU‑Meter mit zwei ADC‑Eingängen und zwei TFT‑Displays, weil er genug RAM/PSRAM, zwei I2S‑Peripherien und bessere HMI‑Leistung als der klassische ESP32 bietet.

Warum das Modul gut passt:
- PSRAM (8 MB) erlaubt große LVGL‑Draw‑Buffer für zwei 240×240‑Displays, sodass du flüssige UI‑Animationen ohne starke Kompromisse bei Puffergröße oder Bildrate realisieren kannst.

- Dual‑Core LX7 CPU und Hardware‑Features (I2S, GDMA, LCD/SPI‑Peripherie) erleichtern paralleles Audio‑Sampling (via I2S/DMA) und Display‑Refresh ohne große Blockaden.

- Das konkrete Modul N16R8 ist weit verbreitet als Dev‑Board/Modul mit 16 MB Flash und 8 MB PSRAM, gut dokumentiert und preislich attraktiv.

Also, die Würfel sind gefallen es wird Stereo mit einem 9,- Prozessor.

Re: DIY: digitales aber analoges VU-Meter

Verfasst: Di 14 Apr, 2026 17:32
von ruessel
Ich warte immer noch auf das Modul mit 8MB PSRAM. Im Regal liegt aber noch ein ESP32 mit 500 KB PSRAM, mal schauen was da geht. Mein erstes Testskript waren rollende Augen die auf zwei Display angezeigt werden sollten. Ja, theoretisch klappte das, aber durch den Speichermangel dauerte das zeichnen des Auges zu lange, das Frame war zu kurz um das Auge vollständig anzeigen zu können. Selbst eine runtersetzung der Bildfolge sah auch noch schlimm aus.

Jetzt habe ich die zu zeichnenen Bildelemente klein gehalten, damit klappt es. Ist nur ein kleines Demo um an einem ESP32 zwei Bildschirme mit jeweils geänderte Inhalte zu zeigen. Auf Bildschirm 1 (links) ist eine VU Balkenanzeige die hoch und runterläuft. Auf Bildschirm 2 (rechts) ist ein analoges VU Meter dargestellt, wo der Zeiger hin und her ausschlägt. Läuft flüssig und könnte wahrscheinlich noch einen dritten oder vierten Bildschirm ansteuern. Es dürfen nur nicht große Flächen gezeichnet werden - bei so wenig Speicher.
Einen kleinen Fehler hat noch das Skript, die Farben. Die auf dem Display dargestellten Farben stimmen nicht mit den programmierten Farben überein. Ob es an der "LovyanGFX" Bibliothek liegt weiß ich nicht, vermute es aber. Hier werden evtl. die Farben anders gehandelt, dazu fehlt mir im Moment noch das Wissen, bin froh das es so schon klappt.

P1000027.JPG

Code: Alles auswählen

// Zwei Displays: VU Bar (Display1) + Analog Nadel (Display2)
// ESP32 + LovyanGFX
// Pins anpassen falls nötig - Ruesseltechnik 14.04.2026

#include <LovyanGFX.hpp>

#define DEG_TO_RAD (3.14159265f/180.0f)

// Shared SPI pins  MOSI = SDA; SCLK = SLC
#define PIN_MOSI 23
#define PIN_SCLK 18

// Display 1 (VU Bar)
#define D1_CS   5
#define D1_DC   16
#define D1_RST  2
#define D1_BL   14  // -1 wenn nicht vorhanden

// Display 2 (Analog Nadel)
#define D2_CS   17
#define D2_DC   19
#define D2_RST  15
#define D2_BL   13  // -1 wenn nicht vorhanden

// ---------------- Device Definitions ----------------
struct LGFX_D1 : public lgfx::LGFX_Device {
  lgfx::Panel_GC9A01 _panel;
  lgfx::Bus_SPI _bus;
  LGFX_D1() {
    lgfx::Bus_SPI::config_t bus_cfg;
    bus_cfg.spi_host   = VSPI_HOST;
    bus_cfg.spi_mode   = 0;
    bus_cfg.freq_write = 20000000; // stabiler Wert
    bus_cfg.freq_read  = 16000000;
    bus_cfg.pin_sclk   = PIN_SCLK;
    bus_cfg.pin_mosi   = PIN_MOSI;
    bus_cfg.pin_miso   = -1;
    bus_cfg.pin_dc     = D1_DC;
    _bus.config(bus_cfg);

    lgfx::Panel_GC9A01::config_t panel_cfg;
    panel_cfg.pin_cs   = D1_CS;
    panel_cfg.pin_rst  = D1_RST;
    _panel.config(panel_cfg);

    _panel.bus(&_bus);
    setPanel(&_panel);
  }
};

struct LGFX_D2 : public lgfx::LGFX_Device {
  lgfx::Panel_GC9A01 _panel;
  lgfx::Bus_SPI _bus;
  LGFX_D2() {
    lgfx::Bus_SPI::config_t bus_cfg;
    bus_cfg.spi_host   = VSPI_HOST; // same SPI host, different CS
    bus_cfg.spi_mode   = 0;
    bus_cfg.freq_write = 20000000;
    bus_cfg.freq_read  = 16000000;
    bus_cfg.pin_sclk   = PIN_SCLK;
    bus_cfg.pin_mosi   = PIN_MOSI;
    bus_cfg.pin_miso   = -1;
    bus_cfg.pin_dc     = D2_DC;
    _bus.config(bus_cfg);

    lgfx::Panel_GC9A01::config_t panel_cfg;
    panel_cfg.pin_cs   = D2_CS;
    panel_cfg.pin_rst  = D2_RST;
    _panel.config(panel_cfg);

    _panel.bus(&_bus);
    setPanel(&_panel);
  }
};

// Instanzen
LGFX_D1 tft1; // VU Bar
LGFX_D2 tft2; // Analog Nadel

// ---------------- Backlight helper ----------------
void setBacklight(int pin, bool on) {
  if (pin < 0) return;
  pinMode(pin, OUTPUT);
  digitalWrite(pin, on ? HIGH : LOW);
}

// ---------------- Display1 VU Bar ----------------
const int barX = 100;
const int barY = 40;
const int barW = 40;
const int barH = 160;
const int border = 3;

uint16_t colBg1, colBorder1, colAccent1, colBarLow1, colBarHigh1;
int prevFill1 = -1;

void drawStaticBackground1() {
  tft1.fillScreen(0);
  tft1.fillRoundRect(barX - border, barY - border, barW + 2*border, barH + 2*border, 6, colBorder1);
  tft1.fillRect(barX, barY, barW, barH, colBg1);
  for (int i = 0; i <= 8; ++i) {
    int y = barY + i * (barH / 8);
    tft1.drawFastHLine(barX + barW + 6, y, 8, colAccent1);
  }
}

void updateBar1(int h) {
  if (h < 0) h = 0;
  if (h > barH) h = barH;
  if (h == prevFill1) return;

  if (prevFill1 >= 0 && h < prevFill1) {
    int y = barY + (barH - prevFill1);
    int hdel = prevFill1 - h;
    tft1.fillRect(barX, y, barW, hdel, colBg1);
  }

  if (h > 0) {
    int y = barY + (barH - h);
    int split = barH * 2 / 3;
    if (h <= split) {
      tft1.fillRect(barX, y, barW, h, colBarLow1);
    } else {
      int greenH = split;
      int redH = h - greenH;
      int yGreen = barY + (barH - greenH);
      int yRed = barY + (barH - h);
      tft1.fillRect(barX, yGreen, barW, greenH, colBarLow1);
      tft1.fillRect(barX, yRed, barW, redH, colBarHigh1);
    }
  }
  prevFill1 = h;
}

// ---------------- Display2 Analog Dial ----------------
// Dial geometry
const int cx2 = 120;
const int cy2 = 120;
const int radius2 = 90;
const int needleLen = 78;
// Hier die Nadeldicke einstellen (größer = dicker)
const int needleW = 14;

uint16_t dialBg, dialTick, needleColor;
float prevAngle = 0.0f;

// Draw static dial (background, ticks, labels)
void drawDialBackground() {
  tft2.fillScreen(0); // schwarz
  // Dial background circle
  tft2.fillCircle(cx2, cy2, radius2 + 6, tft2.color565(30,30,30)); // outer ring
  tft2.fillCircle(cx2, cy2, radius2, tft2.color565(10,10,10));     // inner bg

  // ticks
  for (int i = -60; i <= 60; i += 15) { // -60..60 degrees
    float a = i * DEG_TO_RAD;
    int x1 = cx2 + (int)((radius2 - 6) * cos(a));
    int y1 = cy2 + (int)((radius2 - 6) * sin(a));
    int x2 = cx2 + (int)((radius2 - 18) * cos(a));
    int y2 = cy2 + (int)((radius2 - 18) * sin(a));
    tft2.drawLine(x1, y1, x2, y2, dialTick);
  }

  // center hub
  tft2.fillCircle(cx2, cy2, 6, tft2.color565(200,200,200));
}

// Zeichnet die Nadel als gefülltes Dreieck (dicker, sauber)
void drawNeedle(float angle, uint16_t color) {
  // Spitze
  int tipX = cx2 + (int)(needleLen * cos(angle));
  int tipY = cy2 + (int)(needleLen * sin(angle));
  // Basis halbbreite (perpendicular)
  float px = sin(angle);
  float py = -cos(angle);
  int half = needleW / 2;
  int base1X = cx2 + (int)(px * half);
  int base1Y = cy2 + (int)(py * half);
  int base2X = cx2 - (int)(px * half);
  int base2Y = cy2 - (int)(py * half);

  // gefülltes Dreieck: tip, base1, base2
  tft2.fillTriangle(tipX, tipY, base1X, base1Y, base2X, base2Y, color);
  // optional: kleiner heller Rand am Zentrum
  tft2.fillCircle(cx2, cy2, 3, tft2.color565(220,220,220));
}

// Löscht vorherige Nadel: Bounding-Box des Dreiecks berechnen und Hintergrund + Ticks neu zeichnen
void eraseNeedle(float angle) {
  int tipX = cx2 + (int)(needleLen * cos(angle));
  int tipY = cy2 + (int)(needleLen * sin(angle));
  float px = sin(angle);
  float py = -cos(angle);
  int half = needleW / 2;
  int base1X = cx2 + (int)(px * half);
  int base1Y = cy2 + (int)(py * half);
  int base2X = cx2 - (int)(px * half);
  int base2Y = cy2 - (int)(py * half);

  int minx = tipX;
  int maxx = tipX;
  int miny = tipY;
  int maxy = tipY;
  // include base1
  if (base1X < minx) minx = base1X;
  if (base1X > maxx) maxx = base1X;
  if (base1Y < miny) miny = base1Y;
  if (base1Y > maxy) maxy = base1Y;
  // include base2
  if (base2X < minx) minx = base2X;
  if (base2X > maxx) maxx = base2X;
  if (base2Y < miny) miny = base2Y;
  if (base2Y > maxy) maxy = base2Y;
  // include center
  if (cx2 < minx) minx = cx2;
  if (cx2 > maxx) maxx = cx2;
  if (cy2 < miny) miny = cy2;
  if (cy2 > maxy) maxy = cy2;

  minx -= 6; miny -= 6; maxx += 6; maxy += 6;

  // Clip to screen
  if (minx < 0) minx = 0;
  if (miny < 0) miny = 0;
  int w = (int)tft2.width();
  int h = (int)tft2.height();
  if (maxx >= w)  maxx = w - 1;
  if (maxy >= h)  maxy = h - 1;

  // Fülle Hintergrund in der Box
  tft2.fillRect(minx, miny, maxx - minx + 1, maxy - miny + 1, tft2.color565(10,10,10));

  // Redraw ticks that may intersect (schnelle Methode: redraw all ticks)
  for (int i = -60; i <= 60; i += 15) {
    float a = i * DEG_TO_RAD;
    int x1 = cx2 + (int)((radius2 - 6) * cos(a));
    int y1 = cy2 + (int)((radius2 - 6) * sin(a));
    int x2 = cx2 + (int)((radius2 - 18) * cos(a));
    int y2 = cy2 + (int)((radius2 - 18) * sin(a));
    if (!((x1 < minx && x2 < minx) || (x1 > maxx && x2 > maxx) || (y1 < miny && y2 < miny) || (y1 > maxy && y2 > maxy))) {
      tft2.drawLine(x1, y1, x2, y2, dialTick);
    }
  }

  // redraw center hub if inside region
  if (cx2 >= minx && cx2 <= maxx && cy2 >= miny && cy2 <= maxy) {
    tft2.fillCircle(cx2, cy2, 6, tft2.color565(200,200,200));
  }
}

// Map normalized value 0..1 to angle (radians) for needle
float valueToAngle(float v) {
  // map 0..1 to -60deg .. +60deg
  float deg = -60.0f + v * 120.0f;
  return deg * DEG_TO_RAD;
}

// ---------------- Setup and Loop ----------------
unsigned long last = 0;
float phase = 0;

void setup() {
  Serial.begin(115200);
  delay(100);

  // Backlights
  setBacklight(D1_BL, true);
  setBacklight(D2_BL, true);

  // Colors for display1
  colBg1 = tft1.color565(10,10,10);
  colBorder1 = tft1.color565(0,120,0);
  colAccent1 = tft1.color565(0,200,0);
  colBarLow1 = tft1.color565(0,200,0);
  colBarHigh1 = tft1.color565(200,0,0);

  // Colors for display2
  dialBg = tft2.color565(10,10,10);
  dialTick = tft2.color565(180,180,180);
  needleColor = tft2.color565(255,80,80);

  // Init displays
  tft1.init();
  tft1.setRotation(0);
  drawStaticBackground1();
  prevFill1 = -1;

  tft2.init();
  tft2.setRotation(0);
  drawDialBackground();
  prevAngle = valueToAngle(0.0f);
  // draw initial needle at 0
  drawNeedle(prevAngle, needleColor);
}

void loop() {
  if (millis() - last < 30) return; // ~33 FPS
  last = millis();

  // Simulated input values (replace with real input)
  phase += 0.08f;
  float v1 = (sin(phase) + 1.0f) / 2.0f;        // for bar (0..1)
  float v2 = (sin(phase * 0.9f + 1.2f) + 1.0f) / 2.0f; // for needle

  // Update Display1 bar (only small region)
  int fillH = (int)(v1 * v1 * barH);
  updateBar1(fillH);

  // Update Display2 needle: erase previous and draw new
  float newAngle = valueToAngle(v2);
  eraseNeedle(prevAngle);
  drawNeedle(newAngle, needleColor);
  prevAngle = newAngle;
}