In questo articolo verrà descritta la realizzazione di una interfaccia video VGA implementabile tramite dispositivi logici programmabili. La maggior parte dei dispositivi video collegabili a personal computer (videoproiettori) sono dotati di una interfaccia standard VGA/VESA, oltre ad altre interfacce quale la HDMI. Verranno descritte le caratteristiche elettriche dell’interfaccia, le temporizzazioni, la generazione delle immagini ed il codice VHDL che implementa il controllore.
INTERFACCIA VGA: CARATTERISTICHE ELETTRICHE
L'interfaccia VGA si presenta come un connettore sub-D ad alta densità a 15 pin (Figura 1), in cui solo 5 sono utilizzati per comunicare il segnale video vero e proprio.
In particolare i pin 1, 2 e 3 sono associati alle componenti di colore (rosso, verde e blu, in sigla R, G e B), mentre i pin 13 e 14 ai segnali di sincronismo orizzontale e verticale. I dati relativi all’immagine sono inviati con una scansione orizzontale (da sinistra a destra) e verticale (dall’alto al basso) come avviene nei sistemi televisivi. Dopo la fine di ogni linea segue un impulso di sincronismo orizzontale, mentre dopo la fine di ogni quadro segue un impulso di sincronismo verticale (i dettagli sulle temporizzazioni verranno forniti in seguito). I colori sono trasmessi come segnali analogici con tensioni comprese tra 0V e 0.7V, in cui il valore più alto corrispondente al massimo di luminosità. I segnali di sincronismo invece sono dei segnali digitali a livelli TTL positivi (cioè impulsi attivi alti). In qualche caso i due segnali di sincronismo possono essere accorpati in un unico segnale “composito”, che è ottenuto semplicemente dall’XOR dei due. A volte questo segnale è ulteriormente sovrapposto al segnale del verde. Dal momento che le frequenze e le bande tipiche dei segnali sono piuttosto grandi, i collegamenti prevedono un adattamento d’impedenza a 75o, e sono quindi terminati ad entrambe le estremità con un’impedenza di questo valore. Come già detto se si vuole ottenere la massima luminosità di uno dei segnali di colore occorre fornire circa 0.7V. Per fare questo utilizzando una normale uscita digitale (TTL o LVTTL) è possibile sfruttare proprio la terminazione di 75o per ottenere un effetto di partizione, o eventualmente per costruire un semplice DAC realizzato tramite un ladder resistivo (Figura 2).
Se associamo un solo livello a ciascuno dei canali di colore (acceso o spento), possiamo ottenere un massimo di 8 colori (3 bit di colore). In particolare le combinazioni ottenibili sono quelle visibili in Tabella 1.
Se si vuole ottenere un numero maggiori di colori occorre generare più di un livello per ciascun canale. Questo può essere fatto con le reti resistive mostrate in Figura 3.
In questo caso possono essere ottenuti rispettivamente 64 o 512 colori (6 o 9 bit di colore). Ovviamente in questi ultimi casi sarà necessario utilizzare più uscite digitali per pilotare ciascun canale. Se si vuole ottenere una risoluzione di colore ancora maggiore, come 16 o 24 bit, è conveniente usare dei DAC integrati e da 5 a 8 uscite digitali per canale. Negli esempi seguenti comunque verrà considerato per semplicità il caso di 3 bit di colore. La banda caratteristica di questi segnali può essere calcolata approssimativamente moltiplicando il numero di pixel orizzontali per quelli verticali, per il numero di quadri al secondo e per il tempo aggiuntivo impiegato per il “retrace”, che conta per un fattore compreso tra 1.2 e 1.4. Ad esempio, una tipica risoluzione VGA 640x480 a 60Hz richiede:
640 x 480 x 60 x 1.3 ≈ 25MHz
Questa frequenza è anche il cosiddetto “pixel clock”, cioè la velocità con cui i dati di colore relativi ad ogni pixel devono essere presentati alle uscite. Si intuisce da questa considerazione il perché la generazione di un segnale VGA richieda hardware dedicato, o comunque molto veloce come FPGA o CPLD.
TEMPORIZZAZIONI
Le temporizzazioni orizzontali e verticali di un segnale VGA sono schematizzate in Figura 4.
Si può notare come la temporizzazione orizzontale (H) e quella verticale (V) siano abbastanza simili, anche se su scale temporali diverse (il segnale verticale è molto più lento). In entrambi i casi è presente una “zona attiva”, in cui sono presentati alle uscite i valori di colore associati a ciascun pixel, o le varie linee. Terminata la zona attiva si ha un intervallo chiamato “piedistallo anteriore” (“Front Porch”), poi l’impulso di sincronismo, ed un “piedistallo posteriore” (“Back Porch”). Dall’inizio del piedistallo anteriore e fino al nuovo inizio dell’area attiva i segnali di colore devono essere spenti (portati a 0V). Questo è necessario perché in questo tempo, chiamato intervallo di “Blank orizzontale”, il pennello elettronico (nei monitor CRT) deve spostarsi dal bordo destro a quello sinistro per ricominciare la scansione della linea successiva. Molti dispositivi video, anche non basati su CRT, non funzionano correttamente se non si spengono i segnali di colore durante i blank. Per le temporizzazioni verticali si ha una situazione simile, solo che in questo caso si contano le linee tracciate e non i pixel. Dopo un certo numero di linee (dipendente dalla risoluzione utilizzata) si avrà un intervallo di blank verticale, che comprenderà i due piedistalli e l’impulso di sincronismo. Dal momento che i tempi verticali si contano in linee e non in pixel, risultano molto più lenti di quelli orizzontali. La Figura 5 fornisce una visione d’insieme, in scala, dei tempi e della successione degli eventi (se si tiene conto di come avviene la scansione).
Per ottenere diverse risoluzioni è sufficiente scegliere la velocità con cui si forniscono i dati colore relativi ai pixel (il pixel clock), e la dimensione dei vari intervalli. In Tabella 2 sono riassunti i valori relativi alle risoluzioni più comuni.
I dispositivi video sono in grado riconoscere i tempi utilizzati e di agganciarsi a questi. Molti monitor CRT consentono anche una certa flessibilità, e sono in grado di agganciarsi anche se i tempi dei segnali forniti in ingresso non risultano particolarmente precisi. Altri dispositivi più recenti (ad esempio i videoproiettori), anche per motivi tecnologici, richiedono una precisione maggiore, e potrebbero non funzionare correttamente se non si rispettano perfettamente i tempi indicati o supportati.
GENERAZIONE DEI SINCRONISMI
Come già visto è necessario che i segnali di colore e sincronismo siano forniti con le opportune temporizzazioni. In questo paragrafo sarà analizzata la generazione dei sincronismi, che tra l’altro risulta indipendente dal contenuto dell’immagine. Dal momento che si tratta di segnali periodici con tempi caratteristici multipli del pixel clock, il modo più semplice per generarli consiste nel partire proprio dal pixel clock ed utilizzare dei contatori. In base al valore raggiunto da questi sarà possibile distinguere quale parte del segnale generare. Si avrà quindi in sequenza la zona attiva in cui verranno aggiornati i segnali di colore, il front porch da cui inizierà l’intervallo di blank, l’impulso di sincronismo ed il back porch. Alla fine del back porch il contatore verrà resettato ed il ciclo inizierà daccapo. Per i segnali verticali si utilizza la stessa tecnica, ma il contatore non sarà incrementato dal pixel clock, ma da ogni fine di linea orizzontale. Il codice VHDL che implementa la generazione dei segnali di sincronismo è riportato nel Listato 1.
— Formato: VGA a 640x480 - 60Hz – 25MHz library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_ARITH.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; entity VGA_Synch is Port (HS : out std_logic; VS : out std_logic; BLANK : out std_logic; CLK : in std_logic; RESET : in std_logic); end VGA_Synch; architecture RTL of VGA_Synch is — * Costanti * constant RESET_ACTIVE : std_logic := ‘1’; — Valore reset attivo — * 640x480, 25MHz * constant H_TOTAL : std_logic_vector(11 downto 0) := x”320”; constant H_ACTIVE : std_logic_vector(11 downto 0) := x”280”; constant H_FRONT_PORCH : std_logic_vector(11 downto 0) := x”010”; constant H_BACK_PORCH : std_logic_vector(11 downto 0) := x”030”; constant V_TOTAL : std_logic_vector(11 downto 0) := x”20C”; constant V_ACTIVE : std_logic_vector(11 downto 0) := x”1E0”; constant V_FRONT_PORCH : std_logic_vector(11 downto 0) := x”00B”; constant V_BACK_PORCH : std_logic_vector(11 downto 0) := x”01F”; — * Segnali * signal HCNT : std_logic_vector(11 downto 0); signal VCNT : std_logic_vector(11 downto 0); signal HBLANK : std_logic; signal VBLANK : std_logic; signal CBLANK : std_logic; begin — *** Sincronismo orizzontale *** process(CLK, RESET) begin if RESET = RESET_ACTIVE then HCNT <= (others => ‘0’); HS <= ‘0’; HBLANK <= ‘0’; elsif(CLK’event and CLK=’1’) then — Contatore orizzontale if HCNT=(H_TOTAL-1) then HCNT <= (others => ‘0’); HBLANK <= ‘0’; else HCNT <= HCNT + 1; end if; — Generazione impulso di sincronismo if HCNT=(H_ACTIVE+H_FRONT_PORCH-1) then HS <= ‘1’; elsif HCNT=(H_TOTAL-H_BACK_PORCH-1) then HS <= ‘0’; end if; — Blank orizzontale if HCNT=(H_ACTIVE-1) then HBLANK <= ‘1’; end if; end if; end process; — *** Sincronismo verticale *** process(CLK, RESET) begin if RESET = RESET_ACTIVE then VCNT <= (others => ‘0’); VS <= ‘0’; VBLANK <= ‘0’; elsif(CLK’event and CLK=’1’ and HCNT=(H_TOTAL-1)) then — Contatore orizzontale if VCNT=(V_TOTAL-1) then VCNT <= (others => ‘0’); VBLANK <= ‘0’; else VCNT <= VCNT + 1; end if; — Generazione impulso di sincronismo if VCNT=(V_ACTIVE+V_FRONT_PORCH-1) then VS <= ‘1’; elsif VCNT=(V_TOTAL-V_BACK_PORCH-1) then VS <= ‘0’; end if; — Blank verticale if VCNT=(V_ACTIVE-1) then VBLANK <= ‘1’; end if; end if; end process; CBLANK <= HBLANK or VBLANK; BLANK <= CBLANK; end RTL;
Listato 1 |
Il codice implementa i due contatori (HCNT e VCNT), gestiti in due processi separati, che sono utilizzati per generare i segnali di sincronismo e di blank. Questi segnali costituiscono le uscite del modulo VHDL, che possono essere utilizzati per coordinare la generazione dei segnali di colore da parte di un modulo esterno. Per cambiare la risoluzione è necessario modificare le specifiche sulle durate degli intervalli, che sono indicate nelle costanti (i valori sono gli stessi indicati in Tabella 2, in esadecimale), e fornire il pixel clock corretto.
GENERAZIONE DI UNA IMMAGINE
È possibile generare l’immagine (cioè la successione di valori da fornire come segnali colore) in diversi modi. Sono comuni tre metodi: a partire dalle coorinate attuali (X,Y), utile per generare pattern o semplici disegni o animazioni; a partire da un indirizzo lineare, adatto a visualizzare bitmap memorizzate in una memoria; a partire da riga, colonna e scanline, adatto a visualizzare testo o grafica a tasselli (“tile”). Ciascuno di questi metodi può essere implementato facilmente a partire dai segnali forniti dal modulo di sincronismo. Ci si concentrerà qui sul metodo più comune, semplice e versatile, cioè quello basato su indirizzi lineari. È sufficiente in questo caso utilizzare un contatore che si incrementa ad ogni ciclo del pixel clock quando il segnale di blank non è attivo. Il valore del contatore può essere utilizzato per indirizzare una memoria RAM statica che contiene proprio il valore dei segnali colore per ciascun pixel. La quantità di memoria richiesta (in bit) può essere calcolata moltiplicando il numero di pixel dell’immagine per il numero di bit necessari per rappresentare i colori. In effetti storicamente il principale ostacolo a risoluzioni alte è stata proprio la mancanza o l’alto costo della memoria. Per ovviare a questo limite fino ad una decina di anni fa (soprattutto nelle macchine da gioco) si ricorreva ad un semplice trucco: si alternavano linee dell’immagine con linee vuote (nere), questo permetteva di dimezzare la quantità di memoria richiesta. Riguardo alla memoria bisogna considerare anche un altro problema: la velocità di accesso. Ad una risoluzione di 800x600 occorre leggere i dati con una frequenza di 50MHz, e se i dati richiedono più byte occorre eseguire più letture. Occorrono quindi memorie capaci di tempi di accesso molto piccoli (inferiori a 10ns) o con sufficiente larghezza di parola (16 o 32 bit). La soluzione qui utilizzata si basa sull’impiego di una comune memoria SRAM da 4Mbit organizzata in 256K word da 16 bit (es. Hitachi HM6216255 o equivalenti). L’immagine sarà memorizzata nella SRAM come successione ordinata di pixel. Ciascuna word codifica 4 pixel disposti come segue: -BGR -BGR -BGR -BGR, in cui il bit meno significativo si riferisce al primo pixel (nell’ordine di scansione). Saranno necessarie: 640 x 480 x 4 / 16 = 76800 word. Il codice VHDL che richiama il modulo di generazione dei sincronismi e che genera i segnali per accedere alla memoria è riportato nel Listato 2.
library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_ARITH.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; entity VideoGen is Port (DATA : in std_logic_vector(15 downto 0); ADDR : out std_logic_vector(17 downto 0); CE : out std_logic; OE : out std_logic; WE : out std_logic; LB : out std_logic; UB : out std_logic; R : out std_logic; G : out std_logic; B : out std_logic; HS : out std_logic; VS : out std_logic; CLK : in std_logic; RESET : in std_logic); end VideoGen; architecture Behavioral of VideoGen is signal BLANK : std_logic; signal RED : std_logic; signal GREEN : std_logic; signal BLUE : std_logic; signal HSI : std_logic; signal VSI : std_logic; signal PADDR : std_logic_vector(19 downto 0); signal MDATA : std_logic_vector(15 downto 0); signal CLKDIV : std_logic; signal MCE : std_logic; component VGA_Synch Port (HS : out std_logic; VS : out std_logic; BLANK : out std_logic; CLK : in std_logic; RESET : in std_logic); end component ; begin process(CLK, RESET) begin if RESET=’1’ then CLKDIV <= ‘0’; MDATA <= (others => ‘0’); PADDR <= (others => ‘0’); MCE <= ‘1’; elsif(CLK’event and CLK=’1’) then CLKDIV <= not CLKDIV; if (BLANK=’0’ and CLKDIV=’1’) then PADDR <= PADDR + 1; end if; if VSI=’1’ then PADDR <= (others => ‘0’); end if; MCE <= ‘1’; if PADDR(1 downto 0)=”00” then MCE <= ‘0’; MDATA <= DATA; end if; end if; end process; LB <= ‘0’; UB <= ‘0’; OE <= ‘0’; WE <= ‘1’; CE <= MCE; ADDR <= PADDR(19 downto 2); RED <= MDATA(0) when PADDR(1 downto 0)=”00” else MDATA(4) when PADDR(1 downto 0)=”01” else MDATA(8) when PADDR(1 downto 0)=”10” else MDATA(12); GREEN <= MDATA(1) when PADDR(1 downto 0)=”00” else MDATA(5) when PADDR(1 downto 0)=”01” else MDATA(9) when PADDR(1 downto 0)=”10” else MDATA(13); BLUE <= MDATA(2) when PADDR(1 downto 0)=”00” else MDATA(6) when PADDR(1 downto 0)=”01” else MDATA(10) when PADDR(1 downto 0)=”10” else MDATA(14); R <= RED and (not BLANK); G <= GREEN and (not BLANK); B <= BLUE and (not BLANK); HS <= HSI; VS <= VSI; SYNCH1: VGA_Synch Port Map (HS => HSI, VS => VSI, BLANK => BLANK, CLK => CLKDIV, RESET => RESET); end Behavioral;
Listato 2 |
Il codice non fa altro che generare gli indirizzi utilizzando un contatore incrementato dal pixel clock e bloccato dal segnale di blank (si è supposto di utilizzare un clock in ingresso di 50MHz, quindi è stata prevista una divisione per 2 della frequenza). Gli indirizzi forniti alla memoria devono essere divisi per 4, in modo da tenere in conto il fatto che vengono letti 16 bit alla volta. I dati letti vengono quindi forniti sequenzialmente in uscita (multiplexati usando i due bit meno significativi del contatore). Si può notare anche che i segnali di colore sono azzerati in corrispondenza dell’intervallo di blank, mentre il contatore degli indirizzi è azzerato in corrispondenza dell’impulso di sincronismo verticale.
SCHEMA ELETTRICO
Lo schema dei collegamenti relativi all’interfaccia video è mostrato in Figura 6.
Lo schema non è dettagliato perché il codice e le tecniche proposte possono essere implementate su una notevole quantità di dispositivi (FPGA, CPLD, PLD, logica discreta, etc.). Il codice VHDL presentato è stato testato su una FPGA Xilinx Spartan3, e la sua implementazione ha richiesto circa 50 slices. È stato utilizzato un clock di 25MHz per testare la risoluzione 640x480 a 60Hz ed uno a 50MHz per la 800x600 pixel a 72Hz. Nonostante la prima frequenza sia un po’ inferiore a quella prevista, tutti i dispositivi video provati sono stati in grado visualizzare correttamente il segnale, in entrambe le risoluzioni. Va notato che il codice presentato non permette di scrivere i dati nella memoria, perché questo aspetto dipende strettamente dall’applicazione per cui si sta sviluppando l’interfaccia. In generale si possono utilizzare due tecniche: o si utilizza una memoria a doppia porta, di cui una utilizzata dal controller (es. FPGA) per la lettura, e l’altra lasciata libera per la scrittura da parte di un dispositivo esterno qualsiasi (ad esempio un microcontrollore); oppure si può utilizzare una memoria a singola porta, e si implementa un arbitro nel controller, in modo da potere gestire la lettura e la scrittura in modo che queste operazioni non interferiscano. Il primo metodo è molto semplice e consente un’elevata velocità di aggiornamento dei dati, il secondo consente la scrittura praticamente soltanto durante gli intervalli di blank, ma nonostante questo in pratica è il più utilizzato grazie al suo basso costo.
A proposito di FPGA vi segnalo questo http://it.emcelettronica.com/lo-sviluppo-di-fpga-con-cortex-m1 C’è qualcuno nella community che usa FPGA ?