Was eine Testbench eigentlich ist
Eine Testbench ist einfach ein Verilog-Modul. Der Unterschied zu einem synthetisierbaren Modul ist das, was drinsteht:
- Sie hat keine Ports - sie ist die Spitze der Simulations-Hierarchie.
- Sie instanziiert das Design under Test.
- Sie treibt die Eingänge des DUT aus
initial- undalways-Blöcken. - Sie beobachtet die Ausgänge des DUT mit
$display,$monitoroder durch Dumpen einer VCD. - Sie beendet die Simulation mit
$finish.
Das ist der gesamte Job. Es gibt kein spezielles "testbench"-Schlüsselwort - was eine Testbench ist, erkennt man daran, was sie tut.
Das Standard-Skelett
Das ist das ganze Muster. Fünf Abschnitte, leicht zu kopieren, leicht anzupassen.
Für kombinatorische DUTs (wie den Addierer) brauchst du keinen Takt. Für sequentielle DUTs schon - das ist die nächste Ebene.
Taktgenerierung für sequentielle DUTs
Hat das DUT einen clk-Eingang, muss die Testbench einen erzeugen. Der Standard-Einzeiler:
reg clk = 0;
always #5 clk = ~clk;
Das schaltet clk alle 5 Zeiteinheiten um. Jedes Umschalten ist die halbe Taktperiode, also ist die volle Periode 10 Einheiten (ein Zyklus = 5 high + 5 low). Ist deine Timescale 1ns / 1ps (Default in den meisten Simulatoren), entspricht das einem 100-MHz-Takt.
Wähle die Halb-Periode danach, was du modellieren willst. Für einen 10-MHz-Takt bei gleicher Timescale nimm #50. Für einen 200-MHz-Takt #2.5 (oder ändere die Timescale).
Reset-Sequenz
Synchrone Designs brauchen einen Reset, der für einige Zyklen nach Zeit 0 high gehalten und dann freigegeben wird. Die übliche Form:
reg reset = 1; // beginne aktiviert
initial begin
// Reset für ein paar Takte halten.
#20 reset = 0;
end
Das hält reset high bis Zeit 20, dann fällt es. Bis reset fällt, hat der Takt ein paar Zyklen gemacht, jedes Flip-Flop hat den Reset-Wert erfasst, und das Design ist in einem bekannten Zustand.
Für active-low-Reset (reset_n statt reset) invertiere den Anfangswert:
reg reset_n = 0; // active-low: 0 heißt aktiviert
initial begin
#20 reset_n = 1;
end
Eine sequentielle Testbench in voller Länge
Takt, Reset und Stimulus kombiniert:
Das ist eine vollständige Testbench für einen 4-Bit-Counter. Drücke Run und du siehst den Counter aus dem Reset herauskommen, hochzählen, solange er aktiviert ist, pausieren, wenn enable fällt, wieder aufnehmen, wenn es steigt, und dann stoppen.
Drei Dinge zur Struktur:
- Drei verschiedene Aktivitätsorte: der Taktgenerator (ein
always), der Stimulus (eininitial) und der Monitor (ein weiteresalways). Jeder hat einen Job. - Die
#-Delays im Stimulus-Block messen Zeiteinheiten - keine Taktzyklen. Ist deine Taktperiode 10 Einheiten, dann ist#10exakt ein Zyklus. Manche Testbenches nutzen stattdessen@(posedge clk), was unabhängig von der Periode einen Zyklus vorrückt. - Der Monitor nutzt
$displayaus einemalways @(posedge clk)-Block. Das druckt jeden Zyklus. Für ausgefeiltere Ausgabe wechsle zu$monitor(gleich behandelt).
Zyklusbasierter Stimulus
Manchmal liest sich "warte N Zyklen" besser als "warte N Zeiteinheiten":
initial begin
@(posedge clk); // warte auf nächste Taktflanke
reset = 0;
repeat (5) @(posedge clk); // warte 5 weitere Zyklen
enable = 1;
repeat (8) @(posedge clk);
enable = 0;
repeat (10) @(posedge clk);
$finish;
end
@(posedge clk) blockiert bis zur nächsten steigenden Taktflanke. repeat (N) @(posedge clk); wartet N Zyklen. Dieser Stil ist unabhängig von der Taktperiode - änderst du die Taktfrequenz, tut der Stimulus zyklenmäßig dasselbe.
Für frühe Testbenches sind #-Delays einfacher. Für Produktions-Testbenches, die bei verschiedenen Taktraten laufen können, ist zyklusbasiert der sicherere Stil.
Selbstprüfende Tests
Die Testbench bisher druckt, was passiert ist, und überlässt dir das Lesen der Ausgabe. Eine selbstprüfende Testbench prüft stattdessen die Ausgabe und meldet Pass/Fail:
initial begin
#1;
a = 10; b = 20;
#1;
if (sum !== 30) begin
$display("FAIL: 10 + 20 = %0d (erwartet 30)", sum);
$finish;
end
a = 250; b = 5;
#1;
if (sum !== 255) begin
$display("FAIL: 250 + 5 = %0d (erwartet 255)", sum);
$finish;
end
$display("PASS");
$finish;
end
Nimm !== (den Case-Inequality-Operator) zur Sicherheit - er liefert kein x, wenn einer der Operanden Unknown-Bits hat. Das Muster: Eingänge treiben, auf Einschwingen warten, mit Erwartung vergleichen, Pass oder Fail melden.
Selbstprüfung ist in Regressions-Suites Gold wert: Tausende Tests können unbeaufsichtigt laufen, und nur Fehlschläge brauchen Aufmerksamkeit.
Wie es weitergeht
Du hast jetzt die Testbench-Form, die für jedes Modul reicht. Die nächsten Docs behandeln die Werkzeuge, die in die Testbench gehören: Display und Monitor für reichere Textausgabe, Dumpfile und VCD für grafisches Waveform-Debugging und Timescale und Delays für die Steuerung, wie Simulationszeit auf Wanduhrzeit abbildet.
Häufig gestellte Fragen
Was ist eine Testbench in Verilog?
Eine Testbench ist ein Verilog-Modul - typischerweise ohne Ports -, dessen einziger Zweck darin besteht, ein Design under Test (DUT) auszuüben. Sie instanziiert das DUT, erzeugt einen Takt, treibt Stimulus über initial- und always-Blöcke, beobachtet Ausgaben mit $display/$monitor, dumpt optional eine VCD-Waveform und beendet die Simulation mit $finish.
Wie erzeuge ich einen Takt in einer Verilog-Testbench?
Das Standardmuster ist eine einzelne Zeile: always #5 clk = ~clk; (mit vorher deklariertem reg clk = 0;). Das schaltet clk alle 5 Simulationszeiteinheiten um und ergibt eine 10-Einheiten-Periode (ein 100-MHz-Takt, wenn deine Timescale in Nanosekunden ist). Die #5 ist die Halb-Periode - die Hälfte der Zeit ist clk high, die andere Hälfte low.
Was ist ein DUT in Verilog?
DUT steht für Design Under Test - das Modul, das die Testbench ausübt. Konvention ist, die DUT-Instanz in der Testbench dut oder u_dut zu nennen: my_module dut(.clk(clk), .reset(reset), .in(in), .out(out));. Der Name ist nur ein Label; wichtig ist, dass es instanziiert ist, seine Ports an Testbench-Signale angeschlossen sind und die Testbench diese Signale treibt.
Wie lange sollte eine Verilog-Simulation laufen?
Lange genug, um alles auszuüben, was du verifizieren willst, dann $finish. Die meisten Testbenches setzen ein explizites Zeitlimit (#1000 $finish), damit die Simulation nicht hängt und auf ein Ereignis wartet, das nie kommt. Innerhalb dieses Fensters treibst du deinen Stimulus, lässt das DUT sich einschwingen und fügst idealerweise selbstprüfende if-Anweisungen ein, die FAIL drucken, wenn die Ausgabe nicht den Erwartungen entspricht.