Ricerca…


introduzione

In questo argomento proponiamo un metodo semplice per progettare correttamente semplici circuiti digitali con VHDL. Il metodo si basa su diagrammi a blocchi grafici e un principio di facile memorizzazione:

Pensa prima all'hardware, quindi al codice VHDL

È destinato ai principianti nella progettazione di hardware digitale con VHDL, con una comprensione limitata della semantica di sintesi della lingua.

Osservazioni

La progettazione hardware digitale con VHDL è semplice, anche per i principianti, ma ci sono alcune cose importanti da sapere e un piccolo insieme di regole da rispettare. Lo strumento utilizzato per trasformare una descrizione VHDL nell'hardware digitale è un sintetizzatore logico. La semantica del linguaggio VHDL utilizzato dai sintetizzatori logici è piuttosto diversa dalla semantica della simulazione descritta nel manuale di riferimento linguistico (LRM). Ancora peggio: non è standardizzato e varia tra gli strumenti di sintesi.

Il metodo proposto introduce alcune limitazioni importanti per motivi di semplicità:

  • Nessun fermo a livello attivato.
  • I circuiti sono sincroni sul fronte di salita di un singolo clock.
  • Nessun reset o set asincrono.
  • Nessuna unità multipla su segnali risolti.

L'esempio del diagramma a blocchi , primo di una serie di 3, presenta brevemente le basi dell'hardware digitale e propone una breve lista di regole per progettare uno schema a blocchi di un circuito digitale. Le regole aiutano a garantire una traduzione diretta al codice VHDL che simula e sintetizza come previsto.

L'esempio di codifica spiega la traduzione da uno schema a blocchi al codice VHDL e lo illustra su un semplice circuito digitale.

Infine, l'esempio del concorso di design di John Cooley mostra come applicare il metodo proposto su un esempio più complesso di circuito digitale. Inoltre elabora le limitazioni introdotte e rilassa alcune di esse.

Diagramma a blocchi

L'hardware digitale è costituito da due tipi di primitive hardware:

  • Gate combinatori (inverter, e, o, xor, sommatori completi a 1 bit, multiplexer a 1 bit ...) Queste porte logiche eseguono un semplice calcolo booleano sui loro ingressi e producono un output. Ogni volta che uno dei loro ingressi cambia, iniziano a propagare segnali elettrici e, dopo un breve ritardo, l'uscita si stabilizza sul valore risultante. Il ritardo di propagazione è importante perché è fortemente correlato alla velocità di esecuzione del circuito digitale, ovvero alla sua frequenza di clock massima.
  • Elementi di memoria (latch, D-flip-flop, RAM ...). Contrariamente alle porte logiche combinatorie, gli elementi di memoria non reagiscono immediatamente al cambiamento di nessuno dei loro ingressi. Hanno ingressi dati, ingressi di controllo e uscite dati. Reagiscono su una particolare combinazione di input di controllo, non su qualsiasi cambiamento dei loro input di dati. Il D-flip-flop (DFF) triggerato dal fronte di salita, ad esempio, ha un ingresso di clock e un input di dati. Ad ogni fronte di salita del clock, l'input dei dati viene campionato e copiato sull'output dei dati che rimane stabile fino al prossimo fronte di salita dell'orologio, anche se l'input di dati cambia in mezzo.

Un circuito hardware digitale è una combinazione di logica combinatoria ed elementi di memoria. Gli elementi di memoria hanno diversi ruoli. Uno di questi è consentire il riutilizzo della stessa logica combinatoria per diverse operazioni consecutive su dati diversi. I circuiti che usano questo sono spesso chiamati circuiti sequenziali . La figura seguente mostra un esempio di un circuito sequenziale che accumula valori interi utilizzando lo stesso sommatore combinatorio, grazie a un registro attivato dal fronte di salita. È anche il nostro primo esempio di uno schema a blocchi.

Un circuito sequenziale

Il rivestimento dei tubi è un altro uso comune degli elementi di memoria e la base di molte architetture di microprocessori. Mira ad aumentare la frequenza di clock di un circuito suddividendo un'elaborazione complessa in una successione di operazioni più semplici e parallelizzando l'esecuzione di più elaborazioni consecutive:

Rivestimento di tubi di un complesso processo combinatorio

Lo schema a blocchi è una rappresentazione grafica del circuito digitale. Aiuta a prendere le giuste decisioni e ad acquisire una buona comprensione della struttura generale prima della codifica. È l'equivalente delle fasi di analisi preliminare raccomandate in molti metodi di progettazione del software. I progettisti esperti saltano frequentemente questa fase di progettazione, almeno per circuiti semplici. Se sei un principiante nella progettazione di hardware digitale, tuttavia, e se desideri codificare un circuito digitale in VHDL, l'adozione delle 10 semplici regole seguenti per disegnare il diagramma a blocchi dovrebbe aiutarti a farlo bene:

  1. Circonda il tuo disegno con un grande rettangolo. Questo è il confine del tuo circuito. Tutto ciò che attraversa questo limite è una porta di input o output. L'entità VHDL descriverà questo limite.
  2. Chiaramente separare i registri con trigger di bordo (ad es. Blocchi quadrati) dalla logica combinatoria (ad es. Blocchi rotondi). Nel VHDL saranno tradotti in processi ma di due tipi molto diversi: sincrono e combinatorio.
  3. Non utilizzare latch con trigger di livello, utilizzare solo registri con trigger in salita. Questo vincolo non proviene da VHDL, che è perfettamente utilizzabile per i latch dei modelli. È solo un consiglio ragionevole per i principianti. I latch sono meno frequentemente necessari e il loro uso pone molti problemi che probabilmente dovremmo evitare, almeno per i nostri primi progetti.
  4. Usa lo stesso orologio singolo per tutti i tuoi registri innescati di frontiera. Anche in questo caso, questo vincolo è qui per motivi di semplicità. Non viene da VHDL, che è perfettamente utilizzabile per modellare sistemi multi-clock. Dai un nome al clock . Viene dall'esterno ed è un input di tutti i blocchi quadrati e solo loro. Se lo desideri, non rappresentare nemmeno l'orologio, è lo stesso per tutti i blocchi quadrati e puoi lasciarlo implicito nel tuo diagramma.
  5. Rappresenta le comunicazioni tra i blocchi con frecce con nome e orientate. Per il blocco da cui proviene una freccia, la freccia è un'uscita. Per il blocco a cui va una freccia, la freccia è un input. Tutte queste frecce diventeranno porte dell'entità VHDL, se attraversano il rettangolo grande o segnali dell'architettura VHDL.
  6. Le frecce hanno un'unica origine ma possono avere più destinazioni. Infatti, se una freccia avesse origini diverse, creeremmo un segnale VHDL con diversi driver. Questo non è completamente impossibile, ma richiede una cura particolare per evitare cortocircuiti. Eviteremo così questo per ora. Se una freccia ha più destinazioni, fora la freccia tante volte quante sono necessarie. Usa punti per distinguere traversate connesse e non connesse.
  7. Alcune frecce provengono dall'esterno del grande rettangolo. Queste sono le porte di input dell'entità. Una freccia di input non può essere l'output di nessuno dei tuoi blocchi. Questo è applicato dal linguaggio VHDL: le porte di input di un'entità possono essere lette ma non scritte. Questo è di nuovo per evitare cortocircuiti.
  8. Alcune frecce vanno fuori. Queste sono le porte di uscita. Nelle versioni VHDL precedenti al 2008, le porte di output di un'entità possono essere scritte ma non lette. Una freccia di uscita deve quindi avere un'unica origine e un'unica destinazione: l'esterno. Nessun fork sulle frecce di output, una freccia di output non può essere anche l'input di uno dei tuoi blocchi. Se vuoi usare una freccia di uscita come input per alcuni dei tuoi blocchi, inserisci un nuovo blocco rotondo per dividerlo in due parti: quella interna, con tutte le forcelle che desideri e la freccia di uscita che viene dal nuovo bloccare e va fuori. Il nuovo blocco diventerà un semplice compito continuo in VHDL. Una sorta di rinominazione trasparente. Dal momento che anche le porte ouptut di VHDL 2008 possono essere lette.
  9. Tutte le frecce che non arrivano o vanno da / verso l'esterno sono segnali interni. Li dichiarerai tutti nell'architettura VHDL.
  10. Ogni ciclo nel diagramma deve comprendere almeno un blocco quadrato. Questo non è dovuto a VHDL. Viene dai principi di base della progettazione dell'hardware digitale. I circuiti combinatori devono assolutamente essere evitati. Tranne in casi molto rari, non producono alcun risultato utile. E un ciclo dello schema a blocchi che comprenderebbe solo blocchi rotondi sarebbe un ciclo combinatorio.

Non dimenticare di controllare attentamente l'ultima regola, è essenziale quanto gli altri, ma potrebbe essere un po 'più difficile da verificare.

A meno che tu non abbia assolutamente bisogno di funzionalità che abbiamo escluso per ora, come latch, multi-clock o segnali con più driver, dovresti disegnare facilmente uno schema a blocchi del tuo circuito conforme alle 10 regole. In caso contrario, il problema è probabilmente con il circuito che si desidera, non con VHDL o il sintetizzatore logico. E probabilmente significa che il circuito che vuoi non è l'hardware digitale.

Applicare le 10 regole al nostro esempio di un circuito sequenziale porterebbe a uno schema a blocchi come:

Schema a blocchi rielaborato del circuito sequenziale

  1. Il grande rettangolo attorno al diagramma è attraversato da 3 frecce, che rappresentano le porte di input e output dell'entità VHDL.
  2. Lo schema a blocchi ha due blocchi rotondi (combinatori) - il sommatore e il blocco di ridenominazione dell'output - e un blocco quadrato (sincrono) - il registro.
  3. Utilizza solo registri con trigger su edge.
  4. C'è solo un orologio, chiamato clock e usiamo solo il suo fronte di salita.
  5. Lo schema a blocchi ha cinque frecce, una con una forchetta. Corrispondono a due segnali interni, due porte di ingresso e una porta di uscita.
  6. Tutte le frecce hanno un'origine e una destinazione tranne la freccia denominata Sum che ha due destinazioni.
  7. Le frecce Data_in e Clock sono le nostre due porte di input. Non sono prodotti dai nostri blocchi.
  8. La freccia Data_out è la nostra porta di uscita. Per essere compatibili con le versioni VHDL precedenti al 2008, abbiamo aggiunto un ulteriore blocco di rinomina (rotondo) tra Sum e Data_out . Quindi, Data_out ha esattamente una fonte e una destinazione.
  9. Sum e Next_sum sono i nostri due segnali interni.
  10. Esiste esattamente un ciclo nel grafico e comprende un blocco quadrato.

Il nostro diagramma a blocchi è conforme alle 10 regole. L'esempio di codifica descriverà in dettaglio come tradurre questo tipo di diagrammi a blocchi in VHDL.

Coding

Questo esempio è il secondo di una serie di 3. Se non l'hai ancora fatto, leggi prima l'esempio del diagramma a blocchi .

Con uno schema a blocchi conforme alle 10 regole (vedere l'esempio del diagramma a blocchi ), la codifica VHDL diventa immediata:

  • il grande rettangolo circostante diventa l'entità VHDL,
  • le frecce interne diventano segnali VHDL e vengono dichiarate nell'architettura,
  • ogni blocco quadrato diventa un processo sincrono nel corpo dell'architettura,
  • ogni blocco rotondo diventa un processo combinatorio nel corpo dell'architettura.

Cerchiamo di illustrare questo sullo schema a blocchi di un circuito sequenziale:

Un circuito sequenziale

Il modello VHDL di un circuito comprende due unità di compilazione:

  • L'entità che descrive il nome del circuito e la sua interfaccia (nomi delle porte, direzioni e tipi). È una traduzione diretta del grande rettangolo circostante dello schema a blocchi. Supponendo che i dati siano numeri interi e che l' clock usi il bit tipo VHDL (solo due valori: '0' e '1' ), l'entità del nostro circuito sequenziale potrebbe essere:
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;
  • L'architettura che descrive gli interni del circuito (cosa fa). Qui è dove vengono dichiarati i segnali interni e dove vengono istanziati tutti i processi. Lo scheletro dell'architettura del nostro circuito sequenziale potrebbe essere:
architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  <...processes...>
end architecture ten_rules;

Abbiamo tre processi da aggiungere al corpo dell'architettura, uno sincrono (blocco quadrato) e due combinatori (blocchi rotondi).

Un processo sincrono si presenta così:

process(clock)
begin
  if rising_edge(clock) then
    o1 <= i1;
    ...
    ox <= ix;
  end if;
end process;

dove i1, i2,..., ix sono tutte le frecce che entrano nel blocco quadrato corrispondente del diagramma e o1, ..., ox sono tutte frecce che emettono il corrispondente blocco quadrato del diagramma. Assolutamente nulla deve essere cambiato, ad eccezione dei nomi dei segnali, ovviamente. Niente. Neanche un singolo personaggio.

Il processo sincrono del nostro esempio è quindi:

  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

Che può essere tradotto in modo informale in: se l' clock cambia, e solo allora, se il cambiamento è un fronte ascendente (da '0' a '1' ), assegnare il valore del segnale Next_sum al segnale Sum .

Un processo combinatorio si presenta così:

process(i1, i2,... , ix)
  variable v1: <type_of_v1>;
  ...
  variable vy: <type_of_vy>;
begin
  v1 := <default_value_for_v1>;
  ...
  vy := <default_value_for_vy>;
  o1 <= <default_value_for_o1>;
  ...
  oz <= <default_value_for_oz>;
  <statements>
end process;

dove i1, i2,..., in sono tutte le frecce che entrano nel corrispondente blocco rotondo del diagramma. tutto e niente di più. Non dimenticheremo alcuna freccia e non aggiungeremo altro alla lista.

v1, ..., vy sono variabili che potremmo aver bisogno di semplificare il codice del processo. Hanno esattamente lo stesso ruolo di qualsiasi altro linguaggio di programmazione imperativo: mantenere valori temporanei. Devono assolutamente essere assegnati tutti prima di essere letti. Se non si riesce a garantire ciò, il processo non sarà più combinatorio in quanto modellerà il tipo di elementi di memoria per mantenere il valore di alcune variabili da un'esecuzione di processo a quella successiva. Questo è il motivo per le istruzioni vi := <default_value_for_vi> all'inizio del processo. Nota che <default_value_for_vi> deve essere una costante. In caso contrario, se si tratta di espressioni, potremmo accidentalmente utilizzare variabili nelle espressioni e leggere una variabile prima di assegnarla.

o1, ..., om sono tutte le frecce che emettono il corrispondente blocco circolare del diagramma. tutto e niente di più. Devono assolutamente essere tutti assegnati almeno una volta durante l'esecuzione del processo. Poiché le strutture di controllo VHDL ( if , case ...) possono facilmente impedire l'assegnazione di un segnale in uscita, consigliamo vivamente di assegnare ognuna di esse, incondizionatamente, con un valore costante <default_value_for_oi> all'inizio del processo. In questo modo, anche se un'istruzione if maschera un assegnamento del segnale, avrà comunque ricevuto un valore.

Assolutamente nulla deve essere cambiato in questo scheletro VHDL, eccetto i nomi delle variabili, se ce ne sono, i nomi degli input, i nomi delle uscite, i valori delle <default_value_for_..> costanti e <statements> . Non dimenticate una singola assegnazione valore di default, se si fa la sintesi sarà dedurre elementi di memoria indesiderati (molto probabilmente fermi) e il risultato non sarà quello che inizialmente voleva.

Nel nostro circuito sequenziale di esempio, il processo sommatore combinatorio è:

  process(Sum, Data_in)
  begin
    Next_sum <= 0;
    Next_sum <= Sum + Data_in;
  end process;

Che può essere tradotto in modo informale in: se Sum o Data_in (o entrambi) cambiano, assegna il valore 0 a signal Next_sum e poi assegna nuovamente il valore Sum + Data_in .

Poiché il primo compito (con il valore predefinito costante 0 ) è immediatamente seguito da un altro compito che lo sovrascrive, possiamo semplificare:

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

Il secondo processo combinatorio corrisponde al blocco circolare che abbiamo aggiunto su una freccia di uscita con più di una destinazione per soddisfare le versioni VHDL precedenti al 2008. Il suo codice è semplicemente:

  process(Sum)
  begin
    Data_out <= 0;
    Data_out <= Sum;
  end process;

Per la stessa ragione che con l'altro processo combinatorio, possiamo semplificarlo come:

  process(Sum)
  begin
    Data_out <= Sum;
  end process;

Il codice completo per il circuito sequenziale è:

-- File sequential_circuit.vhd
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;

architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

  process(Sum)
  begin
    Data_out <= Sum;
  end process;
end architecture ten_rules;

Nota: potremmo scrivere i tre processi in qualsiasi ordine, non cambierebbe nulla al risultato finale in simulazione o in sintesi. Questo perché i tre processi sono istruzioni concorrenti e VHDL li tratta come se fossero realmente paralleli.

Concorso di design di John Cooley

Questo esempio è direttamente derivato dal concorso di design di John Cooley a SNUG'95 (incontro del gruppo Synopsys Users). Il concorso intendeva opporsi ai progettisti di VHDL e Verilog sullo stesso problema di progettazione. Quello che John aveva in mente probabilmente era determinare quale lingua fosse la più efficiente. I risultati sono stati che 8 dei 9 designer di Verilog sono riusciti a completare il concorso di design ma nessuno dei 5 designer di VHDL è riuscito a farlo. Speriamo che, usando il metodo proposto, faremo un lavoro molto migliore.

specificazioni

Il nostro obiettivo è progettare in VHDL (entità e architettura) sintetizzabile semplice un contatore 512 modulo 512 sincronizzato, up-by-3, caricabile, con output carry, output di prestito e output di parità. Il contatore è un contatore senza segno a 9 bit, quindi è compreso tra 0 e 511. Le specifiche dell'interfaccia del contatore sono riportate nella seguente tabella:

Nome Bit-width Direzione Descrizione
OROLOGIO 1 Ingresso Master clock; il contatore è sincronizzato sul fronte di salita di CLOCK
DI 9 Ingresso Bus di ingresso dati; il contatore è caricato con DI quando UP e DOWN sono entrambi bassi
SU 1 Ingresso Comando di conteggio fino a 3; quando UP è alto e DOWN è basso il contatore aumenta di 3, avvolgendo il suo valore massimo (511)
GIÙ 1 Ingresso Comando Down-by-5; quando DOWN è alto e UP è basso il contatore decrementa di 5, avvolgendolo attorno al suo valore minimo (0)
CO 1 Produzione Effettuare il segnale; alto solo quando si conta oltre il valore massimo (511) e quindi si avvolge
BO 1 Produzione Prendere in prestito il segnale; alto solo quando il conto alla rovescia scende al di sotto del valore minimo (0) e quindi si avvolge
FARE 9 Produzione Bus di uscita; il valore corrente del contatore; quando UP e DOWN sono entrambi alti, il contatore mantiene il suo valore
PO 1 Produzione Segnale di parità; alto quando il valore corrente del contatore contiene un numero pari di 1

Quando si contano oltre il suo valore massimo o quando si contano al di sotto del suo valore minimo, il contatore si avvolge:

Contro valore attuale SOTTOSOPRA Contro il prossimo valore Next CO Prossimo BO Prossimo PO
X 00 DI 0 0 parità (DI)
X 11 X 0 0 parità (x)
0 ≤ x ≤ 508 10 x + 3 0 0 parità (x + 3)
509 10 0 1 0 1
510 10 1 1 0 0
511 10 2 1 0 0
5 ≤ x ≤ 511 01 x-5 0 0 parità (x-5)
4 01 511 0 1 0
3 01 510 0 1 1
2 01 509 0 1 1
1 01 508 0 1 0
0 01 507 0 1 1

Diagramma a blocchi

Sulla base di queste specifiche possiamo iniziare a progettare uno schema a blocchi. Per prima cosa rappresentiamo l'interfaccia:

L'interfaccia esterna

Il nostro circuito ha 4 ingressi (incluso l'orologio) e 4 uscite. Il prossimo passo consiste nel decidere quanti registri e blocchi combinatori useremo e quali saranno i loro ruoli. Per questo semplice esempio dedicheremo un blocco combinatorio al calcolo del prossimo valore del contatore, del prelievo e del prestito. Un altro blocco combinatorio sarà usato per calcolare il prossimo valore della parità. I valori correnti del contatore, del carry out e del borrow out verranno memorizzati in un registro mentre il valore corrente dell'uscita di parità verrà memorizzato in un registro separato. Il risultato è mostrato nella figura seguente:

Due blocchi combinatori e due registri

Controllare che lo schema a blocchi sia conforme alle nostre 10 regole di progettazione è fatto rapidamente:

  1. La nostra interfaccia esterna è correttamente rappresentata dal grande rettangolo circostante.
  2. I nostri 2 blocchi combinatori (rotondi) ei nostri 2 registri (quadrati) sono chiaramente separati.
  3. Utilizziamo solo registri con trigger in salita.
  4. Usiamo solo un orologio.
  5. Abbiamo 4 frecce interne (segnali), 4 frecce d'ingresso (porte d'ingresso) e 4 frecce d'uscita (porte d'uscita).
  6. Nessuna delle nostre frecce ha origini diverse. Tre hanno diverse destinazioni ( clock , ncnt e do ).
  7. Nessuna delle nostre 4 frecce di input è un'uscita dei nostri blocchi interni.
  8. Tre delle nostre frecce di output hanno esattamente un'origine e una destinazione. Ma do ha 2 destinazioni: l'esterno e uno dei nostri blocchi combinatorie. Questo viola la regola numero 8 e deve essere corretto inserendo un nuovo blocco combinatorio se vogliamo rispettare le versioni VHDL precedenti al 2008:

Un blocco combinatorio in più

  1. Abbiamo ora esattamente 5 segnali interni ( cnt , nco , nbo , ncnt e npo ).
  2. C'è solo un ciclo nel diagramma, formato da cnt e ncnt . C'è un blocco quadrato nel ciclo.

Codifica nelle versioni VHDL precedenti al 2008

La traduzione del nostro diagramma a blocchi in VHDL è semplice. Il valore corrente del contatore varia da 0 a 511, quindi utilizzeremo un segnale bit_vector 9 bit per rappresentarlo. L'unica sottigliezza deriva dalla necessità di eseguire operazioni bit a bit (come il calcolo della parità) e aritmetiche sugli stessi dati. Il pacchetto standard numeric_bit della libreria ieee risolve questo problema: dichiara un tipo unsigned con la stessa dichiarazione di bit_vector e sovraccarica gli operatori aritmetici in modo tale da prendere qualsiasi combinazione di numeri interi e unsigned . Per calcolare l'esecuzione e il prestito, utilizzeremo un valore temporaneo unsigned 10 bit.

Le dichiarazioni della biblioteca e l'entità:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

Lo scheletro dell'architettura è:

architecture arc1 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
    <...processes...>
end architecture arc1;

Ciascuno dei nostri 5 blocchi è modellato come un processo. I processi sincroni corrispondenti ai nostri due registri sono molto facili da codificare. Usiamo semplicemente il modello proposto nell'esempio di codifica . Il registro che memorizza il flag di parità, ad esempio, è codificato:

  poreg: process(clock)
  begin
    if rising_edge(clock) then
      po <= npo;
    end if;
  end process poreg;

e l'altro registro che memorizza co , bo e cnt :

  cobocntreg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
    end if;
  end process cobocntreg;

Il processo combinatorio di rinomina è anche molto semplice:

  rename: process(cnt)
  begin
    do <= (others => '0');
    do <= bit_vector(cnt);
  end process rename;

Il calcolo della parità può utilizzare una variabile e un ciclo semplice:

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

L'ultimo processo combinatorio è il più complesso di tutti, ma l'applicazione rigorosa del metodo di traduzione proposto lo rende anche facile:

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;

Si noti che i due processi sincroni potrebbero anche essere uniti e che uno dei nostri processi combinatori può essere semplificato in una semplice assegnazione simultanea del segnale. Il codice completo, con le dichiarazioni delle librerie e dei pacchetti e con le semplificazioni proposte è il seguente:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc2 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
  reg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      po  <= npo;
      cnt <= ncnt;
    end if;
  end process reg;

  do <= bit_vector(cnt);

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;
end architecture arc2;

Andando un po 'oltre

Il metodo proposto è semplice e sicuro, ma si basa su diversi vincoli che possono essere rilassati.

Salta il disegno del diagramma a blocchi

I progettisti esperti possono saltare il disegno di uno schema a blocchi per progetti semplici. Ma pensano ancora prima l'hardware. Si disegnano in testa anziché su un foglio di carta, ma in qualche modo continuano a disegnare.

Utilizza reimpostazioni asincrone

Ci sono circostanze in cui le reimpostazioni asincrone (o le serie) possono migliorare la qualità di un progetto. Il metodo proposto supporta solo ripristini sincroni (vale a dire i ripristini che vengono presi in considerazione nei fronti di salita dell'orologio):

  process(clock)
  begin
    if rising_edge(clock) then
      if reset = '1' then
        o <= reset_value_for_o;
      else
        o <= i;
      end if;
    end if;
  end process;

La versione con ripristino asincrono modifica il nostro modello aggiungendo il segnale di ripristino nell'elenco di sensibilità e assegnandogli la massima priorità:

  process(clock, reset)
  begin
    if reset = '1' then
      o <= reset_value_for_o;
    elsif rising_edge(clock) then
      o <= i;
    end if;
  end process;

Unisci diversi processi semplici

L'abbiamo già usato nella versione finale del nostro esempio. L'unione di più processi sincroni, se tutti hanno lo stesso clock, è banale. L'unione di diversi processi combinatori in uno è anche banale ed è solo una semplice riorganizzazione dello schema a blocchi.

Possiamo anche unire alcuni processi combinatori con processi sincroni. Ma per fare questo dobbiamo tornare al nostro diagramma a blocchi e aggiungere un'undicesima regola:

  1. Raggruppa diversi blocchi rotondi e almeno un blocco quadrato disegnando attorno a loro un recinto. Includi anche le frecce che possono essere. Non lasciare che una freccia attraversi il confine del recinto se non viene o esce da / a all'esterno del recinto. Una volta fatto, guarda tutte le frecce di uscita del contenitore. Se qualcuno di questi proviene da un blocco rotondo del contenitore o è anche un input del contenitore, non possiamo unire questi processi in un processo sincrono. Altrimenti possiamo.

Nel nostro esempio contatore, ad esempio, non è possibile raggruppare i due processi nel contenitore rosso della seguente figura:

Processi che non possono essere uniti

perché ncnt è un'uscita del recinto e la sua origine è un blocco rotondo (combinatorio). Ma potremmo raggruppare:

Processi che possono essere uniti

Il segnale interno npo diventerebbe inutile e il processo risultante sarebbe:

  poreg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process poreg;

che potrebbe anche essere unito con l'altro processo sincrono:

  reg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process reg;

Il raggruppamento potrebbe anche essere:

Più raggruppamenti

Portando all'architettura molto più semplice:

architecture arc5 of cooley is
  signal cnt: unsigned(8 downto 0);
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
    variable tmp:  bit;
  begin
    if rising_edge(clock) then
      ncnt := '0' & cnt;
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po  <= not tmp;
      cnt <= ncnt(8 downto 0);
    end if;
  end process;

  do <= bit_vector(cnt);
end architecture arc5;

con due processi (l'assegnazione simultanea del segnale di do è una scorciatoia per il processo equivalente). La soluzione con un solo processo è lasciata come esercizio. Attenzione, solleva domande interessanti e sottili.

Andando ancora oltre

I latch con trigger di livello, i bordi di clock in caduta, i clock multipli (e i resynchronizer tra domini clock), i driver multipli per lo stesso segnale, ecc. Non sono malvagi. A volte sono utili. Ma imparare come usarli e come evitare le insidie ​​associate va ben oltre questa breve introduzione al design dell'hardware digitale con VHDL.

Coding in VHDL 2008

VHDL 2008 ha introdotto diverse modifiche che possiamo utilizzare per semplificare ulteriormente il nostro codice. In questo esempio possiamo beneficiare di 2 modifiche:

  • le porte di uscita possono essere lette, non abbiamo più bisogno del segnale cnt ,
  • l'operatore unario xor può essere utilizzato per calcolare la parità.

Il codice VHDL 2008 potrebbe essere:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc6 of cooley is
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
  begin
    if rising_edge(clock) then
      ncnt := unsigned('0' & do);
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      po <= not (xor ncnt(8 downto 0));
      do <= bit_vector(ncnt(8 downto 0));
    end if;
  end process;
end architecture arc6;


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow