Module 6: Verilog for Testbench & Simulation ยท 6.2 Simulation-Only Constructs

$readmemh and $readmemb

An 8-bit ALU with four operations needs operand pairs at the edges: zeros, all ones, overflow, carry, sign boundaries. Fifty such vectors as literals in an initial block is a wall of hex nobody can review, and changing one means editing Verilog. The vectors are data and the testbench is infrastructure, and $readmemh keeps them apart: the vectors live in a text file, editable, scriptable, diffable, and the testbench loads them at the start with one line.

The file

One hex value per whitespace-separated field, // comments allowed, blank lines ignored:

// counter_vectors.hex
0a
ff
80
00

@address jumps:

@0000
aa bb cc dd     // addresses 0 to 3

@0010
11 22 33        // addresses 16 to 18; 4 to 15 stay x

$readmemb is the same with binary digits.

Loading

reg [7:0] test_vectors [0:255];

initial begin
    $readmemh("vectors.hex", test_vectors);
end

Values fill the array from index 0 until the file or the array runs out. Entries the file does not reach stay x; a file with more values than the array holds produces a warning. Optional start and end addresses load a range:

$readmemh("data.hex", mem, 256, 511);    // only mem[256] to mem[511]

Records

For vectors with several fields, pack them into one wide word and unpack in the loop, or keep parallel arrays, one for inputs and one for expected outputs:

reg [17:0] inputs   [0:99];    // {op[1:0], a[7:0], b[7:0]}
reg [7:0]  expected [0:99];

initial begin
    $readmemh("alu_inputs.hex",   inputs);
    $readmemh("alu_expected.hex", expected);
end

Parallel arrays let you regenerate the expected file from a fixed reference model without touching the inputs.

In design files

$readmemh is the one initial-block idiom that FPGA synthesis honours. An array loaded from a file becomes block RAM with those contents in the bitstream, so the same file is the truth for simulation and for the hardware:

module sine_lut (
    input  wire [7:0] addr,
    output reg  [7:0] data
);
    reg [7:0] rom [0:255];

    initial begin
        $readmemh("sine_table.hex", rom);
    end

    always @(*)
        data = rom[addr];
endmodule

An ALU testbench from a file

Each line is one 32-bit word: the top byte holds the opcode in its low two bits, then a, then b, then the expected result.

// alu_vectors.hex: op a b expected
000A050F    // ADD 10 + 5 = 15
00FF0100    // ADD 255 + 1 wraps to 0
00807FFF    // ADD 128 + 127 = 255
00000000
010A0505    // SUB 10 - 5 = 5
010001FF    // SUB 0 - 1 wraps to 255
01FFFF00
02F00F00    // AND
02FFAAAA
03F00FFF    // OR
03000000
03AA55FF
`timescale 1ns / 1ps

module alu_8bit (
    input  wire [1:0] op,
    input  wire [7:0] a,
    input  wire [7:0] b,
    output reg  [7:0] result
);
    always @(*) begin
        case (op)
            2'b00:   result = a + b;
            2'b01:   result = a - b;
            2'b10:   result = a & b;
            2'b11:   result = a | b;
            default: result = 8'hxx;
        endcase
    end
endmodule

module tb_alu_readmem();

    reg  [1:0] op;
    reg  [7:0] a, b;
    wire [7:0] result;

    reg [31:0] vectors [0:255];

    integer error_count, test_count;
    integer log_mcd, log_and_console;
    integer i;

    alu_8bit dut (.op(op), .a(a), .b(b), .result(result));

    initial begin
        #50_000;
        $fatal(1, "TIMEOUT");
    end

    initial begin
        $dumpfile("tb_alu_readmem.vcd");
        $dumpvars(0, tb_alu_readmem);
        $timeformat(-9, 0, " ns", 8);

        log_mcd = $fopen("alu_results.log");
        if (log_mcd == 0)
            $fatal(1, "cannot open alu_results.log");
        log_and_console = log_mcd | 32'h1;

        $readmemh("alu_vectors.hex", vectors);

        error_count = 0;
        test_count  = 0;

        for (i = 0; i < 256; i = i + 1) begin
            if (vectors[i] !== 32'hxxxxxxxx) begin      // stop at the unfilled entries
                op = vectors[i][25:24];
                a  = vectors[i][23:16];
                b  = vectors[i][15:8];
                #10;

                test_count = test_count + 1;

                if (result !== vectors[i][7:0]) begin
                    $fdisplay(log_and_console, "%t  op=%0d a=%02h b=%02h expected=%02h got=%02h  FAIL",
                              $time, op, a, b, vectors[i][7:0], result);
                    error_count = error_count + 1;
                end else begin
                    $fdisplay(log_mcd, "%t  op=%0d a=%02h b=%02h expected=%02h got=%02h  pass",
                              $time, op, a, b, vectors[i][7:0], result);
                end
            end
        end

        if (error_count == 0)
            $fdisplay(log_and_console, "RESULT: PASSED, all %0d tests.", test_count);
        else
            $fdisplay(log_and_console, "RESULT: FAILED, %0d of %0d tests.", error_count, test_count);

        $fclose(log_mcd);
        $finish;
    end

endmodule

The guard vectors[i] !== 32'hxxxxxxxx stops the loop at the end of the real data, so adding lines to the file adds tests with no change to the Verilog. It has to be !==: 4'hx == 4'hx is x, not 1, and only the case-equality operators compare x as a value.

This is the start of data-driven testing: a script writes the file and the testbench reads it, which is the same idea that scales up to constrained-random sequences later. The last article of this section is about the three ways a testbench waits.

โ† Writing to Files #delay, @(event) and wait() โ†’