Menu

Istanziazione di moduli in Verilog: collegare i sottomoduli

Come istanziare un modulo dentro un altro, la differenza tra collegamenti delle porte per nome e per posizione, e i pattern a istanze multiple che userai per costruire progetti reali.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Moduli dentro moduli

Un progetto Verilog è un albero di moduli. Il modulo di primo livello (il tuo testbench o il wrapper principale di un chip) istanzia moduli di livello inferiore, che a loro volta istanziano moduli di livello ancora più basso, giù fino alle primitive di porta fornite dal produttore. L'istanziazione è la sintassi di questo annidamento.

Hai già visto la forma: l'abbiamo usata in Your First Module:

and_gate dut(.a(a), .b(b), .y(y));

Quella riga crea una singola istanza di and_gate, la chiama dut e ne collega le porte a segnali locali. Vediamo ogni parte.

La forma di un'istanziazione

module_name instance_name (port_connections);
  • module_name deve corrispondere al nome di una dichiarazione module presente da qualche parte nel progetto. Verilog distingue maiuscole e minuscole.
  • instance_name è un'etichetta che scegli tu, di solito descrittiva del ruolo di questa istanza. La userai nei percorsi gerarchici e nelle viste delle forme d'onda.
  • port_connections collega le porte dell'istanza ai segnali locali. Ci sono due modi di scriverlo.

Collegamenti per nome (usa questi)

La forma per nome è così:

my_module instance_name(
    .clk    (clk),
    .reset  (reset_n),
    .data_in(in_bus),
    .data_out(out_bus),
    .valid  (out_valid)
);

Ogni coppia .port(signal) dice "collega la porta di questa istanza chiamata port al segnale locale chiamato signal". L'ordine non conta. Se aggiungi una nuova porta alla dichiarazione del modulo, le istanziazioni esistenti non si rompono, purché tu dia alla nuova porta un valore predefinito o aggiorni ogni punto in cui è istanziato.

Due note pratiche:

  • Il nome della porta (prima delle parentesi) deve corrispondere esattamente alla dichiarazione del modulo.
  • Il nome del segnale (dentro le parentesi) è locale al punto in cui si trova l'istanziazione, di solito il modulo padre.

Se una porta non è collegata, lascia vuote le parentesi: .optional_port(). Dentro l'istanza il segnale resta flottante (z). Alcuni strumenti di sintesi avvisano; la maggior parte lo accetta.

Collegamenti per posizione (evitali)

La forma più sintetica elenca i segnali nell'ordine dell'elenco delle porte:

my_module instance_name(clk, reset_n, in_bus, out_bus, out_valid);

È più corta ma fragile. Se riordini l'elenco delle porte del modulo (un refactoring che capita davvero), ogni istanziazione per posizione si ritrova collegata male senza alcun avviso. Non usarla a meno che l'elenco delle porte abbia esattamente uno o due elementi e difficilmente cambierà.

Dove è ancora accettabile: piccoli moduli di utilità in cui l'ordine delle porte fa parte dell'interfaccia. Una porta logica a due ingressi va bene per posizione. Un controller di memoria con 30 porte è un problema annunciato.

Un esempio gerarchico completo

Questa è una vera gerarchia a tre livelli: test → full_adder → due istanze di half_adder. Ogni istanza ha la propria copia delle porte contenute in half_adder; lo strumento di sintesi genererà un circuito per ogni istanziazione.

Più istanze dello stesso modulo

Quando istanzi più volte lo stesso modulo, ogni istanza è hardware indipendente. Non condividono lo stato. Non condividono le porte logiche. Immagina ogni istanza come una copia nuova ricavata dal progetto.

adder add0(.a(a0), .b(b0), .sum(s0));
adder add1(.a(a1), .b(b1), .sum(s1));
adder add2(.a(a2), .b(b2), .sum(s2));
adder add3(.a(a3), .b(b3), .sum(s3));

Sono quattro sommatori separati che lavorano in parallelo. Se adder contenesse un registro, ogni istanza avrebbe la propria copia di quel registro, con il proprio stato.

Cicli generate: creare hardware ripetuto

Scrivere quattro istanze a mano va bene. Scriverne 64 è noioso. Il blocco generate lascia che sia l'elaboratore a scrivere al posto tuo:

Tre elementi di sintassi nuovi:

  • genvar i dichiara una variabile di ciclo utilizzabile in generate. Non è un segnale a runtime: esiste solo durante l'elaborazione.
  • generate ... endgenerate racchiude il ciclo. Alcuni strumenti accettano i cicli generate senza la parola chiave generate esplicita, ma scriverla rende chiara l'intenzione.
  • begin : invert_loop dà un'etichetta allo scope generato. L'etichetta diventa parte del nome gerarchico di ogni istanza generata (dut.invert_loop[0].u_inv, dut.invert_loop[1].u_inv, ecc.).

Il sintetizzatore srotola il ciclo e produce WIDTH copie di bit_inverter. Ogni copia è hardware indipendente.

Sovrascrivere i parametri nell'istanziazione

Se il modulo ha dei parametri, puoi sovrascriverli con #(.PARAM(value)) tra il nome del modulo e il nome dell'istanza:

counter #(.WIDTH(16)) c16 (.clk(clk), .count(out16));
counter #(.WIDTH(32)) c32 (.clk(clk), .count(out32));

Entrambe le istanze usano lo stesso sorgente counter, ma con larghezze diverse. Abbiamo visto la sintassi in Parameters; si inserisce in modo naturale nell'istanziazione.

Nomi gerarchici

Una volta che hai una gerarchia, ogni segnale ha un percorso gerarchico:

test.dut.ha0.sum

Si legge così: nel modulo test, dentro l'istanza dut, dentro l'istanza ha0, il segnale chiamato sum. Vedrai questi percorsi nei visualizzatori di forme d'onda, nei messaggi di errore e in qualche chiamata $display che da un testbench va a leggere in profondità un sottomodulo:

$display("internal carry1 = %b", dut.carry1);

Riferimenti gerarchici come questo servono solo per testbench e debug: l'RTL sintetizzabile non va a pescare dentro altri moduli.

Errori comuni

Nome di porta non corrispondente. .clk_in(clk) collega il clk locale a una porta chiamata clk_in. Se la porta del modulo si chiama in realtà clk, il parser te lo segnalerà (alcuni strumenti in modo più chiaro di altri).

Larghezza non corrispondente su una porta. Collegare un segnale a 4 bit a una porta a 8 bit lo estende con zeri senza avvisi; il contrario lo tronca in silenzio. La maggior parte degli strumenti avvisa; se non vedi avvisi, cerca meglio.

Dimenticare il # dei parametri. counter (.WIDTH(8)) c(.clk(clk)) sembra una sovrascrittura ma non lo è: il parser prova a trattare (.WIDTH(8)) come un collegamento di porta e fallisce. La forma corretta è counter #(.WIDTH(8)) c(.clk(clk)).

Riutilizzare un nome di istanza. Due istanze non possono avere lo stesso nome nello stesso scope. Di solito il messaggio di errore è chiaro; è la tentazione del copia e incolla a fregarti.

Cosa viene dopo

Ora puoi collegare i moduli in una vera gerarchia. La prossima pagina, Continuous Assignment, completa il lato strutturale di Verilog e approfondisce l'istruzione assign che usiamo liberamente fin dal primo capitolo.

Domande frequenti

Come si istanzia un modulo in Verilog?

Scrivi il nome del modulo, poi un nome di istanza, poi tra parentesi l'elenco dei collegamenti delle porte: my_module instance_name(.port(signal), ...);. Lo stile più diffuso usa i collegamenti per nome (.port(signal)), che si abbinano in base al nome della porta indipendentemente dall'ordine. Lo stile per posizione, più sintetico (my_module instance(signal1, signal2)), dipende dall'ordine dell'elenco delle porte ed è rischioso da mantenere.

Qual è la differenza tra collegamenti delle porte per nome e per posizione?

I collegamenti per posizione elencano i segnali nello stesso ordine dell'elenco delle porte del modulo: il primo segnale va alla prima porta, il secondo alla seconda e così via. I collegamenti per nome usano .port_name(signal_name) e si abbinano in base al nome. La forma per nome è più prolissa ma immune al riordino delle porte, e documenta da sola il punto di chiamata. Usala per qualsiasi modulo con più di due o tre porte.

Si può istanziare più volte lo stesso modulo Verilog?

Sì, ed è proprio questo il punto. Ogni istanza è hardware indipendente con il proprio stato. Se hai un modulo adder, puoi istanziarlo 64 volte in un'unità SIMD, ognuna con ingressi diversi. Il ciclo generate è la sintassi canonica quando le istanze sono simili e indicizzate.

Cos'è un blocco generate in Verilog?

generate ... endgenerate è un costrutto della fase di compilazione che crea hardware ripetuto. Un ciclo for dentro generate crea N istanze di ciò che sta nel corpo. generate agisce durante l'elaborazione, prima che la simulazione inizi: non è un ciclo a runtime, è un generatore di codice per il sintetizzatore.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA