Stație meteo DVI cu RP2350: refolosește monitoarele DVI nefolosite

Stație meteo DVI cu NULA Max RP2350 și senzor BME280

Introducere

Ți-a rămas vreodată un monitor DVI nefolosit? Poate un ecran vechi de PC, un ecran portabil mic sau chiar un televizor rămas prin casă, și te-ai întrebat dacă ar putea fi transformat în ceva util?

Cu noul microcontroler Soldered NULA Max RP2350 și un simplu senzor de mediu BME280, ecranul acela poate deveni o stație meteo DVI complet funcțională.

Aceeași tehnologie de ieșire grafică folosită în gadgeturile de consum, în chioșcuri și în panourile digitale de afișaj este acum accesibilă pe un microcontroler pe care îl poți alimenta dintr-un port USB-C. Combină-l cu BME280 și orice ecran DVI de rezervă devine dintr-odată un hub meteo inteligent, mereu pornit, pentru casă sau pentru spațiul de lucru.

În acest articol vei:

  • afla cum pot microcontrolere precum RP2350 să scoată semnale DVI complete
  • înțelege ce măsoară BME280 și de ce e ideal pentru stații meteo compacte
  • vedea cum conectezi ambele componente cu hardware minim
  • primi o schiță Arduino completă pentru citirea senzorilor și randarea graficii
  • construi propriul dashboard meteo plug-and-play pentru orice ecran HDMI

Fie că vrei un companion pentru birou, un monitor pentru atelier sau un panou de informații pentru sufragerie, proiectul acesta îți permite să dai o viață nouă ecranelor vechi, cu hardware modern și open-source.

Introducere în RP2350 și capabilitățile lui DVI

RP2350 este cea mai nouă evoluție din gama de microcontrolere Raspberry Pi: compact, accesibil ca preț și proiectat cu gândul la makeri. Placa noastră NULA Max folosește toate funcțiile lui și adaugă un conector de ieșire DVI. Cu el, și cu bibliotecile potrivite, cipul poate genera semnale video reale și stabile direct din pinii lui, fără hardware grafic suplimentar.

Acest lucru este posibil datorită bibliotecii PicoDVI, care e extraordinară. Folosește tehnici ingenioase de PIO (Programmable I/O) și DMA ca să comande afișaje compatibile DVI complet din software.

Pentru dezvoltatorii Arduino, partea cea mai bună este că biblioteca PicoDVI a fost adaptată complet pentru ecosistemul Arduino. Mai mult, Adafruit întreține un fork care îmbină PicoDVI cu mediul familiar Adafruit GFX. Dacă ai folosit vreodată un afișaj Inkplate cu Adafruit GFX, te vei simți ca acasă desenând forme, text, pictograme sau panouri UI întregi.

Cu doar câteva fire și placa RP2350, poți trimite un semnal digital curat către practic orice ecran DVI.

Prezentarea senzorului BME280

Senzor de mediu BME280 breakout cu conector Qwiic

Senzorul de mediu BME280 este un senzor atmosferic care măsoară trei valori: temperatura, presiunea și umiditatea. În plus, se poate calcula altitudinea. Este simplu de folosit, pentru că comunică prin I2C și este proiectat pentru conectare fără probleme prin Qwiic.

Este foarte mic, deci poate fi amplasat oriunde. Senzorul măsoară tot ce trebuie să știi despre condițiile atmosferice, așa că e ideal pentru proiecte de stații meteorologice!

Conexiuni și configurare

Cablarea proiectului nu putea fi mai simplă. Iată componentele necesare:

Conectează BME280 prin cablul Qwiic, iar monitorul prin cablul DVI, la NULA Max.

Instalează următoarele biblioteci:

Un tutorial despre instalarea definiției de placă Soldered NULA Max RP2350 este disponibil aici.

Programarea în Arduino

Exemplele următoare arată cum se inițializează fiecare componentă, cum se citesc datele de la senzor și cum se desenează elementele UI pe afișaj.

Configurarea inițială

Primul pas din proiect este să definești cum trebuie RP2350 să scoată semnalele DVI. Asta se face cu o structură de configurare a afișajului. Dacă folosești fork-ul Soldered al PicoDVI, poți sări peste acest pas, pentru că fork-ul include deja configurația implicită corectă.

După definirea configurației, se creează un obiect de afișaj cu clasa DVIGFX8. Aceasta selectează un mod de culoare pe 8 biți și o rezoluție de 320×240 la 60 Hz, care merge bine pentru grafică ușoară pe microcontrolere. Configurația mapează pinii RP2350 pe canalele TMDS ale DVI și pregătește biblioteca să genereze semnale video valide.

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

Trebuie de asemenea să introduci datele rețelei Wi-Fi, ca ora să poată fi preluată de la serverul NTP:

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

În setup() se inițializează mai multe lucruri:

  1. Comunicația serială (opțională, dar utilă pentru depanare)
  2. Senzorul BME280
  3. Conexiunea Wi-Fi
  4. Sincronizarea orei prin NTP
  5. Afișajul DVI
  6. Configurarea paletei de culori și un mesaj simplu de stare
  7. Pregătirea bufferelor de date pentru grafice
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));
}

Citirea datelor de la senzor

În bucla principală, datele sunt eșantionate o dată pe secundă. Fiecare citire nouă de temperatură, umiditate și presiune este stocată într-un buffer circular dedicat. Aceste buffere păstrează ultimele 120 de puncte de date pentru grafice. Structura aceasta permite graficelor să defileze lin pe afișaj și face ușoară urmărirea tendințelor în timp.

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

Construirea elementelor grafice

Fiecare zonă de grafic este randată cu funcția drawGraph(). Funcția:

  • desenează un fundal colorat
  • adaugă un chenar subțire
  • suprapune liniile de grilă
  • etichetează graficul
  • afișează valoarea curentă, minimul și maximul
  • trasează linia de date
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
}

Există și o funcție care desenează un tabel cu valorile maxime și minime înregistrate pentru fiecare tip de date, plus ora curentă preluată de la serverul NTP:

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

Rezultatul

Când încărcăm proiectul pe placa NULA Max, ne întâmpină un design retro care seamănă cu Teletextul și care actualizează citirile senzorului în fiecare secundă!

*** Notă: dacă vezi linii roșii pe tot ecranul, mergi la Tools->Optimise->Optimise more (-O2)

Îmbunătățiri posibile

Proiectul a fost creat ca să arate ce poate face RP2350 cu ieșirea DVI de pe placă, așa că în mod intenționat codul pune accent pe simplitate, nu pe finisajul vizual. Interfața este simplă, dar exact de aici începe distracția.

Te încurajăm să pornești de la această bază, să o modifici, să reproiectezi interfața, să adaugi funcții sau să reinventezi complet afișajul. Apoi arată-ne ce ai creat. Să vedem cum transformă makerii exemple simple în ceva ce le aparține cu adevărat este ce face comunitatea asta atât de captivantă.

Articol tradus și publicat pe BazaIoT cu acordul Soldered Electronics. Versiunea originală în limba engleză: RP2350 DVI Weather Station Project: Repurpose your unused DVI displays.

0 comments

Leave a comment

Please note, comments need to be approved before they are published.