Frame Buffer su FPGA: implementazione completa per VGA e Zilog Z80 – ALEX80 #31

Con questo episodio il progetto ALEX80 fa un salto significativo: dalla generazione di semplici barre colorate alla gestione di una vera memoria video. L’obiettivo è avere un frame buffer funzionante su FPGA che riproduca fedelmente il comportamento che avrà il sistema definitivo con la RAM esterna da 32 KB.

La memoria video: quanti bit per pixel?

La risoluzione scelta per ALEX80 è 320×200 pixel, ottenuta dimezzando i pixel orizzontali e duplicando le linee verticali rispetto a una VGA standard. Il risultato sono pixel rettangolari in senso verticale, una caratteristica comune ai computer 8-bit dell’epoca.

320×200 fa esattamente 64.000 pixel. Per stare dentro 32 KB di memoria video — la soglia che era già stata definita nel progetto — bastano 4 bit per pixel: 64.000 × 4 / 8 = 32.000 byte. Un adattamento preciso, senza margine di spreco.

Con 4 bit si ottengono 16 colori. Lo schema adottato si ispira allo ZX Spectrum: 3 bit per il colore (le otto combinazioni RGB che avevamo già visto) più un bit di luminosità (bright). A differenza dello Spectrum, dove il nero con bright attivo restava comunque nero (15 colori effettivi), qui il nero brillante diventa un grigio chiaro, quindi i 16 valori distinti sono tutti disponibili.

Il bit blu è in posizione meno significativa, il bit bright in quella più significativa. All’interno di ogni byte si trovano due pixel, ciascuno da un nibble.

Il DAC a resistenze pesate

Per convertire i 4 bit in segnali analogici RGB è necessario un DAC. La soluzione scelta è quella a resistenze pesate: per ogni canale colore si hanno due bit da convertire — il bit di colore specifico e il bit bright condiviso — con resistenze in rapporto R e 2R verso il carico di 75 ohm del monitor.

Con una tensione logica alta di 3,3 V (uscita FPGA) e un’uscita massima desiderata di 700 mV, i calcoli portano a un valore di R di circa 418 ohm. Approssimando per eccesso per non superare i 700 mV si arriva a 422 ohm (serie E96 all’1%), con una tensione massima risultante di circa 0,69 V. Il valore 2R si ottiene semplicemente mettendo in serie due resistenze uguali, il che semplifica l’approvvigionamento dei componenti.

Per il futuro sistema con ALEX80 a 5 V il valore sale a circa 698 ohm, già calcolato e pronto all’uso.

Sul connettore dell’FPGA si aggiunge un quarto pin rispetto ai tre RGB: il segnale bright, comune a tutti i canali.

La BRAM dello Spartan-6

Per il test su FPGA si utilizza la RAM interna del chip. La RAM distribuita (basata su LUT) è troppo limitata: solo 138 kbit sullo XC6SLX16, contro i 32.000 byte necessari. La Block RAM (BRAM) è invece la scelta corretta.

Lo Spartan-6 SLX16 ha 32 blocchi BRAM da 18 kbit ciascuno (con parità), per un totale di 64 KB utili. Il frame buffer ne usa 16, nella configurazione 2K×8 bit (un blocco = 2 KB). La parità non viene utilizzata, ma la configurazione in ISE richiede di indicare comunque larghezza 9 (e non 8) per evitare errori in fase di sintesi: un dettaglio non documentato in modo ovvio, scoperto empiricamente.

Le BRAM sono a doppia porta e sincrone. Per il test si usa solo la porta A in lettura; l’inizializzazione avviene una tantum tramite il bitstream, scrivendo direttamente i valori di pattern nel generic INIT_xx della primitiva RAMB16BWER. I 64 vettori di inizializzazione da 256 bit ciascuno coprono i 2 KB di ogni blocco. Attenzione all’ordinamento: i byte nei vettori di inizializzazione sono da destra verso sinistra, quindi il primo pixel in alto a sinistra corrisponde alla coppia di cifre hex più a destra della prima riga.

L’indirizzamento non allineato

Qui sta la complessità principale. L’indirizzo in memoria di un pixel dipende da H count e V count, ma 320 non è una potenza del due, quindi non si può semplicemente concatenare i bit.

Una riga occupa 160 byte (320 pixel / 2 pixel per byte). L’indirizzo base in memoria è:

addr = Vcount * 160 + Hcount

dove sia V count che H count vengono usati senza il bit meno significativo: V count perché le righe sono duplicate (il bit 0 discriminerebbe linee identiche), H count perché ogni byte contiene due pixel (il bit 0 discrimina i due nibble, ma non cambia il byte da leggere).

160 si scompone in 128 + 32, ovvero 2⁷ + 2⁵. La moltiplicazione diventa quindi uno shift left di 7 e uno shift left di 5, operazioni che in logica digitale corrispondono semplicemente a ricablare i bit nelle posizioni corrette. Il risultato è un sommatore a tre ingressi da 15 bit, senza moltiplicatori.

In VHDL il calcolo usa resize e shift_right per compatibilità con VHDL93, ma il sintetizzatore ricava correttamente le connessioni fisiche senza logica aggiuntiva.

Per la selezione del blocco BRAM: i 4 bit più significativi dell’indirizzo a 15 bit (bit 14..11) selezionano uno dei 16 blocchi da 2 KB. I restanti 11 bit meno significativi, traslati di 3 posizioni (come impone la tabella di configurazione della BRAM con larghezza parola 8 bit), formano l’indirizzo interno al blocco su 14 bit.

La latenza della BRAM e la risincronizzazione

La BRAM è sincrona: l’indirizzo viene presentato sul fronte di salita del clock e il dato è disponibile solo al colpo di clock successivo. Questo introduce uno sfasamento di un pixel clock rispetto alla logica combinatoria di H count e V count.

La soluzione è semplice e pulita: tutti i segnali che dipendono dal timing di visualizzazione — blank, hsync, vsync — vengono ritardati di un flip flop D prima di essere usati. Si ritarda anche il bit meno significativo di H count, necessario per scegliere quale nibble del byte leggere (pixel pari o dispari).

In questo modo la memoria risponde con un ciclo di ritardo, e i segnali di controllo vengono allineati allo stesso ritardo. Il tutto rimane coerente.

Un bug scoperto durante il test live: la discriminazione tra nibble alto e nibble basso era invertita. Quando il bit meno significativo ritardato di H count vale 0 si prende la parte più significativa del byte (pixel pari), quando vale 1 quella meno significativa (pixel dispari). Invertita la condizione, il disegno è risultato corretto.

Il test con il frame buffer editor

Per validare geometria e proporzioni è stato usato un editor web che genera direttamente il VHDL di inizializzazione del frame buffer. L’editor tiene conto del raddoppio dei pixel verticali: disegnando un cerchio si ottiene un cerchio sullo schermo, non un’ellisse. Il VHDL generato viene copiato nel progetto ISE, sintetizzato e programmato via Impact.

Il risultato sul monitor CRT è un’immagine con cerchi, rettangoli e linee nelle proporzioni corrette. I pixel spurii visibili alle transizioni tra blocchi BRAM — ogni 3840 pixel — sono accettabili in questa fase: dipendono dal mux combinatorio tra blocchi e non si presenteranno nella RAM definitiva asincrona.

Perché questo lavoro servirà anche dopo

La RAM finale del sistema ALEX80 è una 62256 asincrona con tempo di risposta massimo di 70 ns. Il pixel clock è di 80 ns. Presentare l’indirizzo e leggere il dato nello stesso ciclo sarebbe possibile ma instabile, con possibile sfarfallio a fine ciclo.

La soluzione è la stessa adottata con la BRAM: presentare l’indirizzo un ciclo prima, catturare il dato al ciclo successivo con un flip flop, usarlo il ciclo dopo ancora. In questo modo la RAM ha l’intero periodo del pixel clock per rispondere, e il dato letto è sempre stabile. L’hardware sviluppato su FPGA è quindi direttamente trasferibile al progetto definitivo, con gli stessi accorgimenti di temporizzazione.

Il prossimo passo è da decidere: implementare lettura e scrittura della 62256 su FPGA con convertitori di livello, interfacciare qualcosa che scriva nella RAM (ALEX80, Arduino o un sistema intermedio), oppure portare questa logica già consolidata direttamente su logica discreta con GAL o CPLD. Il video successivo chiarirà la direzione.

Se stai seguendo lo sviluppo di ALEX80 o ti interessa la progettazione di sistemi video su FPGA, questo è il momento giusto per guardare il video: il codice VHDL è commentato in modo da seguire passo passo ogni scelta implementativa.

Avatar Paolo Godino