$fopen returns an integer descriptor, and 0 means the open failed. Mode w truncates, a appends and r reads, so check the descriptor every time.
$fdisplay appends a newline to each line. $fwrite writes exactly what the format produces, so it suits partial lines.
Close every file before the run ends. Unclosed files can lose their last buffered lines when the simulator exits.
Call | Newline | Use for |
|---|---|---|
| Yes, one per call | CSV rows and log lines |
| No | Partial lines and prompts |
One write can reach the file and the console together, but only through a multichannel descriptor (MCD). $fopen(name) with no mode returns an MCD: bit 31 clear, one bit per channel, and bit 0 is the console. MCDs OR together.
A descriptor from $fopen(name, "w") has bit 31 set and cannot be OR-ed. ORing 1 into it makes a different descriptor, so the line goes to the wrong place or nowhere. To tee such a file, write it twice.
// snippet
// Tee one write to the log file and the console together.
integer log_mcd;
log_mcd = $fopen("run.log"); // no mode: a multichannel descriptor
$fdisplay(log_mcd | 1, "checkpoint: %0d txns done", done); // bit 0 = console$fgets reads one line and $sscanf parses it. A malformed line fails alone, and the next line still parses.
$fscanf reads straight from the file instead. One bad token desynchronizes everything after it, so production parsers prefer the per-line form.
Skip comments and short reads explicitly. The testbench below drops any line whose conversion count is wrong.
$readmemh loads a whole hex array in one call, and $readmemb does the same in binary. Values are whitespace-separated, comments are allowed, and @address markers set the next load index.
File item | Meaning |
|---|---|
Hex or binary words | Values for consecutive indices |
| Next load index, as an array index |
Start and end arguments | Only that index range loads |
Array indices, not byte addresses, follow the @ marker. A marker of @0040 loads index 64, and the optional range arguments bound the load the same way.
Large memories preload through the hierarchy instead of the bus. The fragment below names DUT internals, so it is not run in this course's checks.
// not-run: needs a testbench harness with a matching DUT hierarchy
string mem_image;
initial begin
if ($value$plusargs("MEM_IMAGE=%s", mem_image)) begin
// Hierarchical reference into the DUT memory array
$readmemh(mem_image, tb_top.dut.u_sram.mem_array);
$display("preloaded %s into dut.u_sram", mem_image);
end
// else: memory powers up uninitialized, which is also a valid test
end
// Run: simv +MEM_IMAGE=images/boot_rom.hex
// Keep all such paths in one file: an RTL rename breaks them all at once.Hand-written images stop scaling early. The generator below builds the image from the scoreboard model. It is a class with no top module, so it is not run here either.
// not-run: class-only listing with no top module to run (Verilator 5.048 compiles it)
class mem_model;
bit [31:0] mem [bit [31:0]]; // sparse associative model
function void randomize_region(bit [31:0] base, int words);
for (int i = 0; i < words; i++)
mem[base + i] = $urandom();
endfunction
// Write a $readmemh-compatible image (sparse via @address)
function void write_image(string fname);
int fd = $fopen(fname, "w");
bit [31:0] addr;
if (fd == 0) $fatal(1, "cannot write %s", fname);
if (mem.first(addr)) begin
do begin
$fdisplay(fd, "@%0h", addr); // index marker
$fdisplay(fd, "%h", mem[addr]);
end while (mem.next(addr));
end
$fclose(fd);
endfunction
endclassThe run closes with a dump and a compare. $writememh writes the array out, and an in-testbench loop with !== reports addresses that differ without X masking the result.
File-driven stimulus replays recorded vectors the same way. That driver needs a bus_if interface and a DUT that are not shown, so it stays a documented pattern rather than a checked block.
// not-run: needs the bus_if interface and a DUT, which are not shown
// File-driven stimulus: replays recorded or generated vectors.
// File format per line: <delay_cycles> <write> <addr> <data>
class file_driver;
virtual bus_if vif;
task run(string fname);
int fd, status, delay;
bit wr;
bit [31:0] addr, data;
string line;
fd = $fopen(fname, "r");
if (fd == 0) $fatal(1, "stim file %s missing", fname);
while ($fgets(line, fd)) begin
status = $sscanf(line, "%d %b %h %h", delay, wr, addr, data);
if (status != 4) continue;
repeat (delay) @(posedge vif.clk);
vif.valid <= 1; vif.write <= wr;
vif.addr <= addr; vif.wdata <= data;
@(posedge vif.clk iff vif.ready);
vif.valid <= 0;
end
$fclose(fd);
endtask
endclassIndex 256. The marker is hex, so 0x100 is 256, and it counts array indices rather than bytes.
if (back[i] != mem[i]) errors++; and passes with X left in memory. What hid the corruption?A != with an X operand yields X, and an X condition is false, so the error is never counted. The !== form distinguishes X from every other value.
The $fclose. Buffered output flushes on close, so every file closes before the run ends.
One block proves four file flows: a CSV log round-trip over 8 lines, a sparse image preload with @ markers, and a dump plus reload compare over 8 words.
module tb_fileio;
localparam int N = 8;
reg [31:0] exp_a [0:7];
reg [31:0] exp_d [0:7];
reg [31:0] mem [0:7];
reg [31:0] back [0:7];
integer fd;
integer status;
integer errors = 0;
integer i;
string line;
reg [31:0] addr;
reg [31:0] data;
initial begin
for (i = 0; i < N; i++) begin
exp_a[i] = 32'h00001000 + i * 4;
exp_d[i] = 32'h0000A000 + i;
end
// Phase 1: log N vectors as hex pairs, one per line.
fd = $fopen("txn_log.csv", "w");
if (fd == 0) errors++;
else begin
for (i = 0; i < N; i++)
$fdisplay(fd, "%h %h", exp_a[i], exp_d[i]);
$fclose(fd);
end
// Phase 2: read the lines back and compare each field.
fd = $fopen("txn_log.csv", "r");
if (fd == 0) errors++;
else begin
for (i = 0; i < N; i++) begin
if ($fgets(line, fd) == 0) errors++;
else begin
status = $sscanf(line, "%h %h", addr, data);
if (status != 2) errors++;
else if (addr !== exp_a[i] || data !== exp_d[i]) errors++;
end
end
$fclose(fd);
end
// Phase 3: write a sparse hex image, then preload it.
for (i = 0; i < N; i++) mem[i] = 32'h0;
fd = $fopen("img.hex", "w");
if (fd == 0) errors++;
else begin
$fdisplay(fd, "%h", 32'h11111111); // index 0
$fdisplay(fd, "%h", 32'h22222222); // index 1
$fdisplay(fd, "@0004"); // skip to index 4
$fdisplay(fd, "%h", 32'h44444444); // index 4
$fdisplay(fd, "%h", 32'h55555555); // index 5
$fclose(fd);
end
$readmemh("img.hex", mem);
if (mem[0] !== 32'h11111111) errors++;
if (mem[1] !== 32'h22222222) errors++;
if (mem[2] !== 32'h0) errors++;
if (mem[3] !== 32'h0) errors++;
if (mem[4] !== 32'h44444444) errors++;
if (mem[5] !== 32'h55555555) errors++;
if (mem[6] !== 32'h0) errors++;
if (mem[7] !== 32'h0) errors++;
// Phase 4: dump the array and reload it elsewhere.
$writememh("dump.hex", mem);
$readmemh("dump.hex", back);
for (i = 0; i < N; i++)
if (back[i] !== mem[i]) errors++;
if (errors == 0) $display("PASS: csv round-trip on 8 lines and image round-trip on 8 words");
else $fatal(1, "FAIL: %0d file checks did not hold", errors);
$finish;
end
endmodule