Writing to Files
Everything $display prints is gone when the terminal closes. A regression needs logs that persist: something to diff against a known-good run, to archive, to parse, to attach to a failure report. Verilog’s file tasks mirror the display family and write to named files, with an interface borrowed loosely from C.
Open and close
integer log_file;
initial begin
log_file = $fopen("results.log", "w");
if (log_file == 0)
$fatal(1, "could not open results.log");
// ...
$fclose(log_file);
end
$fopen with a mode string, "w", "a" or "r", returns a file descriptor, or 0 if the file could not be opened; check it, because writes to a zero descriptor go nowhere without a word. Use "w" for a log, so that each run starts clean. Close before $finish; most simulators flush on exit, but a process killed by a build system may not.
Writing
$fdisplay(log_file, "At %t: input=%h output=%h expected=%h", $time, input_val, actual, expected);
$fwrite(log_file, "no newline");
$fstrobe and $fmonitor exist too, with the same timing as their console versions, and are used less.
Console and file at once
$fopen called without a mode string returns a different thing: a multichannel descriptor, a 32-bit word with one bit per open file, in which bit 0 is the console. OR two together and a write goes to both:
integer log_mcd, both;
initial begin
log_mcd = $fopen("results.log"); // no mode: multichannel descriptor
both = log_mcd | 32'h0000_0001; // bit 0 is standard output
$fdisplay(both, "At %t: test %0d PASSED", $time, test_num);
end
The two kinds must not be mixed: a descriptor from $fopen("f", "w") is a plain handle and cannot be ORed. The multichannel form allows up to 31 files at once and is the usual way to keep the console quiet while the file gets everything.
Reading
$fgetc reads a character, $fscanf reads formatted data, and $feof reports the end:
integer input_file;
initial begin
input_file = $fopen("stimulus.txt", "r");
while (!$feof(input_file)) begin
$fscanf(input_file, "%h\n", data_val);
// apply data_val
end
$fclose(input_file);
end
For a plain list of hex values, $readmemh in the next article is simpler. $fscanf earns its place when lines have mixed content.
A logging testbench
`timescale 1ns / 1ps
module incrementer (
input wire [3:0] in,
output wire [3:0] out
);
assign out = in + 4'd1;
endmodule
module tb_incrementer();
reg [3:0] test_in;
wire [3:0] test_out;
integer log_mcd, log_and_console;
integer error_count, test_count;
integer i;
reg [3:0] expected;
incrementer dut (.in(test_in), .out(test_out));
initial begin
$dumpfile("tb_incrementer.vcd");
$dumpvars(0, tb_incrementer);
$timeformat(-9, 0, " ns", 8);
log_mcd = $fopen("results.log");
if (log_mcd == 0)
$fatal(1, "cannot open results.log");
log_and_console = log_mcd | 32'h1;
$fdisplay(log_mcd, "=== incrementer results ===");
error_count = 0;
test_count = 0;
for (i = 0; i < 16; i = i + 1) begin
test_in = i[3:0];
expected = i[3:0] + 4'd1; // wraps at 15
#10;
test_count = test_count + 1;
if (test_out !== expected) begin
$fdisplay(log_and_console, "%t in=%0d expected=%0d got=%0d FAIL", $time, test_in, expected, test_out);
error_count = error_count + 1;
end else begin
$fdisplay(log_mcd, "%t in=%0d expected=%0d got=%0d pass", $time, test_in, expected, test_out);
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
Failures go to both destinations; passes go only to the file, so the console shows failures and the verdict and nothing else, and the file has every vector. Afterwards:
diff results.log golden_results.log && echo "regression clean" || echo "regression FAILED"
Use a bare filename, not an absolute path; the file lands in the directory the simulator ran from, and the script controls that. Production flows put the test name and a timestamp in the filename so parallel runs do not overwrite each other, passed in from the command line, which is a later subject.
Getting test data into the run is the next step: $readmemh loads a whole array of vectors from a text file.