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