19.01.2026

RP2350 DVI Projekt Meteorološke stanice: Prenamijenite svoje nekorištene DVI zaslone

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

Table of contents

Uvod
Uvod u RP2350 i njegove DVI mogućnosti
Pregled BME280 senzora Ožičenje i postavljanje Arduino programiranje Početna konfiguracija
Očitavanje podataka senzora
Izgradnja grafičkih elemenata Rezultat Moguća poboljšanja
Table of contents
Uvod
Uvod u RP2350 i njegove DVI mogućnosti
Pregled BME280 senzora Ožičenje i postavljanje Arduino programiranje Početna konfiguracija
Očitavanje podataka senzora
Izgradnja grafičkih elemenata Rezultat Moguća poboljšanja

Uvod

Jeste li ikada završili s neiskorištenim DVI monitorom? Možda stari PC zaslon, mali prijenosni ekran ili čak preostali TV, i pitali se može li se pretvoriti u nešto korisno?

S novim Soldered NULA Max RP2350 mikrokontrolerom i jednostavnim BME280 senzorom okoliša, taj zaslon može postati potpuno funkcionalna DVI vremenska stanica.

Ista tehnologija grafičkog izlaza koja se koristi u potrošačkim gadgetima, kioscima i digitalnom oglašavanju sada je dostupna na mikrokontroleru koji možete napajati putem USB-C priključka. Uparite ga s BME280 i odjednom svaki rezervni DVI zaslon postaje pametno, uvijek uključeno ambijentalno vremensko središte za vaš dom ili radni prostor.

U ovom članku ćete:

  • naučiti kako mikrokontroleri poput RP2350 mogu emitirati pune DVI signale

  • razumjeti što BME280 mjeri i zašto je idealan za kompaktne vremenske stanice

  • vidjeti kako spojiti obje komponente s minimalnim hardverom

  • dobiti kompletnu Arduino skicu za očitavanje senzora i iscrtavanje grafike

  • izraditi vlastitu plug-and-play vremensku kontrolnu ploču za bilo koji HDMI zaslon

Bilo da želite stolnog pratitelja, monitor za radionicu ili info ploču za dnevni boravak, ovaj projekt vam omogućuje da udahnete novi život starim zaslonima s modernim hardverom otvorenog koda.

Uvod u RP2350 i njegove DVI mogućnosti

RP2350 je najnovija evolucija linije mikrokontrolera Raspberry Pi - kompaktan, pristupačan i dizajniran imajući na umu makere. Naša NULA Max pločica koristi sve njegove značajke, kao i dodavanje DVI izlaznog priključka. S njim, kao i s pravim bibliotekama, čip može generirati stvarne, stabilne video signale izravno sa svojih pinova, bez potrebe za dodatnim grafičkim hardverom.

To je omogućeno zahvaljujući nevjerojatnoj PicoDVI biblioteci. Ona koristi pametne PIO (Programmable I/O) i DMA tehnike za pokretanje DVI kompatibilnih DVI zaslona u potpunosti u softveru. 

Za Arduino programere, najbolji dio je taj što je PicoDVI biblioteka u potpunosti prilagođena Arduino ekosustavu. Još bolje, Adafruit održava fork koji spaja PicoDVI s poznatim Adafruit GFX okruženjem. Ako ste ikada koristili Inkplate zaslon s Adafruit GFX-om, osjećat ćete se kao kod kuće crtajući oblike, tekst, ikone ili cijele UI panele.

Sa samo nekoliko žica i RP2350 pločicom, možete emitirati čisti digitalni signal na gotovo svaki DVI zaslon.

Pregled BME280 senzora

Enviromental sensor BME280 breakout-easyC ecosystem

 

BME280 senzor okoliša je atmosferski senzor koji mjeri tri vrijednosti: temperaturu, tlak i vlažnost. Dodatno, može se izračunati nadmorska visina. Jednostavan je za korištenje jer komunicira putem I2C i dizajniran je za besprijekorno Qwiic povezivanje.

 Iznimno je malen, pa se može postaviti bilo gdje. Ovaj senzor mjeri sve što trebate znati o atmosferskim uvjetima, pa je idealan za projekte meteoroloških stanica!

Ožičenje i postavljanje

Ožičenje projekta ne može biti jednostavnije, ovdje su potrebne komponente:

  • Soldered NULA Max RP2350

  • Senzor okoliša BME280 breakout

  • Qwiic kabel

  • DVI kabel

Povežite BME280 putem Qwiic kabela, a Monitor putem DVI kabela na NULA Max.

Instalirajte sljedeće biblioteke:


Vodič o tome kako instalirati definiciju ploče Soldered NULA Max RP2350 dostupan je ovdje.

Arduino programiranje

Sljedeći primjeri proći će kroz to kako se svaka komponenta inicijalizira, kako se očitavaju podaci senzora i kako se iscrtavaju elementi korisničkog sučelja na zaslonu.

Početna konfiguracija

Prvi korak u projektu je definiranje kako RP2350 treba emitirati DVI signale. To se radi pomoću strukture konfiguracije zaslona. Ako koristite Soldered fork PicoDVI-ja, ovaj se korak može preskočiti jer fork već uključuje ispravnu zadanu konfiguraciju.

Nakon definiranja konfiguracije, stvara se objekt prikaza pomoću klase DVIGFX8. Ovo odabire 8-bitni način rada u boji i rezoluciju 320×240 pri 60 Hz, što dobro funkcionira za laganu grafiku na mikrokontrolerima. Konfiguracija mapira pinove RP2350 na DVI TMDS kanale i priprema biblioteku za generiranje valjanih video signala.

// 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

 

Također je potrebno unijeti vaše Wi-Fi vjerodajnice kako bi se vrijeme moglo dohvatiti putem NTP poslužitelja:

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";

 

Unutar setup(), inicijalizira se nekoliko stvari:

  1. Serijska komunikacija (opcionalno, ali korisno za debugging)

  2. BME280 senzor

  3. Wi-Fi veza

  4. NTP sinkronizacija vremena

  5. DVI zaslon

  6. Postavljanje palete boja i jednostavna statusna poruka

  7. Priprema međuspremnika podataka za grafove

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


Očitavanje podataka senzora

U glavnoj petlji podaci se uzorkuju jednom u sekundi. Svako novo očitanje temperature, vlažnosti i tlaka pohranjuje se u namjenski kružni međuspremnik. Ovi međuspremnici održavaju posljednjih 120 podatkovnih točaka za grafove. Ova struktura omogućuje glatko pomicanje grafova po zaslonu i olakšava praćenje trendova tijekom vremena.

// 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);

 

Izgradnja grafičkih elemenata

Svako područje grafa iscrtava se funkcijom drawGraph(). Funkcija:

  • crta obojenu pozadinu

  • dodaje tanki obrub

  • prekriva mrežne linije

  • označava graf

  • prikazuje trenutnu vrijednost, min i max

  • iscrtava liniju podataka

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
}

Postoji i funkcija koja crta tablicu koja sadrži maksimalne i minimalne zabilježene vrijednosti svake vrste podataka, kao i trenutno vrijeme dohvaćeno s NTP poslužitelja:

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

 

Rezultat

Kada flashamo projekt na NULA Max pločicu, dočekuje nas retro dizajn koji podsjeća na teletekst koji ažurira očitanja senzora svake sekunde!

*** Napomena: Ako vidite crvene linije preko cijelog zaslona, idite na Tools->Optimise->Optimise more (-O2)

Moguća poboljšanja

Ovaj je projekt stvoren kako bi pokazao što RP2350 može učiniti sa svojim ugrađenim DVI izlazom, tako da se kod namjerno fokusira na jednostavnost umjesto na vizualnu dotjeranost. Korisničko sučelje je osnovno, ali upravo tu počinje zabava.

Potičemo vas da uzmete ovaj temelj, modificirate ga, redizajnirate korisničko sučelje, dodate značajke ili potpuno ponovno izmislite prikaz. Zatim podijelite svoje kreacije s nama. Gledati kako makeri pretvaraju jednostavne primjere u nešto jedinstveno svoje je ono što ovu zajednicu čini tako uzbudljivom.

 

Proizvodi u ovom članku

Povezani članci