Introduzione
Una FPGA (Field Programmable Gate Array) è un chip elettronico riprogrammabile, cioè un circuito integrato che può essere configurato “sul campo” dall’utente per eseguire una vasta gamma di funzioni logiche, a differenza dei chip fissi (ASIC). Contiene una matrice di blocchi logici e interconnessioni programmabili che permettono di creare circuiti digitali personalizzati per applicazioni che richiedono alta velocità, bassa latenza e flessibilità, come nell’IA, nelle telecomunicazioni e nel settore automobilistico.
Essa è costituita da:
- blocchi logici configurabili (CLB): sono piccole unità logiche che possono essere impostate per eseguire operazioni specifiche (come porte AND, OR, ecc.);
- interconnessioni programmabili: un “tessuto” di percorsi configurabili che collega i CLB tra loro e con i dispositivi esterni, permettendo di definire la struttura del circuito.
- linguaggi HDL: si programma usando linguaggi di descrizione hardware come VHDL o Verilog, che definiscono come i blocchi e le interconnessioni devono essere collegati.
L’immagine seguente mostra un esempio di CLB:

Differenze chiave con ASIC e CPU:
- flessibilità: può essere riconfigurata più volte per scopi diversi, mentre un ASIC è fisso dopo la produzione;
- performance: offre un’elaborazione parallela efficiente e personalizzata, spesso superando le CPU in compiti specifici come l’IA.
- costi/tempo: più flessibile e veloce per la prototipazione rispetto agli ASIC, che richiedono costi elevati per la riprogettazione.
Applicazioni principali:
- Intelligenza Artificiale (IA): accelerazione di modelli di deep learning, elaborazione dati in tempo reale;
- settore automobilistico: sistemi di sicurezza, gestione batterie e sensori nei veicoli elettrici;
- telecomunicazioni: infrastrutture di rete e elaborazione dei segnali;
- prototipazione: sviluppo rapido di nuovi progetti hardware.
Quindi le FPGA sono dispositivi logici riconfigurabili: invece di avere una CPU che esegue un firmware istruzione dopo istruzione, c’è una matrice di risorse hardware (logica, registri, memoria, routing) che possono essere ricablati virtualmente per farle diventare un circuito che svolge apposite funzionalità. Questo punto è il vero cambio di paradigma: quando si scrive un programma in C per un microcontrollore si sta definendo una sequenza di operazioni che la CPU eseguirà nel tempo; quando si scrive del Verilog per una FPGA si sta descrivendo una struttura hardware che, dopo la sintesi e il place&route, viene “materializzata” dentro il chip e lavora in parallelo, a colpi di clock, con una prevedibilità temporale che nel mondo MCU spesso è un lusso (interrupt, jitter, bus contesi, RTOS, cache, periferiche condivise). Per un progettista embedded la FPGA non è quindi un “micro più veloce”, ma un modo diverso di risolvere problemi: si possono creare periferiche su misura, pipeline di elaborazione segnali, logiche di controllo hard real-time o interfacce ad alta velocità senza dover forzare l’architettura di un micro a fare cose per cui non è nata. Un punto di forza molto importante è che, oggi, questo non richiede per forza tool proprietari, licenze o workstation: esistono famiglie come la iCE40 (e in particolare la iCE40UP5K) che si prestano bene a un percorso didattico e pratico con toolchain completamente open-source, ideale per costruire competenze “spendibili” e progetti riproducibili.
Per un progettista embedded, guardare alle FPGA ha quindi senso solo se si chiarisce subito un punto fondamentale: una FPGA non è un microcontrollore più potente, né una CPU alternativa. È uno strumento diverso, pensato per risolvere classi di problemi che con un MCU diventano rapidamente complesse, fragili o inefficienti. Il valore di una FPGA sta nel parallelismo reale, nella latenza deterministica e nella possibilità di realizzare logiche e periferiche completamente custom. In una FPGA più operazioni possono avvenire nello stesso ciclo di clock, senza preemption, interrupt o contese di risorse, perché non esiste un “flusso di esecuzione” unico da schedulare. Questo apre scenari pratici molto concreti: ad esempio la generazione di un segnale audio tramite PWM o sigma-delta, dove il timing dei fronti è critico; l’implementazione di un filtro FIR elementare per l’elaborazione di segnali audio o di sensori, sfruttando blocchi DSP hardware; la realizzazione di un sistema di acquisizione digitale con trigger precisi e buffer circolari; oppure la creazione di un’interfaccia SPI o parallela non standard, adattata esattamente al dispositivo esterno da controllare. In tutti questi casi, una FPGA permette di spostare la complessità dal software all’hardware, ottenendo sistemi più prevedibili, più reattivi e spesso più semplici da validare rispetto a soluzioni basate esclusivamente su microcontrollori e firmware.
Un altro aspetto rilevante è il controllo temporale. In molti sistemi embedded il problema non è “fare più calcoli”, ma sapere esattamente quando un evento avviene: fronti di clock, finestre temporali, latenze costanti, sincronizzazione tra segnali. Su una FPGA il comportamento temporale è parte integrante del progetto, perché il circuito risultante è sincrono per costruzione e il timing viene analizzato e validato in fase di sintesi e place&route. Questo rende le FPGA particolarmente adatte a contesti hard real-time, a protocolli non standard o a elaborazioni streaming dove ogni campione deve essere trattato con una latenza nota e costante. Al contrario, una FPGA non è la scelta giusta per tutto: quando servono stack di comunicazione complessi, filesystem, networking o logica decisionale articolata, un microcontrollore o un SoC rimangono strumenti più efficienti. È proprio per questo che, in molti sistemi moderni, FPGA e MCU convivono: il micro gestisce il “controllo alto livello”, mentre la FPGA si occupa delle parti a timing critico o ad alta parallelizzazione. Comprendere questa complementarità è uno degli obiettivi principali di questo percorso introduttivo.
MCU vs FPGA: le differenze fondamentali
La differenza tra un microcontrollore e una FPGA non è una questione di prestazioni, ma di modello computazionale. In un sistema basato su MCU il software descrive una sequenza di istruzioni che una CPU esegue nel tempo: anche quando si utilizzano interrupt, DMA o un RTOS, esiste sempre un flusso di controllo centrale che arbitra l’uso delle risorse e introduce inevitabilmente variabilità temporale. In una FPGA, invece, non esiste un “programma” che viene eseguito: il codice HDL (cioè il linguaggio di descrisione dell’hardware) descrive una struttura hardware composta da logica combinatoria e registri (tipicamente implementati tramite LUT e flip-flop) collegati da una rete di routing, sincronizzati da uno o più clock. Dopo la sintesi e il place&route, questa descrizione viene trasformata in un circuito fisico che opera in parallelo per costruzione, non per simulazione software del parallelismo. Quando si afferma che “tutto succede in parallelo” non è un modo di dire, ma una conseguenza diretta dell’architettura: due blocchi descritti in Verilog non vengono schedulati, ma coesistono come parti indipendenti dello stesso circuito. Le LUT implementano funzioni logiche arbitrarie, i flip-flop memorizzano lo stato sincrono e determinano l’evoluzione del sistema a ogni fronte di clock; il tempo non è più una risorsa condivisa da gestire, ma una dimensione progettuale esplicita, analizzata e verificata tramite vincoli di timing. Questo spiega perché concetti come latenza deterministica, throughput costante e pipeline hardware siano naturali nel mondo FPGA e molto più difficili da garantire in un sistema puramente software. Comprendere questa differenza è il passaggio cruciale per chi proviene dall’embedded tradizionale: non si tratta di “imparare un nuovo linguaggio”, ma di adottare un modo diverso di pensare il progetto, in cui il codice descrive hardware e il comportamento temporale è parte integrante della specifica.
La difficoltà principale, per chi proviene dal mondo dei microcontrollori, sta nel fatto che il linguaggio HDL assomiglia superficialmente a un linguaggio di programmazione, ma il suo significato è radicalmente diverso. In un MCU il tempo è implicito: il programmatore scrive istruzioni e il processore le esegue una dopo l’altra, demandando all’architettura interna (pipeline, cache, interrupt, DMA) la gestione del “quando”. Anche quando si introducono costrutti concorrenti, come task RTOS o interrupt annidati, il parallelismo resta una forma di multiplexing temporale su una singola unità di esecuzione o su poche risorse condivise. In una FPGA il tempo è invece esplicito e strutturale: ogni registro rappresenta uno stato che evolve a ogni fronte di clock, e ogni rete combinatoria rappresenta una trasformazione che avviene tra un evento sincrono e il successivo. Scrivere Verilog non significa dire “fai prima questo e poi quello”, ma definire quali segnali esistono, come sono collegati e come reagiscono agli eventi di clock. Se due blocchi sono descritti nello stesso modulo, entrambi esistono simultaneamente nel silicio e operano in parallelo, indipendentemente dal fatto che il progettista li percepisca come “prima” o “dopo” nel codice sorgente. Questa distinzione ha conseguenze pratiche importanti: non esiste un program counter, non esiste uno stack di chiamate, non esiste un flusso di controllo globale. Esistono invece percorsi di dati, latenze definite dal numero di registri attraversati e vincoli temporali che devono essere rispettati fisicamente. Il comportamento del sistema non è determinato dall’ordine delle istruzioni, ma dalla topologia del circuito e dalla sincronizzazione dei suoi elementi. È per questo che nelle FPGA concetti come pipeline, throughput e latenza sono fondamentali fin dalle prime esperienze, mentre nel mondo MCU emergono solo in contesti avanzati. Chiarire questo punto è essenziale: una FPGA non “esegue codice”, ma realizza hardware, e il codice HDL è semplicemente il mezzo per descriverlo in forma astratta e sintetizzabile.
Come è fatta internamente una FPGA?
Per comprendere cosa renda una FPGA diversa da un microcontrollore è necessario entrare, almeno concettualmente, nelle sue componenti fondamentali. Una FPGA è composta da una grande quantità di blocchi logici elementari, interconnessi da una rete di routing programmabile. Il blocco base è la LUT (Look-Up Table), che può essere vista come una piccola memoria in grado di implementare qualsiasi funzione logica combinatoria di un certo numero di ingressi (praticamente implementa una funzione logica che potrebbe essere realizzata tramite porte logiche di base). Accoppiata alla LUT si trova tipicamente un flip-flop, che consente di memorizzare uno stato sincrono e di realizzare logica sequenziale.
L’immagine seguente riporta le principali porte logiche con le loro tabelle di verità:

L’immagine seguente mostra il flip flop di tipo D con la relativa tabella di verità e il circuito interno:

La distinzione tra logica combinatoria e logica sequenziale è centrale: la prima produce un’uscita che dipende istantaneamente dagli ingressi, la seconda introduce memoria e quindi dipendenza dal tempo e dal clock. In una FPGA, il comportamento di un sistema digitale nasce proprio dalla combinazione di questi due elementi, organizzati in reti più o meno complesse. Accanto alla logica di base, le FPGA moderne integrano memorie interne (Block RAM o, nel caso della iCE40UP5K, anche SRAM dedicate) utilizzabili come buffer, FIFO o storage temporaneo, e blocchi DSP specializzati per operazioni aritmetiche efficienti, come moltiplicazioni e accumuli, fondamentali per l’elaborazione di segnali digitali. Queste risorse non sono “periferiche” nel senso classico del termine, ma parti strutturali del circuito che il progettista può combinare liberamente per costruire pipeline di calcolo, filtri, controllori o interfacce custom.
L’immagine seguente mostra lo schema a blocchi di una logica sequenziale generica:

Il passaggio dalla descrizione astratta all’hardware reale avviene attraverso due fasi chiave: sintesi e place&route. La sintesi analizza il codice HDL e lo trasforma in una netlist logica, cioè una rappresentazione del circuito in termini di LUT, registri, RAM e connessioni logiche. Il place&route prende questa netlist e la mappa fisicamente sulle risorse della FPGA, decidendo dove collocare ogni blocco e come instradare i segnali rispettando vincoli temporali e fisici. È in questa fase che il progetto diventa “reale”: i ritardi di propagazione, i percorsi critici e il timing non sono più concetti teorici, ma parametri misurabili che determinano se il circuito funzionerà correttamente alla frequenza desiderata. Questo flusso evidenzia un’altra differenza sostanziale rispetto al software: il comportamento temporale non è un effetto collaterale dell’esecuzione, ma un risultato diretto delle scelte architetturali.
I linguaggi di descrizione dell’hardware, come VHDL e Verilog, sono lo strumento con cui questa struttura viene espressa. Sebbene la sintassi possa ricordare quella dei linguaggi di programmazione, il loro scopo non è descrivere un algoritmo, ma un circuito. In VHDL e Verilog è possibile descrivere, ad esempio, una semplice rete combinatoria che collega ingressi e uscite, oppure una logica sequenziale che aggiorna registri a ogni fronte di clock. Un’istruzione che in C rappresenterebbe un’operazione da eseguire in sequenza, in un HDL rappresenta invece una relazione permanente tra segnali. Questa differenza semantica è fondamentale: il codice HDL non viene “eseguito”, ma interpretato dal tool di sintesi per costruire l’hardware corrispondente. Comprendere questo aspetto permette di evitare uno degli errori più comuni dei principianti, ovvero tentare di scrivere HDL come se fosse software, perdendo di vista la natura strutturale e temporale del progetto.
LUT: cosa è e cosa implementa
Una LUT (Look-Up Table) implementa una funzione logica combinatoria: dati N ingressi, produce una o più uscite secondo una relazione booleana definita. Concettualmente, una LUT può essere vista come l’equivalente di una rete di porte logiche elementari (AND, OR, NAND, NOT, XOR, ecc.) rappresentata da un unico blocco configurabile. Invece di costruire fisicamente una rete di porte per ogni funzione, la FPGA utilizza una piccola memoria interna che, per ogni possibile combinazione degli ingressi, restituisce il valore dell’uscita. Dal punto di vista funzionale, non c’è differenza tra una LUT configurata per implementare, ad esempio, una funzione XOR o una somma combinatoria, e una rete di porte logiche progettata a mano: ciò che cambia è il modo in cui questa funzione viene realizzata fisicamente all’interno del chip. Questa astrazione consente al tool di sintesi di mappare automaticamente espressioni logiche anche complesse sulle risorse disponibili, mantenendo per il progettista un modello concettuale vicino alla logica digitale classica.
L’immagine seguente mostra un semplice esempio di funzione logica rappresentata tramite una LUT:

Flip-flop: perché è un elemento di memoria
Il flip-flop è l’elemento che introduce la nozione di stato in un circuito digitale. A differenza della logica combinatoria, che reagisce istantaneamente agli ingressi, un flip-flop memorizza un valore e lo mantiene stabile fino a un evento di sincronizzazione, tipicamente il fronte di un clock. In una FPGA, i flip-flop sono accoppiati alle LUT e rappresentano il modo standard per realizzare registri, contatori, macchine a stati finiti e pipeline. Dal punto di vista concettuale, un flip-flop può essere visto come una cella di memoria a un bit, il cui contenuto viene aggiornato solo in istanti ben definiti. Questo è il motivo per cui la logica sequenziale è intrinsecamente legata al tempo: il comportamento del circuito non dipende solo dagli ingressi correnti, ma anche dai valori memorizzati nei flip-flop al ciclo di clock precedente. Senza flip-flop non esisterebbero concetti come “stato”, “memoria” o “evoluzione nel tempo” all’interno di una FPGA.
Logica combinatoria vs logica sequenziale: la distinzione fondamentale
La distinzione tra logica combinatoria e logica sequenziale è uno dei pilastri della progettazione digitale. La logica combinatoria è costituita esclusivamente da funzioni che collegano ingressi e uscite senza alcuna memoria: l’uscita è sempre una funzione diretta e immediata degli ingressi correnti. Un circuito realizzato solo con porte logiche elementari rientra in questa categoria. La logica sequenziale, invece, combina logica combinatoria ed elementi di memoria (flip-flop), permettendo al circuito di “ricordare” il passato. In questo caso l’uscita dipende sia dagli ingressi sia dallo stato interno del sistema. In una FPGA entrambe le tipologie convivono: la logica combinatoria realizza le trasformazioni dei segnali tra un ciclo di clock e il successivo, mentre la logica sequenziale scandisce il tempo e conserva l’informazione. Questa separazione concettuale è essenziale per comprendere come funzionano pipeline, contatori, filtri digitali e macchine a stati, e rappresenta uno dei punti di maggiore distanza rispetto alla programmazione tradizionale, dove stato e tempo sono spesso impliciti e gestiti dal flusso di esecuzione del software.
Organizzazione interna della FPGA
Gli elementi descritti finora (LUT, flip-flop, memorie e blocchi DSP) non esistono in modo isolato all’interno della FPGA, ma sono organizzati in una struttura regolare a matrice, composta da un gran numero di blocchi logici elementari interconnessi. Ogni blocco logico contiene tipicamente una o più LUT, registri associati e risorse ausiliarie, ed è progettato per essere configurabile in modo indipendente. La programmazione di una FPGA non consiste quindi solo nel “settare” il comportamento interno di ciascun blocco (ad esempio definendo quale funzione logica una LUT deve implementare o quando un flip-flop deve campionare un segnale), ma anche nel definire come questi blocchi devono essere collegati tra loro. La rete di interconnessione programmabile permette infatti di instradare segnali da un blocco all’altro, creando catene di elaborazione, strutture gerarchiche e percorsi di dati arbitrari. È proprio questa combinazione di configurazione locale e routing globale che consente di costruire funzioni complesse a partire da elementi semplici: un contatore, una macchina a stati, una pipeline di elaborazione o un filtro digitale non sono entità primitive, ma il risultato dell’aggregazione coordinata di molti blocchi logici distribuiti sulla matrice. In questo senso, una FPGA può essere vista come un grande foglio di logica digitale riconfigurabile, in cui il progettista (assistito dagli strumenti di sintesi) decide sia il comportamento dei singoli mattoni sia il modo in cui questi vengono collegati per realizzare un circuito di ordine superiore. Questa architettura spiega perché concetti come topologia del circuito, lunghezza dei percorsi di segnale e organizzazione dei registri abbiano un impatto diretto sul comportamento e sulle prestazioni del sistema finale.
L’immagine seguente mostra l’architettura interna di una FPGA:

Place&route: collocazione fisica e collegamenti reali
Una volta chiarito che una FPGA è una matrice di blocchi logici elementari interconnessi, diventa naturale chiedersi come una descrizione astratta in HDL venga trasformata in un circuito fisico funzionante. Questo passaggio avviene attraverso una sequenza di processi che vengono comunemente indicati come sintesi, mapping, place e route, e che nel loro insieme rappresentano molto più di una semplice “compilazione”. Dopo la sintesi logica, che traduce il codice HDL in una netlist astratta di funzioni logiche, registri e memorie, il progetto deve essere mappato sulle risorse effettivamente disponibili nella FPGA. Il mapping consiste nell’adattare le funzioni logiche descritte a un insieme finito di elementi reali: LUT di una certa dimensione, flip-flop fisicamente presenti, blocchi RAM e DSP con caratteristiche specifiche. In questa fase vengono prese decisioni architetturali importanti, come la decomposizione di funzioni complesse in più LUT o l’uso di risorse dedicate invece di logica generica.
Il passo successivo è il place, ovvero la collocazione fisica dei blocchi mappati all’interno della matrice della FPGA. Ogni LUT, flip-flop o blocco RAM viene assegnato a una posizione precisa sul chip. Questa scelta non è arbitraria: la distanza fisica tra i blocchi influisce sui ritardi di propagazione dei segnali e quindi sul rispetto dei vincoli temporali. Un cattivo posizionamento può rendere impossibile raggiungere una certa frequenza di clock, anche se la logica in sé è corretta. Per questo motivo il place è un problema di ottimizzazione complesso, in cui il tool cerca di bilanciare compattezza, parallelismo e vincoli di timing.
Dopo il posizionamento entra in gioco il routing, cioè la fase in cui vengono stabiliti i collegamenti fisici tra i blocchi. All’interno della FPGA esiste una fitta rete di interconnessioni programmabili. Il routing consiste nel selezionare, per ogni segnale, un percorso attraverso questa rete che colleghi l’uscita di un blocco all’ingresso di un altro. Dal punto di vista concettuale, questi collegamenti possono essere immaginati come interruttori riconfigurabili: ciascun nodo di routing può essere abilitato o disabilitato tramite bit di configurazione memorizzati nella FPGA. Nelle architetture moderne, come la iCE40, non si tratta di fusibili fisici che vengono bruciati una volta per tutte, ma di celle di memoria (tipicamente SRAM) che controllano la connettività interna e possono essere riprogrammate a ogni caricamento del bitstream. Questo è il motivo per cui una FPGA può essere riconfigurata infinite volte, a differenza delle vecchie tecnologie antifuse.
Il risultato finale del place&route è una descrizione completamente concreta del circuito: ogni funzione logica è assegnata a una LUT specifica, ogni registro a un flip-flop fisico, e ogni segnale percorre un insieme definito di interconnessioni. A questo punto il progetto è diventato, a tutti gli effetti, un circuito digitale reale, con ritardi, percorsi critici e limiti fisici ben definiti. Il bitstream generato dai tool non è altro che l’insieme dei dati necessari a configurare questi comportamenti: contenuto delle LUT, stato dei flip-flop iniziali, configurazione dei blocchi RAM e DSP, e soprattutto impostazione dei nodi di routing interni. Comprendere questo flusso aiuta a capire perché, nel mondo FPGA, concetti come timing closure, percorsi critici e vincoli di clock non siano dettagli di basso livello, ma aspetti centrali del progetto. Il comportamento temporale del sistema non emerge “a runtime”, ma è il risultato diretto delle scelte di mapping, posizionamento e collegamento effettuate prima ancora che il circuito venga acceso.
Vincoli di sintesi per timing o area
Una volta che un progetto è stato mappato, posizionato e instradato, entra in gioco uno degli aspetti più delicati e caratteristici della progettazione su FPGA: il timing. A differenza del software, dove la velocità di esecuzione è spesso una conseguenza indiretta dell’architettura e del carico del sistema, in una FPGA il comportamento temporale è una proprietà strutturale del circuito risultante. Ogni segnale che attraversa la logica combinatoria e le interconnessioni interne impiega un certo tempo di propagazione, determinato sia dalla complessità logica (numero e tipo di LUT attraversate) sia dalla distanza fisica percorsa all’interno del chip. Quando più blocchi sono concatenati tra due elementi di memoria sincrona, il tempo totale di propagazione lungo quel percorso diventa un parametro critico per il corretto funzionamento del sistema.
In un progetto sincrono classico, il periodo di clock massimo è vincolato dal percorso più lento tra due flip-flop consecutivi, noto come percorso critico. Questo percorso include la logica combinatoria intermedia, il routing fisico e i tempi di setup e hold dei registri di destinazione. Se il segnale non arriva stabilizzato entro il fronte di clock successivo, il circuito smette di funzionare correttamente, indipendentemente dal fatto che “funzioni” logicamente. È per questo motivo che la frequenza massima raggiungibile da una FPGA non è una proprietà astratta del chip, ma dipende in modo diretto dalla struttura del progetto e da come esso è stato mappato e instradato. In altre parole, la velocità del sistema è determinata dal percorso più lento, non da quello medio o da quello tipico.
Storicamente, come ricordato, i tool di sintesi permettevano di ottimizzare il progetto privilegiando l’area occupata oppure il tempo di propagazione. Questo concetto è ancora valido, anche se oggi viene espresso in termini più formali tramite vincoli di timing. Il progettista può specificare la frequenza di clock desiderata o i requisiti temporali tra segnali, e i tool cercano di soddisfarli scegliendo soluzioni che bilancino area, prestazioni e consumo. Ottimizzare per area significa ridurre il numero di risorse utilizzate, spesso accettando percorsi logici più lunghi; ottimizzare per velocità implica invece duplicare logica, spezzare percorsi con registri intermedi (pipelining) o usare risorse dedicate, aumentando l’area ma riducendo la latenza critica. Queste scelte non sono automatiche in senso assoluto, ma dipendono dagli obiettivi del progetto e dai vincoli dichiarati.
I tool di place&route moderni eseguono una analisi temporale statica (Static Timing Analysis), che valuta tutti i percorsi rilevanti del circuito senza bisogno di simulare il funzionamento nel tempo. Al termine del processo, il progettista riceve report dettagliati che indicano se i vincoli sono rispettati, quali sono i percorsi critici e con quale margine temporale (slack). Questi report non sono un dettaglio accessorio, ma uno strumento fondamentale di progettazione: segnalano dove il circuito è “lento” e guidano le modifiche architetturali necessarie, come l’introduzione di pipeline, la riorganizzazione della logica o la modifica della topologia dei collegamenti. Comprendere il timing e i percorsi critici significa quindi comprendere perché una FPGA può funzionare perfettamente a una certa frequenza e fallire completamente pochi megahertz più in alto, anche senza cambiare una sola riga di codice HDL.
HDL (VHDL/Verilog) come strumento per descrivere strutture sincrone e pipeline
I linguaggi di descrizione dell’hardware (HDL, Hardware Description Languages) sono strumenti utilizzati per descrivere la struttura e il comportamento temporale di un circuito digitale, non per esprimere una sequenza di istruzioni da eseguire. Questa distinzione è fondamentale: un HDL non è un linguaggio di programmazione nel senso classico del termine. Non esiste un flusso di esecuzione, non esiste un program counter e non esiste un ordine temporale implicito legato alla posizione delle istruzioni nel codice. Un linguaggio HDL serve a specificare quali componenti hardware esistono, come sono collegati tra loro e come reagiscono agli eventi di clock. Il codice HDL viene interpretato dagli strumenti di sintesi come una descrizione strutturale e temporale, dalla quale viene derivato un circuito fisico fatto di logica combinatoria, registri, memorie e interconnessioni. Pensare a un HDL come a un “linguaggio di programmazione per hardware” è quindi fuorviante: è più corretto considerarlo come un linguaggio di specifica di circuiti sincroni.
I due linguaggi HDL più diffusi sono VHDL e Verilog, entrambi standardizzati e ampiamente supportati. VHDL nasce in ambito aerospaziale e militare, ed è fortemente tipizzato, molto verboso e orientato alla descrizione rigorosa del comportamento. Questa rigidità lo rende adatto a grandi progetti industriali e a contesti dove la chiarezza formale e la verificabilità sono prioritarie. Verilog, nato in ambito più accademico e industriale commerciale, ha una sintassi più compatta e una semantica che, pur restando hardware-centrica, risulta spesso più accessibile a chi proviene dal mondo C-like. Le differenze tra i due non sono tanto nelle capacità espressive—entrambi possono descrivere la stessa classe di circuiti—quanto nello stile e nel modo in cui guidano il progettista a pensare l’hardware. In entrambi i casi, il concetto fondamentale resta invariato: il codice descrive strutture che esistono simultaneamente, non operazioni che avvengono una dopo l’altra.
Un esempio semplice aiuta a chiarire il concetto. Si consideri un circuito elementare composto da un ingresso, un registro e un’uscita: a ogni fronte di clock, il valore dell’ingresso viene memorizzato e reso disponibile in uscita. In termini hardware, si tratta semplicemente di un flip-flop. In VHDL, questo comportamento viene descritto come un processo sensibile al clock, che assegna il valore dell’ingresso al registro solo in corrispondenza di un evento temporale ben definito. In Verilog, lo stesso circuito viene espresso tramite un blocco always sensibile al fronte di clock. In entrambi i casi, non si sta dicendo “prima leggi l’ingresso, poi scrivi l’uscita”, ma si sta dichiarando l’esistenza di un registro sincrono che aggiorna il proprio stato a ogni fronte di clock. La sintassi può variare, ma la semantica è identica: il risultato non è un algoritmo, bensì un componente hardware che continuerà a funzionare in parallelo con il resto del circuito.
Questo approccio rende naturali concetti come pipeline e parallelismo strutturale. Aggiungere uno stadio di pipeline non significa inserire un’istruzione in più, ma introdurre un nuovo registro che spezza un percorso combinatorio, riducendo il tempo di propagazione e aumentando la frequenza massima raggiungibile. Allo stesso modo, descrivere due blocchi in parallelo non richiede costrutti speciali: è sufficiente definirli entrambi, perché nella FPGA essi esistano e operino simultaneamente. Comprendere il ruolo degli HDL come strumenti di descrizione strutturale è quindi essenziale per affrontare la progettazione su FPGA con il giusto modello mentale. Solo a questo punto diventa chiaro perché scrivere HDL “come se fosse C” porta a risultati errati o inefficienti, e perché la vera competenza non sta nella sintassi del linguaggio, ma nella capacità di progettare architetture digitali sincrone coerenti con le risorse e i vincoli fisici del dispositivo.
I livelli di descrizione dell’hardware in HDL
In HDL lo stesso circuito può essere descritto a diversi livelli di astrazione. Storicamente (e concettualmente) se ne distinguono tre, anche se nella pratica quotidiana se ne usano soprattutto due.
Descrizione comportamentale / algoritmica (behavioral)
È il livello più alto.
Qui si descrive cosa deve fare il circuito, non come è realizzato internamente.
Il progettista specifica il comportamento funzionale, lasciando al sintetizzatore il compito di decidere quale hardware usare per implementarlo.
Esempi concettuali:
- “L’uscita è l’AND di due ingressi”
- “Somma due numeri e registra il risultato a ogni clock”
- “Se una condizione è vera, aggiorna un registro”
A questo livello:
- non si parla di LUT, flip-flop, porte
- non si specifica la struttura fisica
- si lavora per relazioni logiche e temporali
Il sintetizzatore:
- analizza il comportamento
- lo traduce in logica combinatoria + registri
- decide come mappare tutto sulle risorse disponibili
È il livello più vicino a “descrivo la funzionalità e il tool decide come implementarla”.
Descrizione RTL (Register Transfer Level)
È il livello centrale ed è quello più importante in pratica.
Qui non si scende a “porte elementari”, ma si descrive:
- come i dati si muovono tra registri
- quale logica combinatoria esiste tra un registro e l’altro
- quando (a quale clock) avvengono gli aggiornamenti
RTL significa letteralmente:
trasferimento di dati tra registri sotto controllo del clock
A questo livello:
- si ragiona in termini di registri, pipeline, FSM
- la distinzione combinatoria / sequenziale è esplicita
- il comportamento temporale è chiaro e controllabile
È il punto di equilibrio:
- abbastanza astratto da essere produttivo
- abbastanza concreto da controllare timing, area e architettura
Descrizione strutturale / gate-level
È il livello più basso.
Qui si descrive l’hardware esplicitamente come rete di componenti elementari:
- porte logiche
- multiplexer
- flip-flop istanziati uno per uno
- collegamenti espliciti
È il livello che richiami quando dici:
“descrivo proprio come funziona la AND a basso livello”
Storicamente era importante:
- prima dei sintetizzatori moderni
- per celle standard ASIC
- per modelli di simulazione post-sintesi
Oggi:
- raramente si scrive a mano
- viene generato automaticamente dai tool (netlist)
- è usato soprattutto per verifica o debug
Il punto importante, soprattutto dal punto di vista concettuale, è che:
behavioral → RTL → strutturale
non sono linguaggi diversi, ma livelli diversi di interpretazione dello stesso HDL
- scrivendo behavioral HDL, si chiede al tool di derivare RTL e struttura;
- scrivendo RTL, si controlla direttamente l’architettura risultante;
- la descrizione strutturale è il risultato finale del processo di sintesi.
Nel caso della AND:
- a livello behavioral: “uscita = ingresso1 AND ingresso2”
- a livello RTL: una rete combinatoria tra registri (se presenti)
- a livello strutturale: una LUT configurata come AND + routing interno
Tutte e tre le descrizioni portano allo stesso hardware, ma con gradi di controllo diversi.
Per un progettista embedded che arriva dal software, il messaggio importante è questo: in HDL non si sceglie se descrivere comportamento o struttura, si sceglie quanto controllo si vuole esercitare sull’hardware finale:
- più alto il livello → più libertà al tool
- più basso il livello → più controllo, ma più complessità
E la competenza vera non è “sapere la sintassi”, ma:
- sapere quando stare a livello behavioral
- quando scendere a RTL
- e quando non serve scendere oltre
Questo si collega perfettamente a:
- timing
- area vs prestazioni
- pipeline
- DSP su FPGA
In sostanza, un HDL permette di descrivere lo stesso circuito a diversi livelli di astrazione
Perché iCE40UP5K + toolchain open è una scelta intelligente
Per rendere concreti i concetti introdotti finora è utile riferirsi a una piattaforma reale, accessibile e tecnicamente significativa. In questa serie verrà utilizzata la board iCESugar v1.5, basata sulla FPGA Lattice iCE40UP5K, un dispositivo che rappresenta un buon compromesso tra semplicità, risorse disponibili e apertura dell’ecosistema. La iCE40UP5K appartiene alla famiglia UltraPlus ed integra alcune migliaia di LUT e flip-flop, memorie interne utilizzabili come buffer o FIFO, e un set di blocchi DSP dedicati alle operazioni aritmetiche, rendendo possibile l’implementazione di pipeline di elaborazione, filtri digitali e logiche di controllo sincrone senza ricorrere a soluzioni esterne. Tutti gli elementi teorici descritti nei paragrafi precedenti (logica combinatoria, logica sequenziale, registri, routing programmabile, timing e percorsi critici) trovano riscontro diretto in questa FPGA: le LUT implementano le funzioni booleane, i flip-flop scandiscono il tempo e memorizzano lo stato, i blocchi DSP accelerano le operazioni matematiche, e la rete di interconnessione interna consente di collegare questi elementi in architetture più complesse.
Dal punto di vista architetturale, la iCE40UP5K è sufficientemente piccola da essere comprensibile e sufficientemente grande da non essere limitante per progetti didattici realistici. Si tratta di un dispositivo pensato per applicazioni embedded a basso consumo, dove il controllo temporale, la parallelizzazione e l’integrazione di logiche custom sono più importanti della potenza di calcolo general-purpose. Questo la rende particolarmente adatta come primo contatto serio con la progettazione FPGA: le risorse sono abbastanza limitate da costringere a ragionare in termini di architettura e ottimizzazione, ma abbastanza ricche da permettere esempi significativi come generatori PWM audio, filtri FIR di base, macchine a stati e interfacce di comunicazione personalizzate.
La board iCESugar completa il quadro fornendo un contesto pratico immediatamente utilizzabile. Oltre alla FPGA, integra un sistema di programmazione e debug USB (iCELink), LED e switch onboard utili per i primi esperimenti, e connettori di espansione che permettono di portare all’esterno i segnali e collegare hardware aggiuntivo. Questo consente di concentrarsi sugli aspetti concettuali della progettazione — struttura del circuito, timing, organizzazione della logica — senza doversi occupare subito di circuiteria di supporto o programmatori esterni. In altre parole, la iCESugar permette di osservare direttamente come una descrizione HDL venga sintetizzata, mappata e trasformata in un circuito fisico funzionante, chiudendo il cerchio tra la teoria della progettazione digitale e la pratica su hardware reale.
Accanto alla FPGA vera e propria, la board iCESugar v1.5 integra una serie di componenti pensati per rendere immediatamente accessibile la sperimentazione e la validazione dei progetti. Il primo elemento rilevante è il sistema di programmazione e comunicazione USB, basato su un microcontrollore dedicato (iCELink), che espone al sistema host un’interfaccia di tipo CMSIS-DAP, una porta seriale virtuale e un dispositivo di mass storage. Questo consente di programmare la FPGA, monitorarne il comportamento e scambiare dati senza ricorrere a programmatori esterni o a cablaggi complessi. Dal punto di vista concettuale, questo componente svolge un ruolo analogo a quello di un debugger o di un bootloader nel mondo dei microcontrollori, ma è completamente separato dalla logica implementata nella FPGA, che resta interamente sotto il controllo del progettista.
L’immagine seguente mostra la board iCESugar v1.5:

La board mette inoltre a disposizione dispositivi di input e output onboard, come LED e switch, che permettono di verificare rapidamente il comportamento di un circuito senza hardware aggiuntivo. Questi elementi, apparentemente banali, sono estremamente utili nelle fasi iniziali: consentono di osservare direttamente l’effetto di una descrizione HDL, di testare il corretto funzionamento del clock e della logica sequenziale e di validare le prime interazioni tra logica combinatoria e stato interno. In questo senso, la board diventa uno strumento di apprendimento oltre che di sviluppo, perché rende visibili concetti altrimenti astratti come il cambiamento di stato a ogni fronte di clock o la risposta immediata della logica combinatoria agli ingressi.
Per l’espansione verso l’esterno, la iCESugar espone numerosi pin di I/O attraverso connettori compatibili con lo standard PMOD e header generici. Questi segnali sono collegati direttamente ai pin della FPGA e permettono di interfacciarsi con dispositivi esterni come sensori, convertitori, display o moduli di comunicazione. Dal punto di vista progettuale, questo significa che non si è limitati a esempi “chiusi” sulla board, ma si può estendere progressivamente il sistema, mantenendo il pieno controllo sull’architettura e sul timing delle interfacce. La presenza di alimentazioni già disponibili sui connettori semplifica ulteriormente le prime sperimentazioni, riducendo la necessità di circuiteria ausiliaria.
Nel suo insieme, la iCESugar può essere vista come una piattaforma di riferimento compatta: abbastanza semplice da non nascondere il funzionamento interno della FPGA, ma sufficientemente completa da permettere progetti realistici. Tutti gli elementi descritti (programmazione, I/O di base, espansione) sono funzionali a un obiettivo preciso: consentire di osservare e comprendere il percorso che porta da una descrizione HDL a un comportamento hardware verificabile, senza introdurre complessità non essenziali nelle prime fasi di apprendimento.
L’immagine seguente mostra la il pinout e le porte della board iCESugar v1.5:

La toolchain
Per trasformare una descrizione HDL in un circuito funzionante sulla FPGA è necessario un insieme di strumenti software comunemente indicato come toolchain. A differenza del mondo dei microcontrollori, dove spesso esiste un ambiente integrato monolitico, nel caso delle FPGA la toolchain riflette in modo più esplicito le diverse fasi del flusso di progettazione: analisi del codice, sintesi logica, mappatura sulle risorse fisiche, posizionamento, instradamento e generazione del bitstream finale. Nel contesto di questa serie verrà utilizzata una toolchain completamente open-source, composta da strumenti separati ma ben integrati, ciascuno responsabile di una parte specifica del processo. Questa scelta rende visibili i passaggi intermedi e permette di comprendere cosa accade tra la scrittura del codice HDL e la configurazione dell’hardware.
Il primo elemento del flusso è Yosys, che si occupa della sintesi logica. Yosys analizza il codice HDL e lo traduce in una rappresentazione interna del circuito, eliminando costrutti non sintetizzabili e trasformando le descrizioni comportamentali o RTL in una netlist di logica combinatoria e registri. È in questa fase che il linguaggio HDL perde definitivamente ogni ambiguità “software” e viene ricondotto a elementi hardware concreti. Yosys non decide dove questi elementi verranno collocati fisicamente, ma stabilisce cosa esiste nel circuito: quante funzioni logiche, quanti registri, quali memorie e quali connessioni astratte tra di essi.
Il passo successivo è gestito da nextpnr, che si occupa delle fasi di place&route. Partendo dalla netlist generata dalla sintesi, nextpnr mappa il circuito sulle risorse reali della FPGA, decide dove collocare ciascun blocco logico e come instradare i segnali attraverso la rete di interconnessione interna. È in questa fase che entrano in gioco il timing, i percorsi critici e i vincoli di clock: nextpnr verifica se l’architettura risultante è in grado di rispettare i requisiti temporali e fornisce report dettagliati sul comportamento del circuito. Questo strumento rende esplicito un aspetto spesso nascosto nei tool proprietari: il legame diretto tra struttura del progetto e prestazioni temporali ottenibili.
Una volta completato il place&route, il circuito deve essere trasformato in una forma che la FPGA possa caricare. Questo compito è affidato agli strumenti della suite IceStorm, che generano il bitstream, ovvero il file contenente tutte le informazioni necessarie a configurare la FPGA: contenuto delle LUT, configurazione dei flip-flop, impostazione dei blocchi RAM e DSP, e stato delle interconnessioni programmabili. Il bitstream non è un programma eseguibile, ma una fotografia completa della configurazione hardware desiderata. Caricarlo nella FPGA significa, di fatto, “ricablare” il chip secondo il progetto descritto.
Accanto al flusso di sintesi e implementazione, una toolchain moderna include anche strumenti di simulazione, fondamentali per verificare il comportamento del circuito prima di trasferirlo sull’hardware reale. La simulazione consente di osservare segnali interni, verificare la correttezza logica e analizzare il comportamento temporale senza dipendere dall’hardware fisico. Questo passaggio è particolarmente importante nel mondo FPGA, perché permette di isolare errori concettuali o architetturali prima di affrontare problemi di timing o di integrazione.
L’aspetto più interessante di questa toolchain open-source è la sua coerenza con il modello concettuale introdotto finora. Ogni strumento corrisponde a una fase precisa del flusso di progettazione, e nessun passaggio è nascosto o automatizzato in modo opaco. Questo rende il processo più trasparente e, paradossalmente, più semplice da comprendere: invece di affidarsi a un ambiente chiuso che produce un risultato finale, il progettista può osservare come una descrizione HDL venga progressivamente trasformata in hardware reale. È proprio questa trasparenza che rende la combinazione iCE40UP5K + toolchain open-source particolarmente adatta a un percorso introduttivo serio, orientato non solo a “far funzionare qualcosa”, ma a capire perché e come funziona.
Prerequisiti e approccio
Quanto descritto finora chiarisce che la progettazione su FPGA richiede un approccio diverso rispetto allo sviluppo embedded tradizionale. Non sono necessarie competenze avanzate di elettronica digitale, ma è indispensabile una mentalità orientata all’hardware: concetti come clock, reset, sincronizzazione, latenza e parallelismo devono essere considerati parte integrante del progetto, non dettagli di implementazione. Allo stesso modo, è richiesta una minima dimestichezza con l’ambiente Linux e con il terminale, poiché il flusso di sviluppo FPGA si basa su strumenti che operano prevalentemente da linea di comando e producono output testuali e report tecnici da interpretare. Questa serie non è pensata per replicare un classico “blink” in stile Arduino, ma per accompagnare maker evoluti e progettisti embedded verso una comprensione concreta e spendibile delle FPGA, partendo da esempi semplici ma concettualmente corretti.
Un altro prerequisito fondamentale è la disponibilità di un ambiente di sviluppo Linux coerente e riproducibile. La scelta di utilizzare Ubuntu (o Kubuntu) come piattaforma di riferimento non è dettata da preferenze personali, ma da considerazioni pratiche: la toolchain open-source per FPGA è nativamente supportata su Linux e consente di lavorare in modo stabile e prevedibile. Tentare di replicare lo stesso flusso su altri sistemi operativi introduce complessità inutili e problemi di compatibilità che distolgono l’attenzione dagli aspetti realmente importanti della progettazione. Per questo motivo, nei prossimi articoli si farà riferimento a un ambiente Ubuntu 24.04 LTS, utilizzabile sia in modo nativo sia all’interno di una macchina virtuale. Questa soluzione permette di uniformare il contesto di lavoro e di seguire il percorso senza dipendere dalla configurazione specifica del sistema host. Nel caso non si avesse un ambiente Linux nativo, è possibile installare Ubuntu/Kubuntu su una macchina virtuale Virtualbox seguendo le istruzioni dell’articolo Come installare Ubuntu in macchina virtuale con VirtualBox su Windows e Linux
Nel prossimo articolo si passerà dalla teoria alla pratica: verrà mostrato come preparare l’ambiente di sviluppo su Ubuntu, installare la toolchain open-source e verificare il corretto funzionamento della board iCESugar con un primo progetto minimale. L’obiettivo non sarà solo “far lampeggiare un LED”, ma iniziare a osservare concretamente il percorso che porta da una descrizione HDL a un comportamento hardware reale, chiudendo il cerchio tra i concetti introdotti e la loro applicazione pratica.
Newsletter
Se vuoi essere aggiornato sui nuovi articoli, iscriviti alla newsletter. Prima dell’iscrizione alla newsletter leggi la pagina Privacy Policy (UE)
Se ti vuoi disiscrivere dalla newsletter clicca sul link che troverai nella mail della newsletter.
🔗 Seguici anche sui nostri canali social per non perdere nessun aggiornamento!
📢 Unisciti al nostro canale Telegram per ricevere aggiornamenti in tempo reale.
🐦 Seguici su Twitter per rimanere sempre informato sulle nostre novità.
Grazie per far parte della nostra community TechRM! 🚀