Introduzione
In questo secondo articolo portiamo la FPGA iCE40UP5K dal piano teorico a un flusso pratico completo su Ubuntu. Nell’articolo FPGA per progettisti embedded: cosa sono e perché usarle (iCE40UP5K) abbiamo costruito il modello mentale necessario per affrontare questo tema senza equivoci: una FPGA non è un “microcontrollore più potente”, ma un dispositivo riconfigurabile che permette di implementare logica digitale dedicata, con parallelismo reale, latenza prevedibile e un controllo molto più diretto sull’hardware. Quel passaggio teorico era indispensabile, perché senza una base concettuale solida si rischia di affrontare il flusso di sviluppo in modo meccanico, limitandosi a copiare comandi senza capire davvero cosa stia succedendo. In questo secondo articolo si passa invece dal modello mentale alla pratica: installazione della toolchain open-source, verifica dell’ambiente, simulazione del progetto, generazione del bitstream e programmazione reale della board iCESugar basata su Lattice iCE40UP5K. L’obiettivo non è proporre il solito “blink” da tutorial introduttivo, utile solo a dimostrare che il tool compila, ma impostare da subito un flusso di lavoro credibile, riproducibile e già abbastanza vicino a quello che si userebbe in un contesto embedded serio. Il progetto scelto è volutamente piccolo e semplice, ma non banale: usa Verilog reale, vincoli fisici reali, simulazione con testbench, sintesi, place and route e validazione su hardware tramite LED RGB e DIP switch onboard. Per uniformare l’ambiente ed evitare attriti inutili tra sistemi diversi, il riferimento operativo sarà Ubuntu 24.04, installato nativamente oppure in macchina virtuale; per chi parte da zero o vuole riallinearsi con l’ambiente usato in questa serie, il prerequisito abilitante restal’articolo Come installare Ubuntu in macchina virtuale con VirtualBox su Windows e Linux, che può essere considerato il punto di partenza infrastrutturale del lavoro che segue.
Hardware e ambiente di riferimento
Per la parte hardware, in questa serie useremo come riferimento la iCESugar v1.5, una board compatta e semplice ma adatta a realizzare progetti di una certa complessità, basata su Lattice iCE40UP5K. La scelta non è casuale: da un lato si tratta di una FPGA abbastanza accessibile come costo e complessità, dall’altro è perfettamente adatta a costruire un percorso serio con toolchain completamente open-source, senza dipendere da ambienti proprietari grossi e chiusi o da flussi di sviluppo complessi e poco chiari.
L’ho acquistata su AliExpress, ma è reperibile anche sulle pagine del progetto MuseLab / iCESugar online.
La board integra già ciò che serve per iniziare a lavorare in modo concreto (LED RGB, DIP switch, interfaccia USB e programmazione tramite iCELink) e permette quindi di concentrarsi subito sul flusso FPGA vero e proprio, invece di perdersi in cablaggi, adattatori o componenti esterni aggiuntivi. Quanto all’ambiente software, il riferimento ufficiale di questa serie sarà Ubuntu/Kubuntu 24.04 LTS, sia in installazione nativa sia in macchina virtuale. La scelta di Linux, e in particolare di Ubuntu, nasce dal fatto che, nel mondo degli strumenti open per FPGA, Linux resta l’ambiente più lineare, prevedibile e riproducibile. Su Ubuntu i pacchetti necessari sono disponibili con nomi chiari, le dipendenze si gestiscono senza troppe sorprese, il collegamento USB alla board è più trasparente e l’intero flusso (simulazione, sintesi, place and route, generazione del bitstream) si lascia automatizzare meglio. In teoria parte di questo lavoro si potrebbe fare anche altrove, ma nella pratica aumenterebbero difficoltà, eccezioni e problemi secondari che in una serie introduttiva non aggiungono alcun valore didattico. Per questo motivo qui si preferisce fissare un ambiente unico e ben controllato: meno improvvisazione, meno incompatibilità, più concentrazione sul progetto vero e proprio. Chi non dispone già di una macchina Linux può seguire senza problemi la strada della virtualizzazione, che per questo tipo di attività è più che sufficiente; il riferimento operativo resta quindi l’articolo citato nella introduzioneCome installare Ubuntu in macchina virtuale con VirtualBox su Windows e Linux, da considerare il prerequisito pratico su cui poggia tutto il lavoro che segue.
L’immagine seguente mostra la board e la posizone del DIP switch:

Cosa installeremo per il nostro progetto sulla FPGA iCE40UP5K
In questa fase non installeremo un generico “ambiente FPGA”, ma un insieme molto preciso di strumenti, ciascuno con un ruolo chiaro nel flusso di lavoro. Yosys si occuperà della sintesi del codice Verilog, cioè della traduzione della descrizione hardware in una rappresentazione logica compatibile con la famiglia iCE40. nextpnr-ice40 eseguirà invece il place and route, quindi il posizionamento della logica sulle risorse fisiche della FPGA e il collegamento dei vari blocchi interni; su Ubuntu 24.04 il pacchetto pratico da installare è nextpnr-ice40-qt, che include anche la GUI ma continua a fornire l’eseguibile nextpnr-ice40. Il pacchetto fpga-icestorm fornirà poi gli strumenti di supporto come icepack e icetime, necessari rispettivamente per generare il bitstream finale e ottenere indicazioni sul timing. Per la simulazione useremo Icarus Verilog, mentre GTKWave servirà per osservare graficamente le waveform e verificare il comportamento del progetto prima ancora di programmarlo sulla board. Insieme, questi strumenti coprono già un flusso completo e realistico: simulazione, sintesi, implementazione fisica e generazione del file da caricare sulla FPGA. Per non costringere il lettore a ricostruire tutto a mano, nel pacchetto ZIP distribuito con l’articolo saranno già presenti anche due script di supporto, scripts/setup_ubuntu_fpga_toolchain.sh e scripts/check_fpga_env.sh, pensati per automatizzare rispettivamente l’installazione e la verifica dell’ambiente. Gli script sono stati provati realmente su una macchina virtuale ripulita dai principali tool FPGA, reinstallando da zero la toolchain e verificando poi il corretto funzionamento dell’ambiente prima di ricostruire il progetto. In altre parole, il lettore non riceverà solo una cartella con dei sorgenti Verilog, ma un piccolo pacchetto di lavoro già organizzato per essere eseguito, controllato e riprodotto con il minor attrito possibile.
Il link seguente consente di scaricare il progetto completo:
Installazione toolchain su Ubuntu 24.04
Una volta scaricato il pacchetto ZIP allegato all’articolo, il passo successivo consiste nel predisporre una piccola area di lavoro locale e lanciare gli script inclusi nel progetto. L’idea è volutamente semplice: niente repository Git, niente clone, niente branch, niente dipendenze da strumenti ulteriori oltre a quelli strettamente necessari. Il lettore dovrà solo aprire una shell, creare una cartella dedicata ai progetti, decomprimere l’archivio scaricato, entrare nella directory del progetto ed eseguire i due script forniti: il primo installerà la toolchain, il secondo verificherà che tutti i componenti attesi siano realmente disponibili e funzionanti. In questo modo il flusso resta lineare, riproducibile e soprattutto adatto anche a chi non usa Git abitualmente ma vuole comunque lavorare in modo ordinato.
Di seguito i passaggi operativi. Si assume, per comodità, che il file ZIP scaricato si trovi nella cartella dei download dell’utente; se il percorso reale fosse diverso, sarà sufficiente adattare il comando unzip.
Aprire il terminale e creare la cartella di lavoro
cd ~
mkdir -p progetti
Assicurarsi di avere unzip
sudo apt update
sudo apt install -y unzip
Spostare nella directory di lavoro e decomprimere il pacchetto scaricato
cd ~
cd Scaricati
mv 02_setup_blink_rgb.zip ~/progetti/
cd progetti
unzip ~/progetti/02_setup_blink_rgb.zip
Se il sistema non usa Scaricati come nome della cartella dei download, basterà sostituire il percorso con quello corretto.
Entrare nella directory del progetto
Dopo l’estrazione, ci si sposta nella cartella principale del pacchetto e si controlla il contenuto:
cd 02_setup_blink_rgb
ls
Rendere eseguibili gli script
In genere gli script mantengono già i permessi corretti, ma questo passaggio forza gli script ad essere eseguibili:
chmod +x scripts/*.sh
Lanciare lo script di installazione della toolchain
Questo script installerà i pacchetti necessari per simulazione, sintesi, place and route e generazione del bitstream:
./scripts/setup_ubuntu_fpga_toolchain.sh
Verificare l’ambiente con lo script di check
Una volta completata l’installazione, il secondo script controllerà che i tool attesi siano realmente presenti e raggiungibili:
./scripts/check_fpga_env.sh
L’output dello script dovrebbe confermare la presenza dei tool installati con lo script precedente. Un esempio di tale output può essere visto nella seguente immagine. Questo output è stato catturato con la board collegata alla USB:
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$ ./scripts/check_fpga_env.sh
[INFO] Checking FPGA development environment...
[OK] Yosys: /usr/bin/yosys
[OK] nextpnr-ice40: /usr/bin/nextpnr-ice40
[OK] IceStorm icepack: /usr/bin/icepack
[OK] IceStorm icetime: /usr/bin/icetime
[OK] Icarus Verilog: /usr/bin/iverilog
[OK] GTKWave: /usr/bin/gtkwave
[INFO] Tool versions:
--------------------------------------------------
Yosys 0.33 (git sha1 2584903a060)
"nextpnr-ice40" -- Next Generation Place and Route (Version 0.6-3build5)
Icarus Verilog version 12.0 (stable) ()
GTKWave Analyzer v3.3.116 (w)1999-2023 BSI
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
--------------------------------------------------
[INFO] USB devices matching MuseLab / iCELink / CMSIS-DAP:
Bus 001 Device 003: ID 1d50:602b OpenMoko, Inc. FPGALink
[OK] Matching USB FPGA/debug device detected.
[INFO] Serial ACM devices:
/dev/ttyACM0
[OK] Serial ACM device detected.
[INFO] Mounted removable media:
/dev/sda on /media/ric/iCELink type vfat (rw,nosuid,nodev,relatime,uid=1000,gid=1000,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,showexec,utf8,flush,errors=remount-ro,uhelper=udisks2)
[OK] Mounted removable media detected.
[OK] Environment check completed successfully.
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$
Di seguito è invece visibile l’output nel caso la board non fosse connessa alla porta USB:
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$ ./scripts/check_fpga_env.sh
[INFO] Checking FPGA development environment...
[OK] Yosys: /usr/bin/yosys
[OK] nextpnr-ice40: /usr/bin/nextpnr-ice40
[OK] IceStorm icepack: /usr/bin/icepack
[OK] IceStorm icetime: /usr/bin/icetime
[OK] Icarus Verilog: /usr/bin/iverilog
[OK] GTKWave: /usr/bin/gtkwave
[INFO] Tool versions:
--------------------------------------------------
Yosys 0.33 (git sha1 2584903a060)
"nextpnr-ice40" -- Next Generation Place and Route (Version 0.6-3build5)
Icarus Verilog version 12.0 (stable) ()
GTKWave Analyzer v3.3.116 (w)1999-2023 BSI
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
--------------------------------------------------
[INFO] USB devices matching MuseLab / iCELink / CMSIS-DAP:
[WARN] No matching USB FPGA/debug device found.
[INFO] Serial ACM devices:
[WARN] No /dev/ttyACM device found.
[INFO] Mounted removable media:
[WARN] No mounted removable FPGA/debug media found.
[OK] Environment check completed successfully.
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$
Come puoi vedere è segnalata l’assenza della board ma non è motivo di preoccupazione: lo script segnala la corretta installazione dei tool necessari.
Verifica che la board venga vista correttamente
A questo punto la toolchain è installata, ma manca ancora un passaggio fondamentale: verificare che la board venga effettivamente vista dal sistema operativo. È un controllo banale solo in apparenza, perché rappresenta il primo punto di contatto reale tra l’ambiente software e l’hardware. Nel caso della iCESugar, collegando la board via USB-C non ci si aspetta un singolo dispositivo, ma tre elementi distinti e tutti utili: un’interfaccia di debug di tipo CMSIS-DAP, una seriale virtuale esposta come ttyACM0 e un disco virtuale USB montato come iCELink o, in alcuni casi, MBED. Se uno di questi pezzi manca, conviene fermarsi subito e capire dov’è il problema, invece di andare avanti a tentoni con simulazione e bitstream. Se si sta lavorando in macchina virtuale, questo controllo è ancora più importante, perché conferma che il pass-through USB verso la VM sta funzionando davvero.
Il modo più pulito per fare questa verifica è partire con il monitoraggio del log del kernel, poi collegare la board e controllare in sequenza i dispositivi USB, i blocchi disco e le interfacce seriali.
Per prima cosa si apre un terminale e si mette in ascolto dmesg (potrebbe essere necessario lanciare dmesg con i privilegi di amministratore usando sudo):
sudo dmesg -w
A questo punto si collega la iCESugar alla porta USB-C. Se tutto va bene, nel log compariranno righe che indicano il riconoscimento del dispositivo USB, dell’interfaccia CMSIS-DAP, della seriale virtuale e del mass storage. Nel nostro caso, ad esempio, il sistema ha riconosciuto la board come dispositivo compatibile con DAPLink CMSIS-DAP, ha creato la seriale ttyACM0 e ha esposto un disco virtuale USB.
L’immagine seguente mostra l’output del comando sul mio sistema:
[ 1206.348387] usb 1-3: new full-speed USB device number 4 using xhci_hcd
[ 1206.577378] usb 1-3: New USB device found, idVendor=1d50, idProduct=602b, bcdDevice= 1.00
[ 1206.577382] usb 1-3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 1206.577383] usb 1-3: Product: DAPLink CMSIS-DAP
[ 1206.577384] usb 1-3: Manufacturer: MuseLab
[ 1206.577385] usb 1-3: SerialNumber: 07000001005100594300000a4e575737a5a5a5a597969908
[ 1206.579714] usb-storage 1-3:1.0: USB Mass Storage device detected
[ 1206.580154] scsi host6: usb-storage 1-3:1.0
[ 1206.581221] cdc_acm 1-3:1.1: ttyACM0: USB ACM device
[ 1206.583301] hid-generic 0003:1D50:602B.0003: hiddev0,hidraw1: USB HID v1.00 Device [MuseLab DAPLink CMSIS-DAP] on usb-0000:02:00.0-3/input3
[ 1207.604713] scsi 6:0:0:0: Direct-Access MBED VFS 0.1 PQ: 0 ANSI: 2
[ 1207.605103] sd 6:0:0:0: Attached scsi generic sg1 type 0
[ 1207.609512] sd 6:0:0:0: [sda] 131200 512-byte logical blocks: (67.2 MB/64.1 MiB)
[ 1207.610318] sd 6:0:0:0: [sda] Write Protect is off
[ 1207.610322] sd 6:0:0:0: [sda] Mode Sense: 03 00 00 00
[ 1207.610932] sd 6:0:0:0: [sda] No Caching mode page found
[ 1207.610934] sd 6:0:0:0: [sda] Assuming drive cache: write through
[ 1207.716797] sda:
[ 1207.716830] sd 6:0:0:0: [sda] Attached SCSI removable disk
Una volta osservato il log, si può interrompere dmesg con Ctrl+C e passare ai controlli puntuali. Il primo è l’elenco dei dispositivi USB:
lsusb
Qui ci si aspetta di vedere una riga coerente con la board, tipicamente qualcosa che richiama CMSIS-DAP, FPGALink, MuseLab oppure OpenMoko. Il nome preciso può variare leggermente, ma il punto importante è che il dispositivo compaia davvero nell’elenco.
L’immagine seguente mostra l’output del comando sul mio sistema:
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 0627:0001 Adomax Technology Co., Ltd QEMU Tablet
Bus 001 Device 004: ID 1d50:602b OpenMoko, Inc. FPGALink
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$
Il secondo controllo riguarda le periferiche a blocchi e il mount del disco virtuale:
lsblk
Se il collegamento è corretto, oltre al disco principale della macchina comparirà anche un piccolo dispositivo rimovibile, che nel nostro caso viene montato automaticamente come disco virtuale iCELink. In ambiente desktop Ubuntu/Kubuntu il mount avviene spesso sotto /media/<utente>/iCELink.
L’immagine seguente mostra l’output del comando sul mio sistema:
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 4K 1 loop /snap/bare/5
loop1 7:1 0 74M 1 loop /snap/core22/2292
loop2 7:2 0 251,7M 1 loop /snap/firefox/7766
loop3 7:3 0 531,4M 1 loop /snap/gnome-42-2204/247
loop4 7:4 0 18,5M 1 loop /snap/firmware-updater/210
loop5 7:5 0 91,7M 1 loop /snap/gtk-common-themes/1535
loop6 7:6 0 10,8M 1 loop /snap/snap-store/1270
loop7 7:7 0 48,1M 1 loop /snap/snapd/25935
loop8 7:8 0 576K 1 loop /snap/snapd-desktop-integration/343
loop9 7:9 0 48,4M 1 loop /snap/snapd/26382
loop10 7:10 0 226,6M 1 loop /snap/thunderbird/959
loop11 7:11 0 227,1M 1 loop /snap/thunderbird/1040
sda 8:0 1 64,1M 0 disk /media/ric/iCELink
sr0 11:0 1 1024M 0 rom
vda 253:0 0 80G 0 disk
\u251c\u2500vda1 253:1 0 1M 0 part
\u2514\u2500vda2 253:2 0 80G 0 part /
ric@ric-Standard-PC-Q35-ICH9-2009:~/progetti/fpgablogprojects$
Per una verifica ancora più esplicita si può usare:
mount | grep -Ei 'iCELink|MBED|/media/'
Il terzo controllo riguarda la seriale virtuale. Per verificarne la presenza basta eseguire:
ls /dev/ttyACM*
Se tutto è andato bene, il sistema dovrebbe restituire almeno:
/dev/ttyACM0
A questo punto la verifica può considerarsi riuscita: la board è stata riconosciuta dal sistema come dispositivo USB complesso, con interfaccia di debug, seriale virtuale e disco montato. Per una conferma anche visiva, è utile aprire il file manager e controllare che tra i dispositivi compaia il disco iCELink. È proprio su questo disco virtuale che, più avanti, verrà copiato il bitstream generato dal progetto.
Struttura del progetto nella cartella
Prima di entrare nel codice vero e proprio, conviene chiarire come è organizzato il progetto sul disco. Anche se il lettore scaricherà un archivio ZIP e non un repository Git da clonare, la struttura interna resta quella tipica di un progetto ordinato e riproducibile: ogni cartella ha uno scopo preciso e serve a separare in modo pulito il codice sorgente, la simulazione, i vincoli fisici e i materiali di supporto. In particolare, la cartella 02_setup_blink_rgb/ contiene il primo progetto pratico della serie. Al suo interno:
src/ raccoglie i sorgenti Verilog sintetizzabili, cioè il modulo reale che verrà trasformato in logica per la FPGA;
sim/ contiene invece il testbench usato per pilotare gli ingressi in simulazione e osservare il comportamento del circuito in modo controllato;
constraints/ ospita il file .pcf, fondamentale per associare i nomi logici usati nel codice ai pin fisici della board;
docs/ raccoglie note, screenshot e materiale tecnico utilizzato sia durante lo sviluppo sia per la scrittura di questo articolo;
il Makefile funge da punto di ingresso del flusso operativo, permettendo di lanciare simulazione, sintesi, place and route e generazione del bitstream con comandi compatti e leggibili.
A livello superiore, la cartella scripts/ contiene invece gli script per installare e verificare l’ambiente di lavoro. Questa organizzazione non è un dettaglio estetico: anche in un progetto piccolo come questo, avere una struttura chiara significa rendere il flusso più leggibile, più manutenibile e soprattutto più facile da ripercorrere in seguito.
Il primo progetto: RGB blink con DIP switch
Il primo progetto pratico della serie ha un obiettivo molto semplice da capire, ma abbastanza ricco da toccare già i concetti giusti: far lampeggiare il LED RGB onboard della iCESugar e usare i DIP switch presenti sulla board per selezionare il colore. Qui entrano già in gioco diversi elementi tipici di un flusso FPGA reale: una sorgente di clock interna alla FPGA, una logica sequenziale basata su contatore, una logica combinatoria per la selezione del colore, la gestione di segnali attivi bassi e la distinzione tra comportamento di simulazione e comportamento su hardware reale.
Top module e I/O
Il punto di partenza è il modulo top-level, cioè l’unità principale che descrive gli ingressi e le uscite del progetto. Qui compaiono quattro ingressi corrispondenti ai DIP switch e tre uscite corrispondenti ai tre canali del LED RGB. In simulazione viene inoltre aggiunto un clock esterno clk_sim, mentre nella versione reale il clock verrà ricavato dall’oscillatore interno della FPGA.
module top (
`ifdef SIM
input wire clk_sim,
`endif
input wire sw1_n,
input wire sw2_n,
input wire sw3_n,
input wire sw4_n,
output wire rgb_b_n,
output wire rgb_r_n,
output wire rgb_g_n
);
La nomenclatura scelta non è casuale. Il suffisso _n indica che quei segnali sono attivi bassi: vale sia per gli switch sia per le uscite del LED RGB. È una convenzione molto utile, perché rende subito visibile, già leggendo il codice, che un valore logico basso corrisponde all’attivazione della funzione.
Clock selection: hardware reale e simulazione
Una FPGA ha bisogno di un clock per far avanzare la logica sequenziale. In questo progetto si è scelto di usare, nella versione reale, l’oscillatore interno ad alta frequenza della iCE40UP5K, evitando così di introdurre subito un clock esterno come ulteriore variabile. In simulazione, invece, l’oscillatore interno non viene usato: il clock è fornito dal testbench, che lo genera artificialmente.
wire clk;
`ifdef SIM
assign clk = clk_sim;
`else
SB_HFOSC #(
.CLKHF_DIV("0b10")
) u_hfosc (
.CLKHFPU(1'b1),
.CLKHFEN(1'b1),
.CLKHF(clk)
);
`endif
Questa scelta ha due vantaggi. Il primo è pratico: in hardware il progetto è autosufficiente e non richiede ulteriori connessioni. Il secondo è metodologico: in simulazione si evita di dipendere dal modello di una primitive vendor-specific, e si lascia al testbench il compito di pilotare il clock in modo controllato. Il divisore “0b10” configura l’oscillatore interno a 12 MHz, frequenza più che sufficiente per un semplice blink e già compatibile con il resto del flusso.
Counter e blink
Una volta disponibile il clock, il modo più semplice per ottenere un lampeggio visibile è usare un contatore che incrementa a ogni fronte di salita. Prelevando uno dei bit più alti del contatore si ottiene un segnale che commuta molto più lentamente del clock originale, e quindi adatto a pilotare un LED visibile a occhio nudo.
reg [23:0] counter = 24'd0;
always @(posedge clk) begin
counter <= counter + 24'd1;
end
Il segnale di blink non viene ricavato sempre dallo stesso bit. In hardware si usa un bit alto del contatore, in modo da avere un lampeggio lento; in simulazione si usa invece un bit molto più basso, per evitare di dover simulare tempi troppo lunghi solo per vedere il LED accendersi e spegnersi nelle waveform.
`ifdef SIM
wire blink = counter[3];
`else
wire blink = counter[23];
`endif
In hardware serve un blink lento e visibile; in GTKWave, invece, serve un blink abbastanza veloce da poter essere osservato chiaramente senza zoomare su intervalli troppo lunghi di tempo. Il comportamento logico del progetto resta lo stesso; cambia solo la scala temporale con cui lo si osserva.
Gestione degli switch attivi bassi
I quattro DIP switch onboard della iCESugar sono collegati in modo attivo basso. Questo significa che, quando uno switch è in posizione attiva, il segnale letto dalla FPGA vale 0 e non 1. Per evitare di scrivere tutta la logica successiva ragionando “al contrario”, conviene fare subito una conversione interna e introdurre quattro segnali attivi alti.
wire sw1 = ~sw1_n;
wire sw2 = ~sw2_n;
wire sw3 = ~sw3_n;
wire sw4 = ~sw4_n;
Senza questa normalizzazione il codice combinatorio che segue diventerebbe meno leggibile e più soggetto a errori. In generale, quando si può, conviene convertire subito la polarità dei segnali esterni e lavorare internamente con una semantica più naturale.
Assegnazione dei colori
A questo punto il progetto deve decidere quali canali del LED RGB attivare in base allo switch premuto. La selezione è implementata con un blocco combinatorio molto semplice: a ogni combinazione corrisponde una scelta dei tre segnali interni led_r_on, led_g_on e led_b_on.
always @(*) begin
if (sw4) begin
led_r_on = 1'b1;
led_g_on = 1'b1;
led_b_on = 1'b1;
end else if (sw1) begin
led_r_on = 1'b1;
led_g_on = 1'b0;
led_b_on = 1'b0;
end else if (sw2) begin
led_r_on = 1'b0;
led_g_on = 1'b1;
led_b_on = 1'b0;
end else if (sw3) begin
led_r_on = 1'b0;
led_g_on = 1'b0;
led_b_on = 1'b1;
end else begin
led_r_on = 1'b1;
led_g_on = 1'b1;
led_b_on = 1'b1;
end
end
Il comportamento risultante è il seguente:
- SW1 seleziona il rosso
- SW2 seleziona il verde
- SW3 seleziona il blu
- SW4 seleziona il bianco
- in assenza di switch attivi, il colore di default è ancora il bianco
La scelta del bianco come default è utile perché rende immediatamente visibile che il progetto è vivo anche senza toccare nulla. Inoltre, avendo sia il caso SW4 sia il caso di default impostati a bianco, si ottiene un comportamento molto facile da verificare anche nei primi test su hardware.
È utile soffermarsi un momento sul significato dei due blocchi always, perché qui emerge in modo molto chiaro una differenza fondamentale tra descrizione hardware e programmazione tradizionale. Nel progetto compaiono infatti due costrutti diversi:
always @(posedge clk) begin
counter <= counter + 24'd1;
end
e
always @(*) begin
...
end
Anche se a colpo d’occhio possono sembrare semplicemente “due pezzi di codice”, in realtà descrivono due tipi di logica profondamente diversi. Il blocco always @(posedge clk) descrive una logica sequenziale sincrona: viene aggiornato soltanto al fronte di salita del clock e, nel caso specifico, corrisponde a un insieme di flip-flop che memorizzano il valore del contatore e lo aggiornano a ogni tick. Il blocco always @(*), invece, descrive una logica combinatoria: il suo risultato dipende istantaneamente dagli ingressi che legge, senza aspettare il clock e senza memorizzare uno stato interno. In questo progetto, quel blocco serve a decidere quali canali del LED RGB debbano essere considerati attivi in funzione degli switch.
La differenza importante, però, non è solo “temporizzato contro non temporizzato”. La differenza davvero cruciale è che questi due blocchi non vengono eseguiti in sequenza, come accadrebbe in un programma C che scorre istruzione dopo istruzione. In hardware, i due blocchi corrispondono a porzioni di circuito che esistono contemporaneamente all’interno della FPGA e lavorano in parallelo. Il contatore evolve a ogni fronte di clock, mentre la logica combinatoria osserva continuamente i segnali di ingresso e aggiorna le uscite in funzione del loro valore corrente. In altre parole, non c’è una CPU che prima aggiorna il contatore e poi “chiama” la selezione colore: c’è un circuito sequenziale che produce counter, un circuito combinatorio che usa gli switch per generare led_r_on, led_g_on e led_b_on, e infine una logica di assegnazione che combina tutto con blink per pilotare il LED. Questo è uno dei primi esempi concreti in cui si vede bene perché Verilog non sia un linguaggio di programmazione nel senso classico, ma un linguaggio di descrizione dell’hardware.
Quindi Il blocco always @(posedge clk) descrive elementi di memoria sincronizzati dal clock; il blocco always @(*) descrive invece pura logica combinatoria, cioè una rete di funzioni logiche senza stato.
LED attivo basso
Anche il LED RGB onboard è attivo basso. Questo significa che il canale rosso, verde o blu si accende quando l’uscita corrispondente vale 0, non 1. Di conseguenza, il segnale finale da mandare ai pin della FPGA non può essere semplicemente led_r_on, led_g_on o led_b_on, ma deve essere invertito e combinato con il segnale di blink.
La logica è semplice:
- led_r_on, led_g_on, led_b_on dicono quale colore si vuole selezionare
- blink dice se, in quell’istante, il LED deve essere acceso o spento
- la negazione finale serve perché l’hardware reale del LED è attivo basso
In pratica, se un colore è selezionato e blink vale 1, l’uscita finale diventa 0 e quindi il canale si accende. Se invece blink vale 0, l’uscita torna a 1 e il canale si spegne. Questo è esattamente il motivo per cui, nelle waveform di simulazione, i segnali rgb_r_n, rgb_g_n e rgb_b_n vanno interpretati “al contrario”: uno zero non indica spegnimento, ma attivazione del relativo canale.
Il file .pcf: perché i nomi diventano pin fisici
Se il file Verilog descrive che cosa deve fare il circuito, il file .pcf descrive invece dove i segnali devono andare a finire sulla FPGA reale. Questo passaggio è essenziale, perché i nomi usati nel modulo top-level (rgb_r_n, rgb_g_n, rgb_b_n, sw1_n, sw2_n, sw3_n, sw4_n) da soli non hanno alcun significato fisico: sono solo etichette logiche. È il file dei vincoli che li collega ai pin reali del package della FPGA montata sulla iCESugar. Nel nostro progetto, il file constraints/icesugar_v15.pcf associa i tre canali del LED RGB ai pin 39, 40 e 41, e i quattro DIP switch ai pin 18, 19, 20 e 21.
Anche qui il suffisso _n mantiene il significato già visto nel codice: LED e switch sono attivi bassi, quindi il nome del segnale riflette esplicitamente la loro polarità elettrica. Un dettaglio importante è che i nomi usati nel .pcf devono combaciare esattamente con quelli dichiarati nel modulo top-level Verilog: se il nome non coincide, il vincolo non si applica al segnale giusto e il progetto può fallire o, peggio, compilare con connessioni sbagliate.
Nel nostro caso, il contenuto utile del file è questo:
set_io rgb_b_n 39
set_io rgb_r_n 40
set_io rgb_g_n 41
set_io sw1_n 18
set_io sw2_n 19
set_io sw3_n 20
set_io sw4_n 21
Questa sezione è uno dei primi punti in cui si vede in modo molto concreto il passaggio dalla descrizione logica all’hardware fisico. Nel codice si parla di rgb_r_n; nel .pcf si dice che quel segnale deve uscire dal pin 40 della FPGA; dopo place and route, i tool lo associano a una risorsa fisica reale del chip. Non è ancora necessario fare una “analisi grafica sofisticata” del placement, ma è utile mostrare almeno una prova tangibile del fatto che il collegamento esiste davvero. Per questo motivo ha senso usare la GUI di nextpnr in modo molto mirato: non per fare reverse engineering del routing, ma per individuare almeno un BEL fisico associato a uno degli I/O vincolati, ad esempio quello del canale rosso del LED. Vedremo questo passaggio nel prossimo paragrafo.
Build del progetto e simulazione con testbench e GTKWave
Prima di programmare la board vera e propria, conviene completare la procedura anche lato simulazione e build completa. Qui si vede bene una distinzione fondamentale: il testbench non è hardware reale, ma un piccolo ambiente artificiale che serve a stimolare il modulo top in modo controllato. Nel nostro caso il file tb_top.v genera un clock di simulazione, tiene inizialmente tutti gli switch inattivi e poi li attiva uno alla volta nel tempo, simulando quindi ciò che l’utente farebbe fisicamente sulla board con i DIP switch. Questo permette di osservare in anticipo il comportamento del circuito senza dipendere subito dall’hardware e, soprattutto, di verificare che il progetto reagisca agli ingressi come previsto. In simulazione gli switch non li muove la mano dell’utente, ma il testbench; in hardware accade l’opposto, cioè è l’utente a fornire gli stimoli e il circuito risponde in tempo reale.
Il modo più semplice per affrontare questa fase è partire da una build pulita. Dalla cartella del progetto si può quindi eseguire:
make clean
Questo comando elimina la directory build/ e costringe il flusso a rigenerare da zero tutti gli artefatti del progetto. A questo punto si può compilare la simulazione:
make sim
Questo target usa Icarus Verilog per compilare il sorgente reale src/top.v insieme al testbench sim/tb_top.v, producendo il file eseguibile della simulazione build/top.vvp. Subito dopo si può eseguire la simulazione vera e propria:
make run-sim
Il comando lancia vvp e genera il file waveform build/top.vcd, che può essere aperto con GTKWave:
gtkwave build/top.vcd
Una volta aperto GTKWave, i segnali più interessanti da osservare sono:
- clk_sim
- blink
- sw1_n, sw2_n, sw3_n, sw4_n
- rgb_r_n, rgb_g_n, rgb_b_n
Volendo, è utile aggiungere anche uut.counter e i segnali interni led_r_on, led_g_on, led_b_on, perché rendono ancora più chiaro il legame tra selezione del colore e uscite finali. In particolare, ricordando che LED e switch sono attivi bassi, la lettura delle waveform va fatta con attenzione: quando sw1_n scende a zero, significa che SW1 è attivo; quando rgb_r_n scende a zero, significa che il canale rosso è acceso. È uno di quei casi in cui la simulazione aiuta molto proprio perché permette di vedere con calma la logica senza il rumore della prova su banco.
Potrebbe rendersi necessario “restringere” il grafico della simulazione su GTKWave. L’immagine seguente mostra la simulazione sul mio setup:

Dopo la simulazione, si può lanciare la build completa del progetto:
make
In questo caso il Makefile esegue l’intero flusso: sintesi con Yosys, place and route con nextpnr-ice40, generazione del bitstream con icepack e report di timing con icetime. Il risultato finale è la generazione dei principali file di lavoro:
- build/top.json
- build/top.asc
- build/top.bin
- build/top.rpt
Il file .json rappresenta il risultato della sintesi, il .asc è l’output del place and route, il .bin è il bitstream vero e proprio da caricare sulla FPGA, mentre il .rpt contiene il report di timing. In altre parole, con un singolo make si passa dalla descrizione Verilog a un file realmente programmabile sulla board. Anche questo è un punto importante da sottolineare: il flusso non è stato solo installato, ma è stato verificato davvero fino in fondo, prima in simulazione e poi in implementazione completa.
Sintesi, place & route e report timing
Dopo aver verificato in simulazione che la logica si comporti come previsto, si può passare alla parte che distingue davvero un flusso FPGA serio da un semplice esercizio da simulatore: la trasformazione del progetto in una configurazione reale per il chip. In questa fase il codice Verilog non viene più soltanto “eseguito” in ambiente controllato, ma viene sintetizzato, mappato sulle risorse logiche della iCE40UP5K, collegato fisicamente all’interno del dispositivo e infine convertito nel bitstream che verrà programmato sulla board. Tutto questo, nel progetto, è raccolto nel target principale del Makefile, quindi dalla cartella 02_setup_blink_rgb/ è sufficiente eseguire:
make
Come abbiamo già visto nel paragrafo precedente, questo comando attiva l’intero flusso di implementazione. In particolare, Yosys esegue la sintesi del modulo Verilog e produce il file build/top.json, che rappresenta il progetto in una forma intermedia già mappata logicamente per la famiglia iCE40. Successivamente nextpnr-ice40 prende quel file, applica i vincoli del .pcf, esegue il place and route e genera build/top.asc, che contiene l’implementazione fisica del progetto sul dispositivo. A questo punto entra in gioco icepack, che converte il file .asc nel vero e proprio bitstream binario build/top.bin, cioè il file che verrà poi caricato sulla board. Infine icetime produce build/top.rpt, che contiene una stima utile del timing del progetto. In sintesi, dopo make, nella cartella build/ ci si aspetta di trovare almeno questi quattro file:
- build/top.json
- build/top.asc
- build/top.bin
- build/top.rpt
Quindi il progetto di questo articolo attraversa davvero tutte le fasi classiche di un flusso FPGA: sintesi, implementazione fisica, generazione del bitstream e analisi temporale.
Nel nostro caso, il progetto occupa una quantità molto piccola di risorse del chip, quindi il margine di crescita è enorme; inoltre il clock interno scelto per il progetto è di 12 MHz, mentre il report di implementazione mostra una frequenza massima ottenibile molto superiore, quindi il timing risulta ampiamente rispettato.
Volendo, dopo la build si può anche aprire nextpnr in modalità grafica per osservare il risultato del place and route:
nextpnr-ice40 \
--up5k \
--package sg48 \
--json build/top.json \
--pcf constraints/icesugar_v15.pcf \
--asc build/top.asc \
--gui
Questo comando non è necessario per generare il bitstream (la build è già completa) ma è molto utile per capire meglio che cosa abbia fatto il tool. In particolare:
- –up5k indica il dispositivo target,
- –package sg48 specifica il package fisico della FPGA montata sulla board,
- –json fornisce il risultato della sintesi,
- –pcf passa i vincoli di pin,
- –asc indica il file di output del place and route,
- –gui apre l’interfaccia grafica di ispezione.
Non bisogna aspettarsi un ambiente raffinato come i tool commerciali di fascia alta: la GUI di nextpnr è piuttosto spartana. Tuttavia, per questo progetto, ha un valore didattico concreto perché permette di localizzare almeno una risorsa fisica reale associata a un segnale del progetto. Per esempio, si può cercare un BEL corrispondente a uno degli I/O vincolati, come quello legato al canale rosso del LED RGB. In quel momento si sta osservando qualcosa di molto semplice ma molto importante: il segnale che nel codice si chiamava rgb_r_n, e che nel .pcf era stato associato a un pin fisico, è effettivamente finito in una risorsa reale del chip. Questo non trasforma nextpnr in un fantastico strumento di analisi grafica, ma fornisce una prova concreta del passaggio dalla descrizione logica all’implementazione fisica.
Vediamo nelle immagini seguenti il tool in funzione:


La schermata mostra la GUI di nextpnr dopo il place and route del progetto sulla Lattice iCE40UP5K. Si tratta di una rappresentazione delle risorse fisiche del chip. Nel riquadro centrale si vede una porzione della matrice della FPGA; il piccolo elemento evidenziato in arancione corrisponde al BEL selezionato, cioè una risorsa fisica reale del dispositivo. In questo caso è stato cercato e selezionato X5/Y31/io0, visibile sia nella casella di ricerca in alto a destra sia nell’elenco dei BEL. Il pannello delle proprietà, sempre a destra, conferma che si tratta di un blocco di tipo SB_IO, quindi di una risorsa di input/output della FPGA. Il significato pratico di questa schermata è molto semplice ma importante: un segnale definito nel codice Verilog e vincolato nel file .pcf non rimane un nome astratto, ma viene effettivamente associato a una posizione fisica precisa all’interno del chip. In conclusione la catena top.v → .pcf → place and route porta davvero a una risorsa hardware reale all’interno della FPGA.
Programmazione della board via iCELink
Arrivati a questo punto, il progetto non è più soltanto simulabile o sintetizzabile: esiste anche un bitstream reale pronto per essere caricato sulla FPGA. Nel caso della iCESugar, questa fase è particolarmente comoda perché non richiede, almeno per questo progetto, tool di programmazione separati o procedure complesse: la board espone infatti un disco virtuale USB chiamato iCELink, gestito dal debugger onboard, e la programmazione può avvenire in modo molto semplice copiando al suo interno il file build/top.bin. Questa modalità di caricamento è molto comoda perché si genera il bitstream, lo si copia sul dispositivo esposto via USB e, dopo pochi secondi, la FPGA inizia a eseguire la nuova configurazione.
Prima di programmare la board, conviene verificare ancora una volta che il disco virtuale sia effettivamente montato dal sistema. Questo controllo si può fare con:
mount | grep -Ei 'iCELink|MBED|/media/'
Se tutto è andato bene, si dovrebbe vedere un mountpoint simile a /media/<utente>/iCELink. A questo punto ci si porta nella cartella del progetto e si copia il bitstream appena generato:
cp build/top.bin /media/$USER/iCELink/
sync
Il comando sync serve semplicemente a forzare il flush dei dati su disco, evitando che la copia resti in cache per qualche istante. In alternativa, se si preferisce usare il file manager, si può anche trascinare manualmente build/top.bin dentro il disco virtuale iCELink. Dal punto di vista funzionale non cambia nulla. È però bene sapere che il disco virtuale non si comporta sempre come una normale chiavetta USB: anche dopo la copia, può continuare a mostrare i file standard come DETAILS.TXT, README.HTM o simili. Questo non significa che la programmazione sia fallita. Nel nostro caso, infatti, il test reale ha confermato che la board viene programmata correttamente anche se il contenuto visualizzato nel disco non cambia in modo intuitivo.
L’immagine seguente mostra l’aspetto della cartella del disco virtuale iCELink:

Una volta completata la copia, bisogna attendere qualche secondo e osservare il comportamento del LED RGB onboard. Se il bitstream è stato caricato correttamente, il progetto entra in funzione e risponde ai DIP switch come definito nel codice: SW1 seleziona il rosso lampeggiante, SW2 il verde, SW3 il blu, SW4 il bianco, mentre in assenza di switch attivi il comportamento di default è il bianco lampeggiante.
Per rendere la verifica ancora più solida, è stata effettuata anche una prova di persistenza: dopo aver programmato la board, la iCESugar è stata scollegata e ricollegata, verificando che il comportamento caricato restasse attivo anche dopo il power cycle. Questo punto è tutt’altro che secondario, perché conferma che non si sta solo eseguendo una configurazione temporanea “volatile”, ma che il percorso di programmazione via iCELink è effettivamente sufficiente, in questo contesto, a rendere il progetto disponibile anche al successivo riavvio della board.
Dopo la copia, quindi, il controllo finale non va fatto sul file manager ma sull’hardware: LED RGB e DIP switch devono comportarsi esattamente come definito nel progetto.
Il video seguente mostra la board in funzione:
Problemi incontrati e note utili
Come spesso accade quando si passa dalla teoria a un test reale, anche questo primo progetto ha fatto emergere alcuni dettagli pratici che vale la pena fissare subito, sia per evitare false partenze sia per risparmiare tempo a chi ripercorrerà gli stessi passi. Il primo riguarda la GUI di nextpnr: esiste, si avvia correttamente e per alcune verifiche puntuali può essere utile, ma non va sopravvalutata. Non è un ambiente raffinato come quelli dei tool commerciali più blasonati e, almeno su un progetto piccolo come questo, non offre una visualizzazione particolarmente leggibile del routing o dei percorsi critici. Dove invece torna utile è nella possibilità di localizzare una risorsa fisica concreta del chip, ad esempio un BEL associato a un I/O vincolato dal file .pcf; in questo caso mostra che un segnale logico del progetto finisce davvero in una posizione fisica precisa della FPGA. Un secondo punto importante riguarda icetime: durante il flusso può comparire un warning relativo all’oscillatore interno HFOSC, ma questo non va interpretato come un fallimento del progetto o come un problema strutturale del timing. Per questo semplice esempio, le informazioni temporali più utili e affidabili sono già quelle riportate direttamente da nextpnr, che mostrano chiaramente come il progetto soddisfi con largo margine il requisito dei 12 MHz. Sul fronte hardware, poi, c’è un piccolo dettaglio molto banale ma potenzialmente insidioso: i DIP switch della iCESugar sono davvero minuscoli e, sulla board appena arrivata, risultano anche protetti da una sottile pellicola. È facilissimo non notarli subito o non capire a colpo d’occhio se siano stati effettivamente mossi. Un’altra nota pratica riguarda il disco virtuale iCELink: pur essendo molto comodo per la programmazione, non si comporta esattamente come una normale chiavetta USB. Dopo la copia del bitstream, infatti, il contenuto visualizzato può continuare a mostrare i file standard del dispositivo, senza riflettere in modo intuitivo l’operazione appena eseguita; questo può dare l’impressione che la programmazione non sia avvenuta, quando invece la board viene aggiornata correttamente. Infine, sul lato ambiente software, c’è una piccola trappola specifica di Ubuntu 24.04: i pacchetti nextpnr-ice40 e nextpnr-ice40-qt risultano in conflitto, quindi per avere anche la GUI conviene installare direttamente nextpnr-ice40-qt, che continua comunque a fornire l’eseguibile nextpnr-ice40.
Conclusioni
In conclusione, questo secondo articolo ha permesso di trasformare un modello teorico in un flusso pratico completo e verificato. Si è visto anzitutto che lavorare con una FPGA significa costruire passo dopo passo una catena coerente: ambiente Linux preparato in modo controllato, toolchain open-source installata e verificata, board correttamente rilevata via USB, progetto organizzato in modo ordinato, simulazione prima dell’hardware, sintesi e place and route prima della programmazione reale. Anche un progetto piccolo come questo ha già mostrato alcuni concetti centrali del lavoro su FPGA: l’uso di un oscillatore interno, la distinzione tra logica sequenziale e combinatoria, la gestione di segnali attivi bassi, il ruolo dei vincoli fisici del file .pcf, e il fatto che i nomi scritti nel sorgente Verilog debbano poi finire in posizioni fisiche reali del chip. Si è inoltre imparato a leggere il progetto come una prima implementazione completa che attraversa simulazione, sintesi, implementazione fisica, generazione del bitstream e verifica su hardware.
In fase di preparazione di questo progetto gli script di installazione e di check sono stati verificati su un ambiente ripulito dai principali tool FPGA; il progetto è stato ricostruito da zero; la simulazione con GTKWave ha mostrato il comportamento atteso del testbench; la board è stata programmata tramite iCELink; il comportamento del LED RGB e dei DIP switch è stato confermato su hardware; la persistenza del bitstream dopo scollegamento e ricollegamento è stata verificata; perfino la sostituzione corretta del bitstream è stata controllata caricando temporaneamente una variante con comportamento visibilmente diverso e poi ripristinando la versione finale del progetto. In conclusione, una FPGA non è un oggetto misterioso da laboratorio specialistico, ma uno strumento affrontabile con ordine, rigore e una buona catena di lavoro.
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à.
🦋 Seguici su Bluesky se preferisci questa piattaforma
Grazie per far parte della nostra community TechRM! 🚀