Interfaccia video VGA con FPGA

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.

Figura 1. Connettore VGA

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).

Figura 2. Rete resistiva completa per 3 bit di colore

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.

Tabella 1. Colori ottenibili con 3 bit

Tabella 1. Colori ottenibili con 3 bit

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.

Figura 3. Reti resistive per 6 e 9 bit di colore (è mostrato solo un canale)

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.

Figura 4. Temporizzazione orizzontale (in alto) e verticale (in basso)

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).

Figura 5. Temporizzazioni H e V sovrapposte ed in scala

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.

Tabella 2. Timings relativi alle diverse risoluzioni

Tabella 2. Timings relativi alle diverse risoluzioni

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.

Figura 6. Schema elettrico per la connessione della FPGA

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.

 

 

Una risposta

  1. Maurizio Di Paolo Emilio Maurizio Di Paolo Emilio 25 maggio 2017

Scrivi un commento

ESPertino è la nuova scheda per IoT compatibile ARDUINO.
Scopri come averla GRATIS!