19.01.2026

RP2350 DVI Wetterstationsprojekt: Verwende deine ungenutzten DVI-Displays neu

Raspberry Pi
Raspberry Pi
Arduino
RP2350 DVI Weather Station Project: Repurpose your unused DVI displays

Table of contents

Einführung
Einführung in den RP2350 und seine DVI-Fähigkeiten
Übersicht über den BME280-Sensor Verkabelung und Einrichtung Arduino-Programmierung Initiale Konfiguration
Lesen von Sensordaten
Erstellen der grafischen Elemente Das Ergebnis Mögliche Verbesserungen
Table of contents
Einführung
Einführung in den RP2350 und seine DVI-Fähigkeiten
Übersicht über den BME280-Sensor Verkabelung und Einrichtung Arduino-Programmierung Initiale Konfiguration
Lesen von Sensordaten
Erstellen der grafischen Elemente Das Ergebnis Mögliche Verbesserungen

Einführung

Haben Sie sich jemals mit einem ungenutzten DVI-Monitor wiedergefunden? Vielleicht ein alter PC-Bildschirm, ein kleiner tragbarer Bildschirm oder sogar ein übrig gebliebener Fernseher, und sich gefragt, ob er in etwas Nützliches verwandelt werden könnte?

Mit dem neuen Soldered NULA Max RP2350 Mikrocontroller und einem einfachen BME280 Umweltsensor kann dieser Bildschirm zu einer voll funktionsfähigen DVI-Wetterstation werden.

Dieselbe Grafikausgabetechnologie, die in Verbrauchergeräten, Kiosken und digitaler Beschilderung verwendet wird, ist jetzt auf einem Mikrocontroller verfügbar, den Sie über einen USB-C-Anschluss mit Strom versorgen können. Koppeln Sie ihn mit dem BME280, und plötzlich wird jedes freie DVI-Display zu einem intelligenten, ständig eingeschalteten Ambient-Wetter-Hub für Ihr Zuhause oder Ihren Arbeitsplatz.

In diesem Artikel werden Sie:

  • lernen, wie Mikrocontroller wie der RP2350 volle DVI-Signale ausgeben können

  • verstehen, was der BME280 misst und warum er ideal für kompakte Wetterstationen ist

  • sehen, wie man beide Komponenten mit minimaler Hardware verkabelt

  • einen vollständigen Arduino-Sketch zum Lesen von Sensoren und Rendern von Grafiken erhalten

  • Ihr eigenes Plug-and-Play-Wetter-Dashboard für jeden HDMI-Bildschirm bauen

Egal, ob Sie einen Schreibtischbegleiter, einen Werkstattmonitor oder ein Info-Panel für das Wohnzimmer wünschen, dieses Projekt ermöglicht es Ihnen, alten Displays mit moderner Open-Source-Hardware neues Leben einzuhauchen.

Einführung in den RP2350 und seine DVI-Fähigkeiten

Der RP2350 ist die neueste Evolution der Mikrocontroller-Reihe von Raspberry Pi – kompakt, erschwinglich und für Maker entwickelt. Unser NULA Max Board nutzt alle seine Funktionen und fügt zusätzlich einen DVI-Ausgangsanschluss hinzu. Mit ihm sowie den richtigen Bibliotheken kann der Chip echte, stabile Videosignale direkt von seinen Pins erzeugen, ohne dass zusätzliche Grafikhardware erforderlich ist.

Dies wird dank der unglaublichen PicoDVI-Bibliothek ermöglicht. Sie verwendet clevere PIO- (Programmable I/O) und DMA-Techniken, um DVI-kompatible DVI-Displays vollständig in Software anzusteuern. 

Für Arduino-Entwickler ist das Beste daran, dass die PicoDVI-Bibliothek vollständig an das Arduino-Ökosystem angepasst wurde. Noch besser ist, dass Adafruit einen Fork pflegt, der PicoDVI mit der vertrauten Adafruit GFX-Umgebung verbindet. Wenn Sie jemals ein Inkplate-Display mit Adafruit GFX verwendet haben, werden Sie sich beim Zeichnen von Formen, Text, Symbolen oder ganzen UI-Panels wie zu Hause fühlen.

Mit nur wenigen Drähten und dem RP2350-Board können Sie ein sauberes digitales Signal an praktisch jeden DVI-Bildschirm ausgeben.

Übersicht über den BME280-Sensor

Enviromental sensor BME280 breakout-easyC ecosystem

 

Der BME280 Umweltsensor ist ein atmosphärischer Sensor, der drei Werte misst: Temperatur, Luftdruck und Luftfeuchtigkeit. Zusätzlich kann die Höhe berechnet werden. Er ist einfach zu bedienen, da er über I2C kommuniziert und für nahtlose Qwiic-Konnektivität ausgelegt ist.

 Er ist extra klein, sodass er überall platziert werden kann. Dieser Sensor misst alles, was Sie über atmosphärische Bedingungen wissen müssen, also ist er ideal für meteorologische Stationsprojekte!

Verkabelung und Einrichtung

Die Verkabelung des Projekts könnte nicht einfacher sein, hier sind die benötigten Komponenten:

  • Soldered NULA Max RP2350

  • Umweltsensor BME280 Breakout

  • Qwiic-Kabel

  • DVI-Kabel

Verbinden Sie den BME280 über ein Qwiic-Kabel und den Monitor über das DVI-Kabel mit dem NULA Max.

Installieren Sie die folgenden Bibliotheken:


Ein Tutorial zur Installation der Soldered NULA Max RP2350 Board-Definition ist hier verfügbar.

Arduino-Programmierung

Die folgenden Beispiele gehen darauf ein, wie jede Komponente initialisiert wird, wie Sensordaten gelesen werden und wie die UI-Elemente auf das Display gezeichnet werden.

Initiale Konfiguration

Der erste Schritt im Projekt ist die Definition, wie der RP2350 DVI-Signale ausgeben soll. Dies geschieht mit einer Display-Konfigurationsstruktur. Wenn Sie den Soldered-Fork von PicoDVI verwenden, kann dieser Schritt übersprungen werden, da der Fork bereits die korrekte Standardkonfiguration enthält.

Nach der Definition der Konfiguration wird ein Display-Objekt unter Verwendung der DVIGFX8-Klasse erstellt. Dies wählt einen 8-Bit-Farbmodus und eine Auflösung von 320×240 bei 60 Hz, was gut für leichtgewichtige Grafiken auf Mikrocontrollern funktioniert. Die Konfiguration ordnet die RP2350-Pins den DVI-TMDS-Kanälen zu und bereitet die Bibliothek darauf vor, gültige Videosignale zu erzeugen.

// Display configuration structure
struct dvi_serialiser_cfg rp2350_soldered_nula_dvi_cfg = {
   .pio              = DVI_DEFAULT_PIO_INST, // Use default PIO instance
   .sm_tmds          = {0, 1, 2},            // State machines for TMDS
   .pins_tmds        = {14, 16, 18},         // TMDS data pins
   .pins_clk         = 12,                   // Clock pin
   .invert_diffpairs = true                  // Invert differential pairs
};

DVIGFX8 display(DVI_RES_320x240p60, true, rp2350_soldered_nula_dvi_cfg); // Create display object
BME280 bme280; // Create sensor object

 

Es ist auch erforderlich, Ihre WLAN-Zugangsdaten einzugeben, damit die Zeit über den NTP-Server abgerufen werden kann:

const char* ssid     = "YOUR_SSID_HERE";     // WiFi SSID
const char* password = "YOUR_PASSWORD_HERE"; // WiFi password
const char* ntpServer1       = "pool.ntp.org";  // Primary NTP server
const char* ntpServer2       = "time.nist.gov"; // Secondary NTP server
const long gmtOffset_sec     = 0;               // GMT timezone offset
const int daylightOffset_sec = 0;               // No daylight saving
const char* timezone         = "EST-2";

 

Innerhalb von setup() werden mehrere Dinge initialisiert:

  1. Serielle Kommunikation (optional, aber nützlich für das Debugging)

  2. Der BME280-Sensor

  3. WLAN-Verbindung

  4. NTP-Zeitsynchronisation

  5. DVI-Display

  6. Einrichtung der Farbpalette und eine einfache Statusmeldung

  7. Vorbereitung der Datenpuffer für Diagramme

void setup()
{
  Serial.begin(115200); // Initialize serial communication

  // Uncomment the line below if you are debugging, with the line uncommented nothing will show
  // On the display before a serial communication is available
  //while(!Serial) {} // Wait for serial connection

  Serial.println("Starting config");

  bme280.begin(); // Initialize sensor

  // Connect to WiFi
  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected to Wi-Fi!");

  // Configuring NTP communication as well as the timezone
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
  setenv("TZ", timezone, 1);
  tzset();
  Serial.println("Syncing time via NTP...");
  delay(1000);
  int retries = 0;
  while (!time(nullptr) && retries < 20)
  {
    Serial.print(".");
    delay(500);
    retries++;
  }
  time_t now = time(nullptr);
  retries    = 0;
  do
  {
    now = time(nullptr);
    retries++;
    delay(200);
  } while ((!now || now < 1000000000) && (retries < 30));
  Serial.println("Time synced successfully!");

  // Initialize display
  if (!display.begin())
  {
    Serial.println("Display init failed!");
    while (1)
      ; // Halt if display fails
  }

  // Set color palette
  uint16_t* pal   = display.getPalette();
  pal[COLOR_BG]   = rgb565(0, 0, 32);        // Dark blue background
  pal[COLOR_TEMP] = rgb565(255, 64, 64);     // Red for temperature
  pal[COLOR_HUM]  = rgb565(64, 255, 255);    // Cyan for humidity
  pal[COLOR_PRES] = rgb565(255, 255, 64);    // Yellow for pressure
  pal[COLOR_TEXT] = rgb565(255, 255, 255);   // White for text
  display.swap(false, true);                 // Apply palette to both buffers

  display.fillScreen(COLOR_BG);            // Clear screen with background color
  display.setTextColor(COLOR_TEXT);        // Set text color
  display.setCursor(10, 100);              // Position cursor
  display.print("WiFi OK, Time synced");   // Display status message
  display.swap();                          // Update display
  delay(1000);

  // Initialize data arrays with zeros
  memset(tempData, 0, sizeof(tempData));
  memset(humData, 0, sizeof(humData));
  memset(presData, 0, sizeof(presData));
}


Lesen von Sensordaten

In der Hauptschleife (Main Loop) werden die Daten einmal pro Sekunde abgetastet. Jeder neue Temperatur-, Feuchtigkeits- und Druckmesswert wird in einem dedizierten Ringpuffer gespeichert. Diese Puffer halten die letzten 120 Datenpunkte für die Diagramme vor. Diese Struktur ermöglicht ein flüssiges Scrollen der Diagramme über das Display und macht es einfach, Trends über die Zeit zu verfolgen.

// Read sensor data
float temperature = bme280.readTemperature();
float humidity    = bme280.readHumidity();
float pressure    = bme280.readPressure();

// Store data in circular buffer
tempData[indexData] = temperature;
humData[indexData]  = humidity;
presData[indexData] = pressure;
indexData           = (indexData + 1) % GRAPH_POINTS; // Increment index with wrap-around

// Update min/max values
tempMin = min(tempMin, temperature);
tempMax = max(tempMax, temperature);
humMin  = min(humMin, humidity);
humMax  = max(humMax, humidity);
presMin = min(presMin, pressure);
presMax = max(presMax, pressure);

 

Erstellen der grafischen Elemente

Jeder Diagrammbereich wird mit der Funktion drawGraph() gerendert. Die Funktion:

  • zeichnet einen farbigen Hintergrund

  • fügt einen dünnen Rand hinzu

  • überlagert Gitterlinien

  • beschriftet das Diagramm

  • zeigt den aktuellen Wert, sowie Min und Max an

  • zeichnet die Datenlinie

void drawGraph(int x, int y, int w, int h, float* data, float minVal, float maxVal, uint16_t color,
               const char* label, float value)
{
  // Draw subtle background for graph area
  display.fillRect(x, y, w, h, rgb565(20, 20, 40)); // Dark blue-gray background

  // Draw thin border instead of thick rectangle
  display.drawRect(x, y, w, h, rgb565(100, 100, 120)); // Subtle border

  // Draw grid lines
  for (int i = 1; i < 4; i++)
  {
    int gridY = y + (h * i / 4);
    display.drawFastHLine(x + 1, gridY, w - 2, rgb565(40, 40, 60)); // Horizontal grid lines
  }
  for (int i = 1; i < 5; i++)
  {
    int gridX = x + (w * i / 5);
    display.drawFastVLine(gridX, y + 1, h - 2, rgb565(40, 40, 60)); // Vertical grid lines
  }

  // Draw axes with slightly brighter lines
  display.drawLine(x + 2, y + h - 12, x + w - 2, y + h - 12, rgb565(120, 120, 140)); // X-axis
  display.drawLine(x + 12, y + 10, x + 12, y + h - 2, rgb565(120, 120, 140));         // Y-axis

  // Display current value in a cleaner way
  display.setTextSize(1);
  display.setTextColor(color);
  display.setCursor(x + 15, y + 4);
  display.print(label);
  display.print(": ");
  display.print(value, 1);

  // Add min/max labels on Y-axis
  display.setTextColor(rgb565(150, 150, 170));
  display.setCursor(x + 2, y + 8);
  display.print((int) maxVal);
  display.setCursor(x + 2, y + h - 14);
  display.print((int) minVal);

  // Plot smoother graph lines with optional fill
  int baseY        = y + h - 12;
  int graphHeight  = h - 22;

  // Draw main graph line (thicker and smoother)
  for (int i = 1; i < GRAPH_POINTS; i++)
  {
    int prevX = x + (i - 1) * (w - 14) / GRAPH_POINTS + 12;
    int currX = x + i * (w - 14) / GRAPH_POINTS + 12;
    int prevY =
        baseY - (int) (((data[(indexData + i - 1) % GRAPH_POINTS] - minVal) / (maxVal - minVal)) *
                       graphHeight);
    int currY =
        baseY -
        (int) (((data[(indexData + i) % GRAPH_POINTS] - minVal) / (maxVal - minVal)) * graphHeight);

    // Draw thicker line for better visibility
    display.drawLine(prevX, prevY, currX, currY, color);
    // Optional: draw a second slightly offset line for thickness
    if (i % 2 == 0)
    {
      display.drawLine(prevX, prevY - 1, currX, currY - 1, color);
    }
  }

  // Draw current value indicator dot
  int latestX = x + (GRAPH_POINTS - 1) * (w - 14) / GRAPH_POINTS + 12;
  int latestY =
      baseY -
      (int) (((data[(indexData + GRAPH_POINTS - 1) % GRAPH_POINTS] - minVal) / (maxVal - minVal)) *
             graphHeight);
  display.fillCircle(latestX, latestY, 2, rgb565(255, 255, 255)); // White dot for current value
}

Es gibt auch eine Funktion, die eine Tabelle zeichnet, die die aufgezeichneten Maximal- und Minimalwerte jedes Datentyps sowie die aktuelle, vom NTP-Server abgerufene Zeit enthält:

void drawTable(int x, int y, int cellW, int cellH)
{
  display.setTextSize(1); // Set text size

  const char* labels[3] = {"Temp", "Hum", "Pres"};              // Row labels
  uint16_t colors[3]    = {COLOR_TEMP, COLOR_HUM, COLOR_PRES};  // Row colors
  float current[3]      = {tempData[(indexData + GRAPH_POINTS - 1) % GRAPH_POINTS],
                           humData[(indexData + GRAPH_POINTS - 1) % GRAPH_POINTS],
                           presData[(indexData + GRAPH_POINTS - 1) % GRAPH_POINTS]}; // Current values
  float minV[3]         = {tempMin, humMin, presMin}; // Minimum values
  float maxV[3]         = {tempMax, humMax, presMax}; // Maximum values

  int cols = 4; // Number of columns
  int rows = 4; // Number of rows

  // Draw table background
  display.fillRect(x, y, cellW * cols, cellH * rows, rgb565(30, 30, 50)); // Dark background

  // Draw header row with accent color
  const char* colLabels[4] = {"", "Min", "Curr", "Max"}; // Column headers
  for (int c = 0; c < cols; c++)
  {
    int cellX = x + c * cellW;
    int cellY = y;
    display.fillRect(cellX, cellY, cellW, cellH, rgb565(60, 60, 80));     // Header background
    display.drawRect(cellX, cellY, cellW, cellH, rgb565(100, 100, 120));  // Subtle border
    display.setTextColor(COLOR_TEXT);
    display.setCursor(cellX + 4, cellY + 4); // Slightly more padding
    display.print(colLabels[c]);             // Print header
  }

  // Draw data rows with colored labels
  for (int r = 0; r < 3; r++)
  {
    for (int c = 0; c < cols; c++)
    {
      int cellX = x + c * cellW;
      int cellY = y + (r + 1) * cellH;

      // Draw cell background and border
      display.fillRect(cellX, cellY, cellW, cellH, rgb565(40, 40, 60));   // Cell background
      display.drawRect(cellX, cellY, cellW, cellH, rgb565(80, 80, 100));  // Cell border

      display.setCursor(cellX + 4, cellY + 4); // Consistent padding

      if (c == 0)
      {
        display.setTextColor(colors[r]); // Use graph colors for labels
        display.print(labels[r]);        // Print row label
      }
      else
      {
        display.setTextColor(COLOR_TEXT); // White for data values
        if (c == 1)
          display.print(minV[r], 1); // Print minimum value
        else if (c == 2)
          display.print(current[r], 1); // Print current value
        else if (c == 3)
          display.print(maxV[r], 1); // Print maximum value
      }
    }
  }
}

 

Das Ergebnis

Wenn wir das Projekt auf das NULA Max Board flashen, werden wir mit einem Retro-Design begrüßt, das an Teletext erinnert und die Sensorwerte jede Sekunde aktualisiert!

*** Hinweis: Wenn Sie rote Linien über den gesamten Bildschirm sehen, gehen Sie zu Tools->Optimise->Optimise more (-O2)

Mögliche Verbesserungen

Dieses Projekt wurde erstellt, um zu demonstrieren, was der RP2350 mit seinem integrierten DVI-Ausgang leisten kann, daher konzentriert sich der Code absichtlich auf Einfachheit statt auf visuellen Feinschliff. Die Benutzeroberfläche ist einfach, aber genau da beginnt der Spaß.

Wir ermutigen Sie, diese Grundlage zu nehmen, sie zu modifizieren, die Benutzeroberfläche neu zu gestalten, Funktionen hinzuzufügen oder die Anzeige komplett neu zu erfinden. Teilen Sie dann Ihre Kreationen mit uns. Zu sehen, wie Maker einfache Beispiele in etwas einzigartig Eigenes verwandeln, macht diese Community so aufregend.

 

In diesem Artikel erwähnte Produkte

Verwandte Artikel