SystemVerilog can read and write files on disk directly from a testbench — useful for logging results, loading a stimulus file, or reading back expected data to compare against.

10 min read | Intermediate Level

What You'll Learn

  • How to open, check, and close a file with $fopen/$fclose
  • The different file access modes, and why $fopen returns unusual-looking descriptor values
  • How to write and read lines with $fdisplay/$fwrite and $fgets
  • Why a while (!$feof(fd)) loop can produce one extra, empty iteration — and how $fscanf avoids it
  • What a multichannel descriptor is, and how it differs from a single-file descriptor

How to Open and Close a File?

A file is opened for reading or writing with $fopen(), which returns a 32-bit integer handle called a file descriptor. This handle is used for every subsequent read or write, until it's closed with $fclose() — no further reads or writes are allowed on it after that.


module tb;
  initial begin
  	// 1. Declare an integer variable to hold the file descriptor
  	int fd;

  	// 2. Open a file called "note.txt" in the current folder with a "read" permission
  	// If the file does not exist, then fd will be zero
    fd = $fopen ("./note.txt", "r");
    if (fd)  $display("File was opened successfully : %0d", fd);
    else     $display("File was NOT opened successfully : %0d", fd);
  
    // 2. Open a file called "note.txt" in the current folder with a "write" permission
    //    "fd" now points to the same file, but in write mode
    fd = $fopen ("./note.txt", "w");
    if (fd)  $display("File was opened successfully : %0d", fd);
    else     $display("File was NOT opened successfully : %0d", fd);
  
    // 3. Close the file descriptor
    $fclose(fd);
  end
endmodule

Output

Output
ncsim> run
Open failed on file "./note.txt". No such file or directory
File was NOT opened successfully : 0
File was opened successfully : -2147483645
ncsim: *W,RNQUIE: Simulation is complete.

The first $fopen fails because note.txt doesn't exist yet and was opened for reading only — fd comes back as 0. The second call, opening the same file for writing, succeeds and creates it, returning -2147483645.

That large, negative-looking number isn't a bug — a file descriptor's most significant bit is always forced to 1 (so an implementation can tell it apart from a multichannel descriptor), while the remaining bits hold an ordinary small count of which file this is. Interpreted as a plain signed integer, that forced top bit alone is what makes the value read as large and negative — 0x80000003 simply displays as -2147483645, with 3 being the actual small count underneath. Checking if (fd) still works correctly either way — only a failed open returns exactly 0.

It's important to close every open file before the end of simulation, or its contents may not be fully written to disk.

Quick Check

Q: If fd = $fopen("missing.txt", "r") and the file doesn't exist, what value does fd hold, and what does if (fd) evaluate to?

Show Answer

fd is 0, and if (fd) evaluates to false — 0 is the one specific value that always means the open failed, regardless of what a successful descriptor's bit pattern looks like.

File Access Modes

By default a file opens in write mode ("w"). Several other modes are available:

ArgumentDescription
"r"Open for reading
"w"Create for writing, overwrite if it exists
"a"Create if the file does not exist, else append; open for writing at end of file
"r+"Open for update (reading and writing)
"w+"Truncate or create for update
"a+"Append, open or create for update at EOF

module tb;
  initial begin
  	int fd_w, fd_r, fd_a, fd_wp, fd_rp, fd_ap;
  
    fd_w = $fopen ("./todo.txt", "w"); 	// Open a new file in write mode and store file descriptor in fd_w
    fd_r = $fopen ("./todo.txt", "r"); 	// Open in read mode
    fd_a = $fopen ("./todo.txt", "a"); 	// Open in append mode
  
  
    if (fd_w)     $display("File was opened successfully : %0d", fd_w);
    else      	  $display("File was NOT opened successfully : %0d", fd_w);
  
    if (fd_r)     $display("File was opened successfully : %0d", fd_r);
    else      	  $display("File was NOT opened successfully : %0d", fd_r);
  
    if (fd_a)     $display("File was opened successfully : %0d", fd_a);
    else      	  $display("File was NOT opened successfully : %0d", fd_a);
  
    // Close the file descriptor
    $fclose(fd_w);
    $fclose(fd_r);
    $fclose(fd_a);
  end
endmodule

Output

Output
ncsim> run
ncsim: *W,VFOPTW: File ./todo.txt being opened by Initial stmt (file: ./testbench.sv, line: 2 in worklib.tb [module]) has been opened earlier.
File was opened successfully : -2147483645
File was opened successfully : -2147483644
File was opened successfully : -2147483643
ncsim: *W,RNQUIE: Simulation is complete.

All three variables get a different descriptor value, each pointing to the same underlying file but with different access permissions. The simulator warns that the file was already open, since fd_w hadn't been closed before fd_r and fd_a opened it again — a heads-up, not a fatal error.

How to Read and Write to a File?

A file opened in write ("w") or append ("a") mode can be written to with $fdisplay() or $fwrite(), both taking the file descriptor as their first argument. A file opened in read ("r") or read-write ("r+") mode can be read one line at a time with $fgets() — calling it 10 times reads 10 lines.


module tb;
  int 	 fd; 			// Variable for file descriptor handle
  string line; 			// String value read from the file
  
  initial begin
  
    // 1. Lets first open a new file and write some contents into it
    fd = $fopen ("trial", "w");
  
    // Write each index in the for loop to the file using $fdisplay
    // File handle should be the first argument
    for (int i = 0; i < 5; i++) begin
      $fdisplay (fd, "Iteration = %0d", i);
    end
  
    // Close this file handle
    $fclose(fd);
  
  
    // 2. Let us now read back the data we wrote in the previous step
    fd = $fopen ("trial", "r");
  
    // Use $fgets to read a single line into variable "line"
    $fgets(line, fd);
    $display ("Line read : %s", line);
  
    // Get the next line and display
    $fgets(line, fd);
    $display ("Line read : %s", line);
  
    // Close this file handle
    $fclose(fd);
  end
endmodule

Output

Output
ncsim> run
Line read : Iteration = 0

Line read : Iteration = 1

ncsim: *W,RNQUIE: Simulation is complete.

Each output line is followed by a blank line — that's not a mistake in the code. $fgets() keeps the line's trailing newline character as part of the string it returns, and $display() adds its own newline on top of that, so every line ends up printed with what looks like a double line break.

How to Read Until End of File?

$feof() returns true once the end of a file has been reached, and can drive a loop that reads an entire file:


module tb;
  int 	 fd; 			// Variable for file descriptor handle
  string line; 			// String value read from the file
  
  initial begin
    // 1. Lets first open a new file and write some contents into it
    fd = $fopen ("trial", "w");
    for (int i = 0; i < 5; i++) begin
      $fdisplay (fd, "Iteration = %0d", i);
    end
    $fclose(fd);
  
  
    // 2. Let us now read back the data we wrote in the previous step
    fd = $fopen ("trial", "r");
  
    while (!$feof(fd)) begin
      $fgets(line, fd);
      $display ("Line: %s", line);
    end
  
    // Close this file handle
    $fclose(fd);
  end
endmodule

Output

Output
ncsim> run
Line: Iteration = 0

Line: Iteration = 1

Line: Iteration = 2

Line: Iteration = 3

Line: Iteration = 4

Line:
ncsim: *W,RNQUIE: Simulation is complete.

Notice the trailing empty Line: at the end, even though the file only has 5 lines. This is a well-known gotcha, not a bug in this specific code: $feof() only becomes true after a read has already been attempted and found nothing left. Right after the 5th real line is read, $feof(fd) is still false — the read that would actually discover end-of-file hasn't happened yet — so the loop runs one more time, $fgets() comes back empty, and that empty result still gets displayed.

Quick Check

Q: Why doesn't the loop simply stop after printing "Iteration = 4"?

Show Answer

Because the loop condition, !$feof(fd), is checked before each read, using whatever $feof reported as of the previous read. Right after reading "Iteration = 4" successfully, the file hasn't been read again yet, so $feof is still false at that point — the loop only discovers it's actually at the end on the following iteration, once it tries to read again and gets nothing.

How to Parse a Line for Values?

$fscanf() scans a line and extracts specific values, and its return value is the number of items it successfully matched:


module tb;
  int 	 fd; 			// Variable for file descriptor handle
  int 	 idx;
  string str;
  
  initial begin
    // 1. Lets first open a new file and write some contents into it
    fd = $fopen ("trial", "w");
    for (int i = 0; i < 5; i++)
      $fdisplay (fd, "Iteration = %0d", i);
    $fclose(fd);
  
  
    // 2. Let us now read back the data we wrote in the previous step
    fd = $fopen ("trial", "r");
  
    // fscanf returns the number of matches
    while ($fscanf (fd, "%s = %0d", str, idx) == 2) begin
      $display ("Line: %s = %0d", str, idx);
    end
  
    // Close this file handle
    $fclose(fd);
  end
endmodule

Output

Output
ncsim> run
Line: Iteration = 0
Line: Iteration = 1
Line: Iteration = 2
Line: Iteration = 3
Line: Iteration = 4
ncsim: *W,RNQUIE: Simulation is complete.

No trailing empty line here, unlike the $feof example above. That's because the loop condition checks $fscanf's own return value directly — once there's nothing left to scan, it returns fewer than 2 matches immediately, and the loop stops right there, with no lagging extra iteration.

What Are Multichannel File Descriptors?

A multichannel descriptor (mcd) is a 32-bit packed value where a single bit is set to indicate which file is open. The least significant bit always refers to standard output, and the most significant bit is always 0.

$fopen returns an mcd only when it is called with just a file name and no mode argument; the file is then opened for writing. Writing to two or more files at once is done by bitwise OR-ing their mcds together and writing to the combined result:


module tb;
  integer mcd1, mcd2, both;

  initial begin
    // No mode argument: each call returns a multichannel descriptor
    mcd1 = $fopen ("file1.txt");
    mcd2 = $fopen ("file2.txt");

    // Combine both descriptors into one - a single $fdisplay call
    // now writes the same line to both files at once
    both = mcd1 | mcd2;
    $fdisplay (both, "This goes to both files");

    $fclose(mcd1);
    $fclose(mcd2);
  end
endmodule

A single $fdisplay(both, ...) call writes to both file1.txt and file2.txt at once, since both's bit pattern has both files' bits set. OR-ing in 1 as well, as in $fdisplay(both | 1, ...), also prints the line to standard output.

This only works with mcds. A descriptor returned by $fopen with a mode such as "w" is an ordinary file descriptor: its top bit is set and the low bits hold the file's number, so OR-ing two of them gives a value that names neither file. Open the files without a mode whenever one call should write to several of them.

Where Is File I/O Used?

Writing test results and debug logs to a file is standard practice for anything run as part of an automated regression, where a script needs to parse the output afterward rather than a person watching the simulator's console in real time. Reading from a file is just as common for loading stimulus — a set of expected values, or a memory image — without hardcoding it directly into the testbench.

Common Beginner Mistakes

Mistake #1: Forgetting to close a file before simulation ends

An open file descriptor's contents may not be completely written to disk until it's closed. Why? Always pair every $fopen() with a matching $fclose(), especially before $finish or the natural end of the test, or the file on disk may be incomplete.

Mistake #2: Using while (!$feof(fd)) without accounting for the extra iteration

As shown above, this pattern reliably produces one trailing empty read past the real end of the file. Why? $feof reports the result of the previous read, not a live look-ahead at whether the next read will succeed — check $fgets's own return value directly instead if you want to avoid this exact lag.

Mistake #3: Assuming $fopen returns small sequential integers

A successfully opened file descriptor can come back looking like a large negative number, such as -2147483645. Why? The count underneath genuinely is small and does increase sequentially, but its most significant bit is always forced to 1 on top of that — exactly what makes the signed value read as large and negative instead of a small positive number like 1, 2, 3. Always check for success with if (fd) rather than expecting any particular numeric range.

Practice Exercise

Challenge: Fix the Extra Empty Line

Rewrite the $feof-based read loop from earlier so it prints exactly 5 lines, with no trailing empty one — without switching to $fscanf.

Hint

$fgets itself returns a value: the number of characters read, which is 0 once there's nothing left. Check that return value directly instead of checking $feof beforehand.

Solution

fd = $fopen ("trial", "r");

while ($fgets(line, fd)) begin
  $display ("Line: %s", line);
end

$fclose(fd);

$fgets returns 0 the moment there's nothing left to read, so checking its own return value directly reflects the real outcome of that specific call — unlike $feof, there's no lag between what the loop condition reports and what actually just happened.

Summary

In this tutorial, you learned:

  • $fopen() returns a file descriptor (0 on failure), and $fclose() must be called before the file's contents are guaranteed to be fully written
  • File access modes control read, write, append, and update behavior, each represented by a different string like "r", "w", or "a"
  • $fdisplay()/$fwrite() write formatted data; $fgets() reads one line at a time, keeping its trailing newline
  • while (!$feof(fd)) reliably produces one extra, empty iteration past the real end of the file; checking $fgets's or $fscanf's own return value avoids that lag
  • A multichannel descriptor, returned by $fopen called without a mode, is a bit-flag value that can be OR'd together with another to write the same data to multiple files in a single call; ordinary file descriptors cannot be combined this way

Continuing the Testbench Components Chapter

Continue with SystemVerilog Testbench Example 1.

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5