Fino alla puntata scorsa avevamo un sistema in grado di generare il segnale VGA e di visualizzare sul monitor dei pattern precaricati nella BRAM dell’FPGA — utile per verificare i sincronismi e i colori, ma sostanzialmente statico. La BRAM veniva inizializzata una volta e poi solo letta, mai scritta durante il funzionamento. Adesso è il momento di fare il passo successivo: rendere il frame buffer scrivibile, capire come si lavora con le coordinate dello schermo e, alla fine, portare fuori la memoria dall’FPGA collegando una SRAM esterna.
Siamo ancora su Xilinx Spartan-6 — la Devboard X309 — e tutto il codice è in VHDL. L’obiettivo di questa fase è consolidare il massimo possibile prima di passare all’implementazione finale con logica discreta su breadboard, dove ogni errore di progetto si traduce in ore di debug con un oscilloscopio.
Dal connettore VGA integrato alla scrittura del frame buffer
La prima modifica rispetto alle puntate precedenti riguarda l’uscita VGA. Finora avevamo usato un circuito esterno su breadboard con resistenze pesate per la conversione digitale-analogica. La Devboard ha però un connettore VGA integrato con il suo schema di resistenze pesate — 5 bit per il rosso, 6 per il verde, 5 per il blu. Dato che il nostro schema colori usa solo 3 bit di colore più un bit di bright, è sufficiente pilotare i due bit più significativi di ogni canale e portare a zero gli altri. Il risultato è visivamente corretto rispetto allo schema finale, e si guadagna la possibilità di lavorare senza la breadboard aggiuntiva.
La modifica al codice VHDL è molto limitata: nel processo del pixel clock, quando si è in blank si portano tutti i segnali a zero; durante la visualizzazione attiva si mappano i bit di RGB color sulle uscite del connettore integrato. Nulla di strutturalmente diverso rispetto a prima, solo una diversa destinazione dei segnali.
Sul frame buffer invece la novità è sostanziale. L’inizializzazione statica della BRAM viene abbandonata in favore di un semplice array — type ram_type is array (0 to 32767) of std_logic_vector(7 downto 0) — che il tool di sintesi mapperà comunque sulla BRAM interna. Il punto non è evitare la BRAM, ma semplificare la gestione della scrittura: lavorare concettualmente con un array rende il codice più leggibile e la logica di accesso più diretta. La risoluzione rimane 320×200 con 4 bit per colore, per un totale di poco meno di 32KB — ogni byte contiene due pixel adiacenti.
Macchine a stati per scrivere la memoria video
Scrivere sequenzialmente tutte le celle di un array in VHDL non si fa con un ciclo for. Il costrutto esiste nel linguaggio, ma descrive hardware parallelo, non una sequenza temporizzata. Il meccanismo corretto per eseguire operazioni in sequenza — un’operazione per colpo di clock — è la macchina a stati.
Per la cancellazione dello schermo la macchina a stati è minimale: uno stato di reset che inizializza l’indirizzo di scrittura, uno stato di clear che scrive il dato corrente e incrementa l’indirizzo ad ogni fronte di clock, e uno stato di run che segnala il completamento. Finché si è nello stato di clear, il frame buffer viene congelato in lettura — una semplificazione accettabile perché la scrittura su BRAM è abbastanza veloce da completarsi in un tempo trascurabile rispetto al periodo di blank verticale.
Per rendere il sistema interattivo senza dover risintetizzare ogni volta, è stato integrato il modulo UART già sviluppato nel corso di elettronica digitale. Tramite un terminale seriale è possibile inviare singoli caratteri che attivano diversi pattern: cancellazione schermo, riempimento con un colore uniforme, righe verticali alternate, righe orizzontali. Questo ha permesso di testare rapidamente le diverse modalità di scrittura durante lo sviluppo.
Il fattore di compressione: perché un quadrato non è un quadrato
Questa è la parte più istruttiva dell’episodio, e anche quella in cui le aspettative iniziali si sono rivelate sbagliate.
Il segnale generato segue i parametri dello standard 640×400. Il monitor lo aggancia, ma lo tratta come 720×400 — una differenza che di per sé non crea problemi. Il vero problema è che i monitor VGA dell’epoca sono 4:3. Un’immagine 720×400 non ha proporzioni 4:3, quindi il monitor applica una compressione orizzontale per adattarla allo schermo fisico: le 720 colonne logiche diventano circa 533 colonne fisiche, mentre le 400 righe rimangono invariate.
La conseguenza pratica: se si disegna nel frame buffer un quadrato di 160×160 pixel — uguale in larghezza e altezza — sullo schermo appare un rettangolo con la base più corta dell’altezza. La compressione orizzontale schiaccia la dimensione X più di quanto la duplicazione delle righe allunghi la dimensione Y.
Per compensare bisogna lavorare con due fattori distinti: il rapporto di scala orizzontale (da 320 a 533 logici) è circa 1,66×, quello verticale (da 200 a 400) è esattamente 2×. Il rapporto tra i due fattori è circa 0,83, che si approssima convenientemente a 4/5 — una moltiplicazione realizzabile in hardware con uno shift e una somma, senza ricorrere a numeri in virgola mobile. Per disegnare un quadrato di lato L sullo schermo, nel frame buffer va disegnato un rettangolo di larghezza L e altezza L×4/5.
Verificato sperimentalmente: con lato 160 e altezza non corretta si ottiene un rettangolo verticale; con l’altezza corretta a 133 pixel (o 128 come approssimazione) il quadrato è visivamente corretto.
Da quadrati a cerchi: l’equazione dell’ellisse in VHDL
Una volta capito il fattore di compressione, disegnare un cerchio diventa un’estensione naturale del problema. Un cerchio sullo schermo è un’ellisse nella memoria video, con semiassi che incorporano il fattore di correzione. L’equazione utilizzata per decidere se un pixel è interno all’ellisse è la forma: x² × b² + y² × a² ≤ a² × b². Quando la disuguaglianza è soddisfatta il pixel viene colorato, altrimenti no.
L’elevamento al quadrato si implementa semplicemente come moltiplicazione del segnale per se stesso — nessuna funzione potenza, nessuna complessità aggiuntiva. La scansione della memoria avviene comunque su tutta la superficie dello schermo; si potrebbe ottimizzare limitandosi al rettangolo che circoscrive l’ellisse, ma per i test è superfluo.
Il risultato è visibile: cerchio perfettamente inscritto nel quadrato, entrambi corretti nelle proporzioni.
RAM esterna: come interfacciare SRAM 62256 con una FPGA a 3,3V
L’ultimo blocco di esperimenti è stato anche il più delicato dal punto di vista hardware. L’obiettivo era sostituire l’array interno dell’FPGA con una SRAM esterna 62256 — la stessa famiglia che verrà usata nel sistema Alex80 finale — pilotata dall’FPGA via GPIO.
Il problema principale è la tensione. Lo Spartan-6 lavora a 3,3V e i suoi GPIO non sono tolleranti ai 5V. La RAM 62256 è un componente a 5V. Per i segnali unidirezionali dall’FPGA verso la RAM — indirizzi, write enable, chip select, output enable — non c’è problema: la RAM riconosce come livello alto qualunque tensione superiore a 2,2V, quindi i 3,3V dell’FPGA sono sufficienti.
Il problema è il data bus, che deve essere bidirezionale. La soluzione semplice — un level shifter bidirezionale automatico — si è rivelata inutilizzabile: i componenti di questo tipo disponibili sul banco erano troppo lenti per lavorare a 12,5MHz senza artefatti visibili. Il 74LVX245, che sarebbe il componente ideale (alimentazione 3,3V, ingressi tolleranti 5V), è difficile da reperire.
La soluzione adottata per il test è pragmatica: dividere fisicamente il data bus in due percorsi separati — uno di sola scrittura dall’FPGA alla RAM, uno di sola lettura dalla RAM all’FPGA. In scrittura i GPIO dell’FPGA pilotano la RAM attraverso un 74HC244 abilitato dal write enable, che fornisce un’uscita pulita a 3,3V. In lettura, i 5V uscenti dalla RAM vengono abbassati a circa 3,2V tramite un semplice partitore resistivo (560Ω e 1kΩ verso massa), sufficiente a non danneggiare i pin dell’FPGA e correttamente riconosciuto come livello logico alto.
Non è una soluzione da produzione — lo spreco di GPIO e la gestione separata dei bus non sarebbero accettabili in un sistema definitivo — ma per validare il comportamento del controller durante i test funziona perfettamente.
Cosa si impara da questi test prima di passare all’hardware
Il confronto tra la versione con array interno e quella con RAM esterna ha confermato alcune ipotesi importanti per il progetto finale.
Il tempo di accesso della 62256 a 70ns è adeguato al pixel clock utilizzato. L’immagine è stabile, senza pixel ballerini, il che valida la scelta della RAM per il sistema definitivo. Sull’FPGA il clock aiuta la stabilità in modo significativo — su breadboard a 12,5MHz le cose potrebbero comportarsi diversamente, e questo sarà il primo nodo da sciogliere.
La registrazione del dato letto dalla RAM — equivalente a otto flip-flop D sincronizzati sul pixel clock — si è rivelata necessaria per eliminare i glitch dovuti ai transitori degli indirizzi. Questo dettaglio dovrà essere replicato nell’implementazione con logica discreta.
La scrittura da parte dello Z80 in un tempo di blank verticale di circa 1ms non consentirà di aggiornare l’intero schermo in un singolo frame. È un vincolo reale che influenzerà le scelte di progetto sull’interfaccia grafica del sistema Alex80. Una possibile ottimizzazione — usare più chip di RAM in parallelo per scrivere su più celle contemporaneamente con un unico ciclo — è una possibilità, anche se probabilmente non verrà implementata nella prima versione.
La prossima puntata porterà tutto questo fuori dall’FPGA: schema elettrico con contatori, sommatori e circuiti programmabili semplici come i 16V8 e i 22V10, e prima accensione su breadboard con lo Z80 che scrive direttamente nella memoria video.
Se siete arrivati a questo punto della serie, il video completo è la giusta prosecuzione del percorso: si vedono in tempo reale i test sul monitor, le iterazioni sul codice VHDL e i momenti in cui la teoria incontra il comportamento reale dell’hardware. Vi aspetto anche nella prossima puntata, dove — finalmente — togliamo l’FPGA dal percorso.
Slide usate nel video
Questo contenuto è riservato agli abbonati premium di Alexa Academy.
