10 min read | Intermediate Level

The linear testbench from the previous article had every input value typed directly into the testbench code. That works for five test cases. It falls apart when you need five hundred — and it creates a tighter problem: the testbench code and the test data are the same file, so updating one risks breaking the other.

The file-based testbench solves this by separating stimulus from testbench logic. Input vectors live in an external file. The testbench reads them in, applies them to the DUT, and optionally writes the DUT's responses back out for offline comparison against a golden reference. The testbench code stays the same across test runs — only the input file changes.

What You'll Learn

  • How to load stimulus vectors from a hex file using $readmemh
  • How to write DUT outputs to a file using $fdisplay for offline comparison
  • Why separating test data from testbench code improves reproducibility and sharing
  • Where the file-based approach still falls short

The Same DUT: 4-bit Adder

We continue with the same 4-bit adder from the linear testbench article — two 4-bit operands a and b, a carry-in cin, producing sum and cout. Using the same DUT makes the comparison between styles direct: we are solving the same verification problem, just with a better approach.


// DUT: 4-bit adder (unchanged from linear testbench article)
module adder (
    input  [3:0] a,
    input  [3:0] b,
    input        cin,
    output [3:0] sum,
    output       cout
);
    assign {cout, sum} = a + b + cin;
endmodule

Separating Stimulus into a File

Instead of typing input values directly into the testbench, we store them in a plain text hex file. The Verilog system task $readmemh reads this file into a memory array at the start of simulation. The testbench then steps through the array, driving one row of values to the DUT per clock cycle.

The Input Stimulus File

Each line of the file holds one test vector. For our adder we need three values per test: a, b, and cin. We pack all three into a single 9-bit word — bits [8:5] for a, bits [4:1] for b, bit [0] for cin — and write each word as a two-digit hex value on its own line.

The file is named stimulus.mem. Lines starting with // are comments and are ignored by $readmemh:


// stimulus.mem
// Format: {a[3:0], b[3:0], cin} packed into 9 bits, written as hex
// a=0,  b=0,  cin=0  → 9'b0000_0000_0 → 000
000
// a=3,  b=5,  cin=0  → {4'h3, 4'h5, 1'b0} → 06A
06A
// a=7,  b=8,  cin=0  → {4'h7, 4'h8, 1'b0} → 0F0
0F0
// a=8,  b=8,  cin=0  → {4'h8, 4'h8, 1'b0} → 110
110
// a=15, b=15, cin=1  → {4'hF, 4'hF, 1'b1} → 1FF
1FF
// a=0,  b=0,  cin=1  → {4'h0, 4'h0, 1'b1} → 001
001
// a=15, b=14, cin=0  → {4'hF, 4'hE, 1'b0} → 1DC
1DC

$readmemh reads hexadecimal values. Its counterpart $readmemb reads binary values — useful when the vector format is already in binary from another tool. Both work identically otherwise.

The Testbench

The testbench declares a memory array sized to hold all stimulus vectors, loads the file into it with $readmemh, then loops through each entry — unpacking the fields, driving the DUT, and printing results.


module tb_adder_file;

    // ── DUT signals ────────────────────────────────────────────────
    reg  [3:0] a;
    reg  [3:0] b;
    reg        cin;
    wire [3:0] sum;
    wire       cout;

    // ── DUT instantiation ──────────────────────────────────────────
    adder dut (
        .a   (a),
        .b   (b),
        .cin (cin),
        .sum (sum),
        .cout(cout)
    );

    // ── Clock ──────────────────────────────────────────────────────
    reg clk = 0;
    always #5 clk = ~clk;

    // ── Stimulus memory ────────────────────────────────────────────
    // Each entry is 9 bits: {a[3:0], b[3:0], cin}
    // 16 slots is plenty for our 7-vector file
    reg [8:0] stimulus [0:15];

    // ── Output file handle ─────────────────────────────────────────
    integer out_file;

    integer i;  // loop index

    initial begin
        // Load stimulus vectors from hex file into the array
        // $readmemh fills stimulus[0], stimulus[1], ... in order
        $readmemh("stimulus.mem", stimulus);

        // Open an output file to record DUT responses
        out_file = $fopen("results.out", "w");
        if (out_file == 0) begin
            $fatal(1, "ERROR: could not open results.out");
        end

        // Write a header line to the output file
        $fdisplay(out_file, "a    b    cin | sum  cout");
        $fdisplay(out_file, "------------------------");

        // Wait for two clock cycles before driving stimulus
        @(posedge clk);
        @(posedge clk);

        // Step through each stimulus vector
        // $size returns the number of entries loaded by $readmemh
        for (i = 0; i < $size(stimulus); i = i + 1) begin

            // Skip uninitialised entries (unfilled slots show as 'x)
            if (^stimulus[i] === 1'bx) disable;

            // Unpack the 9-bit vector into individual DUT inputs
            a   = stimulus[i][8:5];   // bits 8..5 → operand a
            b   = stimulus[i][4:1];   // bits 4..1 → operand b
            cin = stimulus[i][0];     // bit  0    → carry-in

            // Wait one clock edge for the combinational output to settle
            @(posedge clk);

            // Print to simulation log (visible in console)
            $display("a=%0d b=%0d cin=%0d | sum=%0d cout=%0d",
                      a, b, cin, sum, cout);

            // Write the same result to the output file for offline comparison
            $fdisplay(out_file, "%04b %04b  %b   | %04b  %b",
                       a, b, cin, sum, cout);
        end

        $fclose(out_file);
        $display("Done. Results written to results.out");
        $finish;
    end

endmodule

Simulation Log Output

Output
a=0  b=0  cin=0 | sum=0  cout=0
a=3  b=5  cin=0 | sum=8  cout=0
a=7  b=8  cin=0 | sum=15 cout=0
a=8  b=8  cin=0 | sum=0  cout=1
a=15 b=15 cin=1 | sum=15 cout=1
a=0  b=0  cin=1 | sum=1  cout=0
a=15 b=14 cin=0 | sum=13 cout=1
Done. Results written to results.out

The Output File: results.out

The file written by $fdisplay looks like this. A software reference model — a Python script, a C program, or a spreadsheet formula — can read both this file and the golden expected values and flag any row where they differ, without involving the simulator at all:


// results.out  (written by the testbench during simulation)
a    b    cin | sum  cout
------------------------
0000 0000  0   | 0000  0
0011 0101  0   | 1000  0
0111 1000  0   | 1111  0
1000 1000  0   | 0000  1
1111 1111  1   | 1111  1
0000 0000  1   | 0001  0
1111 1110  0   | 1101  1

Quick Check

Q: The stimulus file stores a, b, and cin packed into a single 9-bit word. For the test vector a=7, b=8, cin=0, walk through how the hex value 0F0 is computed and how the testbench unpacks it back into three separate signals.

Show Answer

Packing: a=7 is 4'b0111, b=8 is 4'b1000, cin=0 is 1'b0. Concatenated: 9'b0111_1000_0 = 9'h0F0.
Unpacking: The testbench reads bits [8:5] → 4'b0111 = 7 → a. Bits [4:1] → 4'b1000 = 8 → b. Bit [0] → 1'b0 → cin.

Why This Is Better Than the Linear Testbench

Test data and testbench code are separate. Adding 100 more test vectors means editing stimulus.mem — the testbench code does not change at all. A designer, a software engineer, or an automated script can generate new test vectors without touching Verilog.

Vectors are shareable and version-controlled. The .mem file can live in version control alongside the RTL. Different engineers can maintain different vector sets for different features. Running the same file on two different simulators — or two different RTL revisions — produces results that are directly comparable line-by-line.

Reproducibility is guaranteed. Every run with the same stimulus.mem applies exactly the same sequence in exactly the same order. There is no seed dependency, no timing sensitivity, no environmental variation. A failure is always reproducible with the same file.

Offline comparison becomes possible. The results.out file can be diffed against a golden reference file from a software model or a previous known-good simulation. A one-line shell command — diff results.out golden.out — flags any discrepancy without opening a waveform viewer.

Quick Check

Q: A software team delivers a new stimulus.mem file with 2,000 test vectors generated from a C reference model. They also provide a golden.out file with the expected results for each vector. What is the complete verification workflow from file delivery to pass/fail result, without writing any new Verilog?

Show Answer

1. Drop the new stimulus.mem into the simulation directory — the testbench picks it up automatically via $readmemh.
2. Run the simulation — results.out is written with the DUT's actual responses.
3. Run diff results.out golden.out — any line with a mismatch is a failing test vector, identified by line number.
No testbench changes, no waveform inspection, no manual comparison. The only human decision needed is what to do about any reported differences.

Reading Binary Files with $readmemb

When a tool generates stimulus in binary rather than hex — common for simple control signals or when the bit layout is clearer in binary — $readmemb works identically to $readmemh, just reading binary digits instead of hex. The same 9-bit vector for a=3, b=5, cin=0 would appear in the file as 001100000 instead of 06A.


// For a binary-format file, replace:
$readmemh("stimulus.mem", stimulus);

// with:
$readmemb("stimulus.bin", stimulus);

// Everything else in the testbench stays exactly the same.

Use $readmemh when your stimulus comes from a tool that outputs hex — most EDA tools and C/Python reference models default to hex. Use $readmemb when working with raw bit patterns where binary is more readable, such as control word encodings or FSM state sequences.

Limitations

The file-based testbench solves the data-separation problem but introduces its own constraints that become significant as designs grow more complex.

No feedback loop. Every input vector is applied regardless of what the DUT does. If the DUT asserts a stall or not_ready signal, the testbench does not notice — it drives the next vector on the next clock edge anyway, producing illegal stimulus. Protocols that require the testbench to wait for a DUT response before sending the next transaction cannot be handled by a file-based approach.

Vectors must be pre-computed. The entire test sequence is decided before simulation starts. There is no way to generate the next input based on the DUT's current output, which means conditional scenarios — "if the DUT returns an error, retry the transaction" — are impossible without building a more intelligent testbench.

Packing and unpacking adds fragility. Cramming multiple signals into a single word (as we did with the 9-bit vector) creates a manual encoding that must stay in sync between the file generator, the testbench unpacking logic, and any offline comparison tool. A width change in any DUT port silently breaks the packing scheme.

Common Mistakes

Mistake #1: Declaring the memory array smaller than the number of vectors in the file.

Wrong: Declaring reg [8:0] stimulus [0:4] (5 slots) and loading a file with 7 vectors — $readmemh fills the first 5 entries and silently discards the remaining 2, with no error message.
Right: Always declare the array larger than the maximum expected file size, or use a parameter for the depth and size it conservatively. Check with $display("Loaded %0d vectors", $size(stimulus)) to confirm the count matches expectations.

Why? $readmemh does not report an overflow — it fills what fits and stops. Missing vectors mean missing test coverage with no indication anything went wrong.

Mistake #2: Using $readmemh on a file with inconsistent line formatting.

Wrong: The .mem file has some lines with comment text after the hex value on the same line (e.g. 06A // a=3 b=5), causing $readmemh to misparse the value on those lines.
Right: Put comments on their own lines starting with //, never inline after a data value. Keep each data line as a clean hex value with no trailing characters.

Why? The IEEE standard for $readmemh supports /* */ and // comments, but inline comments after a value on the same line are not universally supported across simulators and can cause silent misreads or simulator-specific behaviour.

Mistake #3: Forgetting to close the output file before $finish.

Wrong: Calling $finish without first calling $fclose(out_file) — the output file is either empty or truncated because the simulator flushes and closes file handles at exit, and some simulators do not flush buffered writes before terminating.
Right: Always call $fclose on every open file handle before $finish, in the same initial block, immediately before the $finish call.

Why? File writes in simulation are often buffered. Without an explicit close, the last several lines of $fdisplay output may never reach the file on disk — making the output file incomplete and the offline comparison unreliable.

Practice Exercise

Challenge: Add an Inline Checker

The testbench above writes results to a file but does not check them during simulation — it relies on an offline diff after the run. Extend the testbench loop to also check the DUT output at simulation time, calling $error if the result does not match the expected value.

The expected result for each vector is simply a + b + cin — the same arithmetic the DUT performs. Compute it inside the loop and compare it against {cout, sum}.

Hint

Declare a 5-bit variable expected (one extra bit for the carry). Assign expected = a + b + cin after unpacking the vector. Then compare {cout, sum} against expected using !==. If they differ, call $error with a message showing the input values, expected output, and actual output.

Sample Solution

    // Inside the for loop, after @(posedge clk):

    reg [4:0] expected;  // 5-bit: bit[4]=expected carry, bits[3:0]=expected sum

    // Compute expected result using the same arithmetic as the DUT
    expected = a + b + cin;

    // Compare DUT output against expected — use !== to catch X/Z
    if ({cout, sum} !== expected) begin
        $error("MISMATCH: a=%0d b=%0d cin=%0d | expected=%05b got=%b%04b",
                a, b, cin, expected, cout, sum);
    end

With this addition the testbench is now partially self-checking — it flags mismatches during simulation rather than waiting for an offline diff. This is the first step toward the self-checking testbench style covered later in this chapter.

Summary

  • A file-based testbench separates test data from testbench logic — stimulus vectors live in an external .mem file, loaded at simulation start with $readmemh or $readmemb
  • DUT outputs can be written to a file using $fdisplay and compared offline against a golden reference — enabling pass/fail checking without waveform inspection
  • Key advantages over the linear testbench: test data is shareable, version-controllable, and updatable without modifying Verilog; runs are perfectly reproducible with the same file
  • Core limitation: the testbench cannot react to DUT responses — all vectors are pre-computed and driven blindly, making protocol handshake interfaces impossible to handle correctly
  • Always size the stimulus array generously, keep .mem files clean of inline comments, and close all file handles before $finish
Did you find what you were looking for?

Quiz

No quiz questions available for this article.

Cron Job Starts