VHDL per FPGA: introduzione completa con esempi pratici

La prima volta che si incontra un linguaggio di descrizione dell’hardware come il VHDL si può essere sorpresi. Sembra codice, usa parole chiave e costrutti che ricordano Ada o Pascal, ha variabili e strutture condizionali, eppure non è un linguaggio di programmazione. Descrive hardware. E l’hardware, per sua natura, funziona in parallelo.

In questo video affrontiamo il VHDL in modo organico: non un elenco di costrutti, ma una progressione che parte dai concetti fondamentali e arriva a un risultato concreto, un LED che lampeggia sulla nostra scheda AX309 con Spartan-6.

Perché il VHDL

Finché si lavora con porte logiche e schematici su dispositivi semplici come le GAL o l’Altera MAX II, lo schematico può essere una buona scelta. Appena si scala verso un FPGA propriamente detto, come lo Spartan-6 che usiamo sulla AX309, lo schematico diventa rapidamente ingestibile. Il VHDL (VHSIC Hardware Description Language, dove VHSIC sta per Very High Speed Integrated Circuit) nasce proprio per risolvere questo problema, permettendo di descrivere circuiti complessi a diversi livelli di astrazione.

Esistono due HDL di riferimento: Verilog, che per certi versi ricorda il C, e VHDL, più formale e derivato da Ada. Noi partiamo dal VHDL, nella revisione del 1993, che è la più compatibile con ISE, il tool di sintesi Xilinx che usiamo per programmare lo Spartan-6.

Una caratteristica importante: il VHDL è uno standard IEEE. Questo significa che lo stesso codice scritto in modo aderente allo standard funziona con Quartus, con ISE, con qualunque tool che dichiari compatibilità con quella revisione.

Hardware vs. software: il cambio di mentalità

Chi arriva dalla programmazione porta con sé un’abitudine profonda: le istruzioni si eseguono in sequenza. Prima una, poi l’altra. Il VHDL rompe questa logica.

Un 7400 ha quattro porte NAND. Se variano gli ingressi, tutte e quattro le uscite variano contemporaneamente. Non c’è una schedulazione, non c’è un ordine. L’hardware è intrinsecamente parallelo, e il VHDL riflette questa realtà. Tutto ciò che scriviamo nell’architecture viene eseguito in parallelo, indipendentemente dall’ordine in cui lo scriviamo.

Questo non significa che non esista il concetto di sequenzialità: esiste, ma è temporizzata dal clock, ed è qualcosa che dobbiamo implementare esplicitamente.

Livelli di astrazione

Il VHDL permette di descrivere un circuito a diversi livelli:

  • Comportamentale (behavioral): descrive cosa deve fare il circuito, non come è fatto. Adatto ad algoritmi di alto livello, macchine a stati, test bench.
  • Dataflow: descrive il flusso dei dati, equazioni booleane, multiplexer, decoder.
  • RTL (Register Transfer Logic): vicino all’hardware, descrive registri, clock e trasferimenti di dati. Sempre sintetizzabile.
  • Strutturale: descrive come sono collegati i moduli tra loro, analoga a uno schematico gerarchico.

In pratica questi stili si mescolano, e non c’è nulla di formale che imponga di usarne uno solo.

Struttura di un file VHDL

Un file VHDL minimale è composto da tre sezioni.

Dichiarazione delle librerie: equivale agli #include del C. La libreria più usata è IEEE, con i package std_logic_1164 (che estende il tipo booleano base a nove valori, tra cui alta impedenza e stato sconosciuto) e numeric_std (che aggiunge tipi e operazioni per interi con e senza segno).

Entity: descrive l’interfaccia del componente, ovvero i pin di ingresso e uscita. È l’equivalente del pinout su un datasheet. Le porte hanno una direzione (in, out, buffer, inout) e un tipo. L’entity può anche contenere parametri generici (generic), che permettono di creare componenti configurabili, come un contatore il cui numero di bit viene specificato da chi lo istanzia.

Architecture: descrive il comportamento interno del componente. È formata da una zona dichiarativa (segnali, costanti, tipi) e da un insieme di istruzioni concorrenti. Il nome dell’architecture di solito riflette lo stile di modellazione usato: behavioral, rtl, structural.

Segnali e variabili

Un segnale corrisponde a un collegamento fisico, un filo. Si dichiara nella zona dichiarativa dell’architecture e si assegna con l’operatore <=. Nella simulazione, il suo valore viene aggiornato quando il processo che lo contiene viene sospeso, non nell’istante in cui viene scritto. Questo meccanismo si chiama delta cycle ed è uno dei punti che genera più confusione a chi inizia. Nel video cerchiamo di capire bene questa differenza.

Una variabile vive solo all’interno di un processo, si assegna con :=, e viene aggiornata immediatamente, anche nella simulazione. Se si legge una variabile prima di scriverla, il sintetizzatore introduce un elemento di memoria, tipicamente un flip-flop.

Il processo

Il processo è il costrutto che permette di scrivere logica sequenziale all’interno di un’architecture altrimenti tutta parallela. Al suo interno, le istruzioni vengono eseguite in sequenza nella simulazione, ma il risultato finale corrisponde comunque a logica hardware parallela.

Un processo ha una sensitivity list: l’elenco dei segnali a cui è sensibile. Quando uno di questi segnali varia, il processo viene eseguito. Nella simulazione questo meccanismo è fondamentale; nella sintesi, il sintetizzatore lo ignora ededuce la logica hardware dal codice.

All’interno di un processo si possono usare le istruzioni if e case, che non sono ammesse nel corpo dell’architecture fuori dai processi. Questo è il motivo per cui, per rilevare un fronte di clock, serve necessariamente un processo: rising_edge(clk) deve stare dentro un if, e l’if deve stare dentro un processo.

Simulazione e sintesi

Sono due attività collegate ma distinte. La simulazione esegue il codice VHDL su un computer per verificare il funzionamento prima della sintesi. Permette di usare costrutti non sintetizzabili, come wait for 100 ns, che nel mondo fisico non hanno equivalente diretto ma sono utili per generare stimoli durante i test.

La sintesi traduce il VHDL in bitstream, la sequenza di bit che configura l’FPGA. Non tutto ciò che funziona in simulazione è sintetizzabile, e viceversa, alcuni comportamenti sull’hardware reale possono differire dalla simulazione per via del delta cycle e dei ritardi di propagazione fisici. Il test sull’hardware rimane sempre necessario.

Dalla porta AND al contatore parametrico

Nel video proviamo tutto questo su EDA Playground, un ambiente online che permette di simulare VHDL senza installare nulla in locale, usando GHDL come simulatore.

Il primo esempio è una semplice porta AND: entity con due ingressi a e b di tipo std_logic e un’uscita z, e un’architecture RTL con una sola istruzione concorrente z <= a and b. Il test bench applica tutte e quattro le combinazioni di ingresso e permette di verificare il comportamento nel waveform viewer.

Il secondo esempio è un contatore parametrico: il numero di bit è configurabile tramite un generic N_BIT con valore di default a 8. L’uscita q è uno std_logic_vector(N_BIT-1 downto 0). Per poter incrementare il contatore, occorre convertire il vettore in unsigned, sommare 1, e riconvertire. Per aggirare il vincolo del VHDL93 che non permette di leggere un’uscita di tipo out, si introduce un segnale interno q_internal che viene assegnato all’uscita fuori dal processo.

Il LED che lampeggia su Spartan-6

L’ultimo passo è portare tutto su hardware reale. Su ISE creiamo un nuovo progetto con top level HDL, scheda AX309, chip XC6SLX16 speed -2, linguaggio VHDL93.

Il componente ha un ingresso clk e un’uscita led, entrambi std_logic. Internamente un segnale intero cnt viene incrementato a ogni fronte di salita del clock. Quando raggiunge il valore di soglia (circa 25 milioni, per dimezzare i 50 MHz del quarzo e ottenere circa 1 Hz), il valore del LED viene invertito e il contatore azzerato.

Anche qui compare il vincolo sull’uscita: led è out e non si può leggere, quindi si introduce led_internal. Un’assegnazione concorrente fuori dal processo copia led_internal su led.

Il file UCF assegna il clock al pin T8 e il LED al pin M6. La sintesi va a buon fine, la programmazione via Impact con il Platform Cable funziona, e il LED sul bordo destro della scheda comincia a lampeggiare al ritmo atteso.

Cosa rimane fuori

Questa è un’introduzione, non un manuale. Restano fuori molti argomenti: la gestione gerarchica dei componenti, i package personalizzati, le macchine a stati, l’ottimizzazione delle risorse, il timing closure. Li affronteremo progressivamente nei prossimi video, man mano che i progetti lo richiedono.

Il punto di partenza, però, c’è. E il LED lampeggia.

Avatar Paolo Godino