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. 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 fd1, fd2, both;

  initial begin
    fd1 = $fopen ("file1.txt", "w");
    fd2 = $fopen ("file2.txt", "w");

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

    $fclose(fd1);
    $fclose(fd2);
  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.

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 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

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
Cron Job Starts