Part 7 · Advanced & Integration · Intermediate

File I/O

$fopen/$fclose, $fdisplay/$fwrite logging, $fscanf/$fgets parsing, $readmemh/$readmemb preload, and a stimulus-from-file driver.

Opening, writing, closing

$fopen returns an integer file descriptor — zero means the open failed, so always check it. Mode "w" truncates and writes, "a" appends, "r" reads. $fdisplay writes a line with a newline appended; $fwrite writes exactly what the format produces, so you control line breaks. Unclosed files may lose buffered output when simulation ends — close them in a final block.

systemverilog
int log_fd;

initial begin
  log_fd = $fopen("txn_log.csv", "w");
  if (log_fd == 0) $fatal(1, "cannot open txn_log.csv");
  $fdisplay(log_fd, "time,kind,addr,data");   // CSV header
end

// Per-transaction logging from the monitor
function void log_txn(string kind, bit [31:0] addr, bit [31:0] data);
  $fdisplay(log_fd, "%0t,%s,%h,%h", $time, kind, addr, data);
endfunction

final begin
  $fclose(log_fd);   // flush buffers — do not skip this
end

// MCD trick: fd from $fopen has bit 31 set (regular descriptor).
// Writing to (log_fd | 1) sends output to the file AND stdout,
// because descriptor 1 is the console channel.

Reading: $fgets, $fscanf, and $readmemh

systemverilog
// Line-oriented parse: $fgets reads a line, $sscanf parses it
int fd, status;
string line;
bit [31:0] addr, data;

initial begin
  fd = $fopen("stimulus.txt", "r");
  if (fd == 0) $fatal(1, "stimulus.txt missing");
  while ($fgets(line, fd)) begin
    if (line.substr(0, 0) == "#") continue;        // skip comments
    status = $sscanf(line, "%h %h", addr, data);
    if (status != 2) continue;                     // skip malformed lines
    $display("stim: addr=%h data=%h", addr, data);
  end
  $fclose(fd);
end

// $fscanf reads directly from the file — convenient, but a malformed
// token desynchronizes everything after it. $fgets + $sscanf lets you
// recover per line, which is why production parsers prefer it.

$readmemh / $readmemb format

For memory images, $readmemh (hex) and $readmemb (binary) load a whole array in one call. The file holds whitespace-separated values, optional comments, and optional @address markers that set where the next value lands — perfect for sparse images. Optional start/end arguments restrict which part of the array is loaded.

systemverilog
logic [31:0] mem [0:1023];

initial begin
  $readmemh("boot.hex", mem);                 // fill from index 0
  $readmemh("patch.hex", mem, 512, 767);      // only load this range
end

// boot.hex:
//   // comments allowed
//   @0040            <- @address in HEX, sets next load index
//   DEADBEEF 00000001 0000FFFF
//   @0100
//   CAFEF00D

A stimulus-from-file driver

systemverilog
// 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
endclass
diagram
FILE TRAFFIC IN A TESTBENCH

   IN                                          OUT
   ──                                          ───
   stimulus.txt ──► file_driver ──► DUT ──► monitor ──► txn_log.csv
   boot.hex ──► $readmemh ──► dut.mem                └─► waves (lesson 5)
                                  │
                                  ▼ end of test
                            mem dump ──► compare vs golden (lesson 4)

Key takeaways

  • Always check the $fopen descriptor and $fclose in a final block — unflushed buffers lose data.

  • $fdisplay appends a newline; $fwrite does not — pick per use, and CSV logs make post-processing trivial.

  • Prefer $fgets + $sscanf over raw $fscanf — per-line recovery from malformed input.

  • $readmemh loads whole arrays with @address sparse markers and optional range limits.

Common pitfalls

  • Forgetting $fclose — the last buffered lines of the log vanish when simulation exits.

  • $readmemh file values wider than the array element — high bits silently truncated by most tools.

  • Treating @address markers in readmem files as byte addresses — they are array indices.

  • Opening a file per transaction instead of once — file-system overhead dominates simulation time.

Interview angle

Common questions: $fdisplay vs $fwrite (newline handling); why $fgets+$sscanf beats $fscanf for robust parsing; the $readmemh file format including @address; and what the descriptor returned by $fopen means — including the (fd | 1) trick for tee-to-console logging.