Table of contents
UvodUvod 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

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:
-
Serijska komunikacija (opcionalno, ali korisno za debugging)
-
BME280 senzor
-
Wi-Fi veza
-
NTP sinkronizacija vremena
-
DVI zaslon
-
Postavljanje palete boja i jednostavna statusna poruka
-
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.