Cos'è davvero un testbench
Un testbench è semplicemente un modulo Verilog. La differenza rispetto a un modulo sintetizzabile sta nel contenuto:
- Non ha porte: è il vertice della gerarchia di simulazione.
- Istanzia il design under test.
- Pilota gli ingressi del DUT dai blocchi
initialealways. - Osserva le uscite del DUT con
$display,$monitoro registrando un VCD. - Termina la simulazione con
$finish.
Questo è tutto il suo lavoro. Non esiste una parola chiave speciale "testbench": capisci che qualcosa è un testbench leggendo cosa fa.
Lo scheletro standard
Questo è l'intero pattern. Cinque sezioni, facili da copiare e da adattare.
Per i DUT combinatori (come il sommatore) non serve un clock. Per quelli sequenziali sì, ed è il livello successivo.
Generazione del clock per DUT sequenziali
Quando il DUT ha un ingresso clk, il testbench deve produrne uno. La riga standard:
reg clk = 0;
always #5 clk = ~clk;
Inverte clk ogni 5 unità di tempo. Ogni inversione dura metà del periodo di clock, quindi il periodo completo è di 10 unità (un ciclo = alto per 5 + basso per 5). Se il tuo timescale è 1ns / 1ps (il predefinito nella maggior parte dei simulatori), è un clock a 100 MHz.
Scegli il semiperiodo in base a ciò che vuoi modellare. Per un clock a 10 MHz con lo stesso timescale, usa #50. Per un clock a 200 MHz, #2.5 (oppure cambia il timescale).
Sequenza di reset
I progetti sincroni hanno bisogno di un reset tenuto alto per qualche ciclo dopo il tempo 0 e poi rilasciato. La forma convenzionale:
reg reset = 1; // start asserted
initial begin
// Hold reset for a few clocks.
#20 reset = 0;
end
Così reset resta alto fino al tempo 20, poi scende. Quando reset scende, il clock ha già fatto un paio di cicli, ogni flip-flop ha catturato il valore di reset e il progetto si trova in uno stato noto.
Per un reset attivo basso (reset_n invece di reset), inverti il valore iniziale:
reg reset_n = 0; // active-low: 0 means asserted
initial begin
#20 reset_n = 1;
end
Un testbench sequenziale completo
Mettendo insieme clock, reset e stimoli:
Questo è un testbench completo per un contatore a 4 bit. Premi Run e vedrai il contatore uscire dal reset, contare mentre è abilitato, fermarsi quando enable scende, ripartire quando risale e poi fermarsi.
Tre cose da notare sulla struttura:
- Tre centri di attività distinti: il generatore di clock (un
always), gli stimoli (uninitial) e il monitor (un altroalways). Ognuno ha un solo compito. - I ritardi
#nel blocco degli stimoli si misurano in unità di tempo, non in cicli di clock. Se il periodo di clock è di 10 unità,#10è esattamente un ciclo. Alcuni testbench usano invece@(posedge clk), che avanza di un ciclo indipendentemente dal periodo. - Il monitor usa
$displayda un bloccoalways @(posedge clk). Così stampa a ogni ciclo. Per un output più sofisticato, passa a$monitor(lo vediamo dopo).
Stimoli basati sui cicli
A volte "aspetta N cicli" si legge meglio di "aspetta N unità di tempo":
initial begin
@(posedge clk); // wait for next clock edge
reset = 0;
repeat (5) @(posedge clk); // wait 5 more cycles
enable = 1;
repeat (8) @(posedge clk);
enable = 0;
repeat (10) @(posedge clk);
$finish;
end
@(posedge clk) si blocca fino al prossimo fronte di salita del clock. repeat (N) @(posedge clk); aspetta N cicli. Questo stile non dipende dal periodo del clock: se cambi la frequenza, gli stimoli fanno sempre la stessa cosa in termini di cicli.
Nei primi testbench i ritardi # sono più semplici. Nei testbench di produzione, che possono girare a più velocità di clock, lo stile basato sui cicli è più sicuro.
Test con autoverifica
Il testbench visto finora stampa cosa è successo e lascia a te la lettura dell'output. Un testbench con autoverifica invece controlla l'output e segnala se il test è passato o fallito:
initial begin
#1;
a = 10; b = 20;
#1;
if (sum !== 30) begin
$display("FAIL: 10 + 20 = %0d (expected 30)", sum);
$finish;
end
a = 250; b = 5;
#1;
if (sum !== 255) begin
$display("FAIL: 250 + 5 = %0d (expected 255)", sum);
$finish;
end
$display("PASS");
$finish;
end
Per sicurezza usa !== (l'operatore di disuguaglianza case): non restituisce x quando un operando ha bit sconosciuti. Il pattern: applica gli ingressi, aspetta che tutto si stabilizzi, confronta con il valore atteso, segnala il successo o il fallimento.
L'autoverifica è preziosa nelle suite di regressione: migliaia di test possono girare senza supervisione e solo i fallimenti richiedono attenzione.
Cosa viene dopo
Ora hai la struttura di testbench che basta per qualsiasi modulo. Le prossime pagine trattano gli strumenti che vanno dentro il testbench: Display and Monitor per un output testuale più ricco, Dumpfile and VCD per il debug grafico con le forme d'onda e Timescale and Delays per controllare con precisione come il tempo di simulazione si rapporta al tempo reale.
Domande frequenti
Cos'è un testbench in Verilog?
Un testbench è un modulo Verilog, di solito senza porte, il cui unico scopo è mettere alla prova un design under test (DUT). Istanzia il DUT, genera un clock, applica gli stimoli tramite blocchi initial e always, osserva le uscite con $display/$monitor, eventualmente registra una forma d'onda VCD e termina la simulazione con $finish.
Come si genera un clock in un testbench Verilog?
Il pattern standard è una sola riga: always #5 clk = ~clk; (con reg clk = 0; dichiarato prima). Inverte clk ogni 5 unità di tempo di simulazione, dando un periodo di 10 unità (un clock a 100 MHz se il timescale è in nanosecondi). #5 è il semiperiodo: per metà del tempo clk è alto, per l'altra metà è basso.
Cos'è un DUT in Verilog?
DUT sta per Design Under Test, il modulo che il testbench mette alla prova. Per convenzione, nel testbench l'istanza del DUT si chiama dut o u_dut: my_module dut(.clk(clk), .reset(reset), .in(in), .out(out));. Il nome è solo un'etichetta; ciò che conta è che il modulo sia istanziato, che le sue porte siano collegate ai segnali del testbench e che il testbench piloti quei segnali.
Per quanto tempo deve durare una simulazione Verilog?
Abbastanza da mettere alla prova tutto ciò che vuoi verificare, poi $finish. La maggior parte dei testbench fissa un limite di tempo esplicito (#1000 $finish), così la simulazione non può restare bloccata in attesa di un evento che non arriva mai. Dentro quella finestra applica gli stimoli, lascia che il DUT si stabilizzi e, idealmente, includi qualche if di autoverifica che stampi FAIL se l'uscita non corrisponde alle attese.