Logica combinatoria in VHDL

Il percorso sulle logiche programmabili arriva a un punto in cui conviene fermarsi e guardare indietro. Nel corso di elettronica digitale abbiamo dedicato diversi video ai singoli componenti della logica combinatoria: codificatori, decodificatori, multiplexer, comparatori, sommatori. Ora che il VHDL è stato introdotto e che la dev board basata su Xilinx Spartan-6 ha finalmente una scheda di espansione con pulsanti, switch e LED, è il momento giusto per rivedere tutti questi elementi in un colpo solo, descrivendoli in codice invece che con porte logiche e circuiti integrati.

È un ripasso, ma con un cambio di prospettiva importante: lo stesso comportamento che un tempo si otteneva scegliendo il chip giusto della serie 74 diventa qui una manciata di righe di codice sintetizzabile, con un livello di astrazione che permette di concentrarsi sul cosa fa il circuito più che su come è fatto internamente.

La logica combinatoria in una frase

Un circuito combinatorio non ha memoria: lo stato delle uscite dipende soltanto dallo stato degli ingressi in quel momento. Se cambiano gli ingressi, le uscite si adeguano subito. Escludendo le porte logiche elementari, i componenti base della famiglia, quelli che nel mondo dei circuiti integrati corrispondono alla media scala di integrazione, sono codificatori e decodificatori, multiplexer e demultiplexer, comparatori, sommatori e sottrattori.

Proprio perché non c’è memoria, in VHDL queste strutture si descrivono in modo molto naturale con istruzioni case, catene di if/elsif o assegnazioni condizionali: ogni combinazione di ingressi produce un’uscita, e il compito del codice è solo quello di elencare la corrispondenza nel modo più chiaro possibile.

Circuit Deck: scrivere VHDL e vederlo prendere vita

Per costruire gli esempi passo passo nel video viene usato Circuit Deck, un’applicazione che ho sviluppato e pubblicato da poco sull’App Store per Mac, con una versione Windows in lavorazione. Permette di disegnare schemi elettrici, diagrammi temporali, tavole della verità e mappe di Karnaugh, e include un blocco VHDL che unisce una parte di codice e una parte di schematico con ingressi e uscite del componente.

Un chiarimento importante riguarda il tipo di simulazione. Circuit Deck non esegue una simulazione con delta cycle, quella precisa che si usa tipicamente prima di andare sull’FPGA fisico, ma una simulazione logica: è pensata per capire il funzionamento di un circuito, non per sostituire gli strumenti di sintesi. L’interprete accetta i tipi std_logic e std_logic_vector, oltre a signed e unsigned, senza bisogno di dichiarare le librerie.

Qualche dettaglio pratico che torna utile negli esempi:

  • quando una porta diventa un std_logic_vector, il collegamento nello schematico si trasforma automaticamente in un bus;
  • il formato dei dati su un bus si può impostare in esadecimale, decimale senza segno, decimale con segno o bit per bit;
  • in alternativa al bus, le porte possono essere esposte come bit separati, molto più comodo quando si vogliono attivare gli ingressi uno alla volta in simulazione;
  • dal componente si possono ricavare il diagramma temporale (con il calcolo automatico delle uscite per ogni passo) e la tavola della verità, anche in forma compatta con i mintermini;
  • gli errori di sintassi vengono segnalati e bloccano la simulazione, come un punto e virgola di troppo prima della parentesi di chiusura della dichiarazione delle porte.

Encoder 8-3: il caso più semplice con il case

Il codificatore ha tipicamente 2^N ingressi e N uscite: converte un segnale “uno su N” attivo in ingresso nella sua codifica binaria in uscita. Nell’encoder 8-3, se è attivo l’ingresso I2, l’uscita vale 010.

La descrizione VHDL usa un ingresso std_logic_vector(7 downto 0) e un’uscita std_logic_vector(2 downto 0), con un processo che contiene un case sull’ingresso: per ogni configurazione con un solo bit a 1 si assegna il corrispondente valore binario, mentre when others porta l’uscita a zero. Ne derivano due comportamenti da tenere presenti:

  • l’ingresso I0 attivo non è distinguibile dalla condizione “nessun ingresso attivo”, perché in entrambi i casi l’uscita è 000. Può essere un problema o no a seconda dell’applicazione;
  • se più ingressi sono attivi contemporaneamente, la configurazione ricade in others e l’uscita torna a zero.

In Circuit Deck il componente si costruisce scrivendo il codice e lasciando che il programma generi i terminali di ingresso e uscita, poi lo si verifica sia interattivamente sia con un diagramma temporale in cui gli ingressi vengono attivati in sequenza, compreso un passo con due ingressi a 1 per vedere l’uscita tornare a zero.

Lo stesso VHDL viene poi portato su ISE, sulla macchina Windows, per la prova sull’hardware reale. Gli ingressi sono gli otto pulsanti della scheda di espansione, le uscite tre LED della LED bar. C’è un solo adattamento rispetto alla versione simulata: i pulsanti sono collegati con pull-up verso VCC, quindi a pulsante non premuto arriva un 1 e a pulsante premuto uno 0. Nel case le costanti risultano quindi invertite, con gli zeri al posto degli uno, mentre il resto del codice è identico. Dopo la generazione del file di programmazione, Impact con il consueto boundary scan carica il bitstream nella RAM della FPGA, senza programmarlo in modo permanente, e premendo i pulsanti uno alla volta la LED bar mostra in binario la posizione del tasto premuto.

Encoder a priorità: meglio una catena di if

Il priority encoder risolve il problema degli ingressi multipli introducendo un ordine: quando più linee sono attive, vince quella con indice più alto, come avviene anche in molti encoder integrati. Con I1 e I4 attivi insieme, l’uscita è 100.

Descriverlo con un case significherebbe elencare tutte le 2^8, cioè 256, combinazioni possibili: un costrutto enorme e poco sensato. Molto più efficace una cascata di if/elsif: se I7 è a 1 l’uscita è 111, indipendentemente dagli altri ingressi; altrimenti, se I6 è a 1, l’uscita è 110, e così via scendendo fino all’ultimo ramo, in cui nessun ingresso significativo è attivo e l’uscita vale zero. La priorità è implicita nell’ordine stesso delle condizioni.

Anche qui resta l’ambiguità tra I0 attivo e nessun ingresso attivo. Una possibile soluzione è usare solo sette ingressi invece di otto, riservando lo zero alla condizione “niente premuto”.

Per la prova su FPGA, in questo caso, si usano gli switch al posto dei pulsanti, perché è molto più semplice tenerne attivi più di uno contemporaneamente. Con I6 alzato, attivare gli switch precedenti non cambia nulla; abbassando I6, entra in gioco lo switch attivo immediatamente successivo in ordine di priorità.

Decoder: l’operazione inversa

Il decodificatore fa il percorso opposto: riceve un codice binario e attiva l’unica uscita corrispondente. Con un ingresso a 2 bit si ottengono quattro uscite: 00 attiva Y0, 01 attiva Y1, e così via. Qui il case torna a essere la scelta naturale e il codice risulta molto leggibile.

In simulazione conviene rappresentare l’ingresso come bus in formato decimale e le uscite come bit separati: inserendo i valori da 0 a 3 nel diagramma temporale si vede l’uscita attiva spostarsi di conseguenza, in modo decisamente più intuitivo rispetto a una rappresentazione con ingressi separati.

Multiplexer e demultiplexer: cinque modi di scrivere la stessa cosa

Il multiplexer aggiunge un elemento rispetto a encoder e decoder: accanto agli ingressi di dato ci sono gli ingressi di selezione, e in uscita viene riportato il valore dell’ingresso selezionato. La tavola della verità generata da Circuit Deck lo rende evidente: fissata la selezione, solo uno degli ingressi di dato è rilevante, e tutti gli altri diventano indifferenti.

Per descriverlo in VHDL esistono diverse strade, e nel video ne vengono confrontate cinque:

  • un if sulle coppie di valori di selezione (00, 01, 10, 11);
  • if annidati, che prima esaminano il bit più significativo della selezione e poi quello meno significativo, forse meno leggibili;
  • un case sulla selezione, probabilmente la forma più chiara;
  • l’assegnazione condizionale con when ... else, più compatta;
  • varianti equivalenti di queste forme, più o meno leggibili a seconda della familiarità con il linguaggio.

Il demultiplexer fa il contrario: un solo dato in ingresso viene instradato sull’uscita indicata dalla selezione. Nell’esempio si usano assegnazioni del tipo “Y0 riceve D quando entrambi i bit di selezione sono a 0”, ripetute per ciascuna uscita.

Messi in coppia con gli stessi segnali di selezione, multiplexer e demultiplexer diventano un modo per trasformare un segnale parallelo in un flusso seriale su una sola linea e poi riconvertirlo in parallelo: un’idea utile quando servono meno conduttori o quando un collegamento parallelo risulta troppo sensibile ai disturbi.

Il comparatore a 4 bit

Il comparatore confronta due valori A e B e attiva una delle tre uscite: uguale, maggiore (da intendere come A maggiore di B) e minore. Con ingressi a 4 bit si arriva fino a 15, il valore massimo rappresentabile.

Il VHDL dell’esempio usa due processi: il primo imposta l’uscita di uguaglianza confrontando A e B, il secondo gestisce maggiore e minore con una semplice struttura if. Rivedendo il codice emerge anche un piccolo errore, la chiusura dell’entity con un nome diverso da quello di apertura: un VHDL rigoroso dovrebbe segnalarlo, mentre l’interprete di Circuit Deck è più permissivo e usa solo il primo nome per identificare il componente. Un buon promemoria del fatto che una simulazione logica non sostituisce il controllo di uno strumento di sintesi vero e proprio.

Sommatore a 8 bit e carry

Con il sommatore si vede bene il vantaggio dell’astrazione offerta dal VHDL: passare da 4 a 8 bit, o oltre, è quasi solo una questione di dimensionare i vettori, con il limite pratico rappresentato dalle risorse disponibili sulla FPGA.

L’esempio è un sommatore a 8 bit per numeri senza segno, con carry in ingresso e in uscita. Il trucco sta nel lavorare a 9 bit: gli operandi vengono estesi concatenando uno zero davanti a ciascuno (l’operatore & in VHDL è una concatenazione, non un AND logico) e convertiti in unsigned, così come il carry in ingresso. Dalla somma a 9 bit, gli 8 bit meno significativi diventano il risultato e il bit più alto diventa il carry out.

In simulazione, 12 più 24 restituisce 36 e 129 più 24 restituisce 153. Con 129 più 132, invece, il risultato è 5 con carry out a 1: 261 meno 256 fa proprio 5, e il carry segnala l’overflow, pensato per collegare più sommatori in cascata. Attivando il carry in ingresso, 3 più 4 diventa 8.

Circuit Deck offre anche una funzione comoda per questo scenario: duplicando un blocco VHDL, le copie condividono lo stesso codice, mentre con “rendi indipendente” ciascuna può avere una propria versione.

Sottrattore con segno e overflow

Il sottrattore dell’esempio lavora invece con numeri con segno: gli ingressi std_logic_vector vengono convertiti in signed, la differenza viene calcolata e riconvertita, e accanto al risultato si genera un bit di overflow che indica se il valore ottenuto è corretto.

La logica dell’overflow merita attenzione. Se A e B hanno lo stesso segno, la sottrazione non può uscire dal range rappresentabile: anche nel caso limite di 0 meno 127 si ottiene -127, valore rappresentabile su 8 bit. Il problema nasce solo quando i segni sono diversi. Sottrarre un positivo da un negativo produce un numero ancora più negativo, sottrarre un negativo da un positivo ne produce uno ancora più positivo: in entrambi i casi il risultato corretto deve avere lo stesso segno di A. Se così non è, si è verificato un overflow. In VHDL questo si traduce in un confronto sui bit di segno, cioè i bit più significativi, usando l’operatore di diversità /=.

Il full subtractor a un bit, in cascata

L’ultimo esempio è un full subtractor a un bit con borrow in ingresso e in uscita: lo stesso concetto di “prestito” delle sottrazioni in colonna imparate alle elementari. Il bit di differenza è lo XOR dei tre ingressi, mentre il borrow in uscita è descritto da un’espressione che Circuit Deck permette di verificare direttamente sulla tavola della verità, con i relativi mintermini.

Mettendo in cascata tre copie dello stesso blocco, che condividono il codice, e usando un bus builder per riunire i bit singoli in un bus visualizzato in decimale con segno, si ottiene un sottrattore a 3 bit completo. Il diagramma temporale conferma i risultati: 5 meno 2 fa 3, 7 meno 6 con un prestito in ingresso fa 0, 2 meno 5 fa -3.

File, Maker Faire e prossimi passi

Gli esempi sono stati costruiti volutamente in modo rapido, perché si tratta di concetti già trattati in dettaglio nei video precedenti del corso. Tutto il VHDL mostrato è sintetizzabile, e chi vuole approfondire può partire dai file di Circuit Deck che trovi qui sotto. Il blocco VHDL è una funzionalità della versione a pagamento, ma anche con la versione gratuita i file si possono aprire, consultare e simulare, senza però modificarli.

Schemi CircuitDeck

Avatar Paolo Godino