Verify a RAM with SystemVerilog

Build a self-checking SystemVerilog testbench to verify a synchronous RAM.

Introduction

30 Second Summary

A memory can return the right value in a quick spot check while hiding a fault at the edge of its address range. Reliable hardware needs a repeatable way to test every case.

In this project, you will build a 16-address by 8-bit synchronous RAM. A SystemVerilog testbench will expose the hidden boundary defect before proving the corrected design works across all 16 addresses.

What You'll Build

Your finished self-checking regression will expose the blocked final address before proving the corrected RAM passes all 16 locations with zero errors.

By the end of this project, you'll have:

  • A directed testbench that proves writes and clocked reads work at addresses 2 and 5.
  • An independent reference model whose complete address sweep reports the hidden mismatch at address 15.
  • A zero-error regression you can rerun with concurrent assertions guarding the RAM interface on every clock.
  • Secret Mission: Prove that two consecutive writes to address 3 preserve the newest value.

Are there any prerequisites?

You need basic Verilog or SystemVerilog syntax plus a Windows computer with Questa Sim-64 2021.2 already installed. Step 1 verifies Notepad, Windows PowerShell, and the simulator commands from your project folder.

Before We Start

This step is your commitment to building a synchronous single-port RAM through a progressively self-checking SystemVerilog testbench. The proof drives every RAM input (clock/reset/address/write enable/write data) while an independent model predicts contents for comparison against clocked read data.

Verify Questa on Windows

Your RAM needs a reliable local simulation path from its project folder.

This smoke test proves that your existing Questa Sim-64 2021.2 installation can compile a SystemVerilog file. It also proves that the simulator can run that file to completion.

In this step, get ready to:
  • Create the Windows project folder in a fixed location.
  • Save a SystemVerilog smoke test with the correct file extension.
  • Compile and simulate the smoke test with Questa.
Create the Windows project folder

Windows PowerShell gives each simulator command the same project location. A fixed location prevents Questa from searching for files in another folder.

  • Press the Windows key to open the Windows search bar.
  • Type Windows PowerShell and press Enter to open it.
  • Create C:\nextwork\ram-verification and switch into it by running these commands:
New-Item -Path C:\nextwork\ram-verification -ItemType Directory -Force
Set-Location -Path C:\nextwork\ram-verification -PassThru

What do these commands do?

  • The New-Item command creates the project folder.
  • The -Force flag keeps the command safe to repeat when the folder already exists.
  • The Set-Location command makes the project folder your current PowerShell location.
  • The -PassThru flag prints the new location for confirmation.

You should see C:\nextwork\ram-verification as the current path. Every file in this project now has one predictable home.

Unable to create the folder?

Check that the path begins with C:\nextwork. Confirm that your Windows account can create folders on the C: drive.

If access is still blocked, help me create the project folder in Windows PowerShell.

Create the SystemVerilog smoke test

Notepad saves plain text that Questa can compile as SystemVerilog. The filename must end in .sv so the simulator receives the intended source file.

  • Press the Windows key to open the Windows search bar.
  • Type Notepad and press Enter to open it.
  • Paste this smoke test into the blank document:
initial begin
  $display("QUESTA READY");
  $finish;
end

What does this code do?

  • The initial block runs once when the simulation begins.
  • The $display statement prints QUESTA READY in the Transcript.
  • The $finish statement ends the smoke test after the message prints.
  • Select File in Notepad.
  • Select Save As.
  • Enter "C:\nextwork\ram-verification\hello.sv" in the File name box.
  • Press Enter to save the file.
  • Switch back to Windows PowerShell from earlier.
  • Confirm that the exact hello.sv filename exists by running:
Get-ChildItem hello.sv

What does this command check?

The Get-ChildItem command searches the current project folder for hello.sv. A matching result confirms that Notepad preserved the .sv extension.

You should see hello.sv listed in the PowerShell output. The filename should not end in .txt.

File not listed?

  • Return to Notepad.
  • Select File.
  • Select Save As.
  • Enter the quoted path "C:\nextwork\ram-verification\hello.sv" in the File name box.
  • If the exact file still does not appear, help me save hello.sv without a .txt extension.

✔️ Awesome, I've got everything!

Your hello.sv file is saved in C:\nextwork\ram-verification with the correct extension.

ⓧ I'd like to double check the full code

  • Compare hello.sv with this full file:
initial begin
  $display("QUESTA READY");
  $finish;
end

What should match?

Your file should contain one initial block. Its output text must remain exactly QUESTA READY.

Run the Questa smoke test

Questa uses the folder selected in PowerShell as the starting location for its files. Starting the simulator there lets the Transcript find hello.sv by filename.

  • Start Questa from the current project folder by running this command in Windows PowerShell:
vsim

What does this command do?

The vsim command starts the Questa simulator from C:\nextwork\ram-verification. The simulator inherits that folder as its starting location.

You should see the Questa interface open. Its Transcript pane accepts the commands that build the smoke test.

Questa does not start?

If PowerShell cannot locate vsim, use the Questa command-shell shortcut supplied with your existing installation. Repeat the project-folder command from the first substep before starting the simulator again.

For environment-specific help, help me make Questa Sim-64 2021.2 reachable from my project folder.

Before you run the flow, what message do you expect the final command to reveal?

  • Click inside the Transcript pane.
  • Build and run the smoke test by entering these commands one line at a time:
vlib work
vlog hello.sv
vsim hello
run -all

What does this flow do?

  • The vlib command creates the work library for compiled design units.
  • The vlog command compiles hello.sv.
  • The vsim command loads the hello design for simulation.
  • The run -all command runs the simulation until $finish stops it.

You should see QUESTA READY in the Transcript. The compile stage should show no errors.

You have cleared the environment risk. Your Windows project folder can now reach a working SystemVerilog simulation flow.

Smoke test not reaching the ready message?

  • Confirm that the Transcript is working from C:\nextwork\ram-verification.
  • Confirm that PowerShell listed hello.sv without an extra .txt extension.
  • Compare the file with the full-code tab before compiling again.
  • If the Transcript still reports a problem, help me debug my Questa smoke test.

Your Questa workflow is ready. Next, you will create the synchronous RAM and drive its first two write-read checks.

Build a Directed RAM Test

Your smoke test proved that Questa Sim-64 2021.2 can compile SystemVerilog from C:\nextwork\ram-verification. It also proved that your simulator can run a design to completion.

A directed testbench checks a small set of chosen transactions. This gives you a complete verification loop while leaving the wider address space for stronger testing.

In this step, get ready to:
  • Create a parameterized synchronous RAM with clocked writes and registered reads.
  • Build a testbench that writes data to two chosen addresses.
  • Run the directed checks until the Transcript prints SMOKE TEST PASS.
Create the synchronous RAM

A synchronous single-port RAM performs one clocked operation through a shared address port. Reset clears its storage so every directed check starts from a predictable state.

  • Switch back to Notepad from the setup check.
  • Create a blank document in Notepad.
  • Save the document as C:\nextwork\ram-verification\single_port_ram.sv.
  • Add the RAM module by pasting this code into single_port_ram.sv:
module single_port_ram #(
  parameter int DATA_WIDTH = 8,
  parameter int ADDR_WIDTH = 4,
  parameter int DEPTH = 16
) (
  input  logic                  clk,
  input  logic                  rst_n,
  input  logic                  wr_en,
  input  logic [ADDR_WIDTH-1:0] addr,
  input  logic [DATA_WIDTH-1:0] wr_data,
  output logic [DATA_WIDTH-1:0] rd_data
);

  logic [DATA_WIDTH-1:0] mem [0:DEPTH-1];
  integer i;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      rd_data <= '0;
      for (i = 0; i < DEPTH; i = i + 1)
        mem[i] <= '0;
    end else if (wr_en && (addr < DEPTH - 1)) begin
      mem[addr] <= wr_data;
    end else begin
      rd_data <= mem[addr];
    end
  end

endmodule

What does this RAM do?

  • The parameters define an 8-bit word width with a 4-bit address port and 16 storage locations.
  • The mem array stores one 8-bit word at each address.
  • The reset branch clears rd_data plus every location in mem.
  • The write branch stores wr_data at addr when its condition is true.
  • The read branch registers the selected memory value in rd_data on a rising clock edge.
  • Save single_port_ram.sv.
  • Return to the Questa Transcript from earlier.
  • Compile the RAM module by entering this command:
vlog single_port_ram.sv

What does this command do?

The vlog command compiles single_port_ram.sv into the existing work library. A successful compile proves that Questa accepts the module syntax.

You should see the compile finish without an error. The Transcript is now ready for a testbench that drives the RAM.

RAM module not compiling?

  • Confirm that Notepad saved the file as single_port_ram.sv without adding a .txt extension.
  • Check that the final line contains endmodule.
  • Compare each opening parenthesis with its closing parenthesis.
  • Ask for help with the first compiler message: Help me understand the first Questa compile error in single_port_ram.sv.
Build the directed testbench

The testbench owns the RAM inputs during simulation. It generates a clock first so later transactions can line up with the RAM's rising-edge behavior.

  • Switch back to Notepad from earlier.
  • Create another blank document.
  • Save the document as C:\nextwork\ram-verification\ram_tb.sv.

You should now see ram_tb.sv as the document name in Notepad.

  • Build a runnable clock skeleton by pasting this code into ram_tb.sv:
`timescale 1ns/1ps

module ram_tb;

  logic clk;

  always #5 clk = ~clk;

  initial begin
    clk = 1'b0;
    repeat (2) @(posedge clk);
    $finish;
  end

endmodule

How does the clock skeleton work?

  • The timescale gives the simulation a 1 ns time unit with 1 ps precision.
  • The always process inverts clk every 5 ns.
  • The initial block starts the clock at zero.
  • The testbench finishes after two rising clock edges.
  • Save ram_tb.sv.
  • Return to the Questa Transcript.
  • Compile both modules and run the clock skeleton by entering these commands:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does this simulation sequence do?

  • The first command recompiles the RAM plus its testbench.
  • The second command loads ram_tb as the simulation top level.
  • The final command runs until the testbench calls $finish.

The simulation should return control to the Transcript without a compile error. You have now proved that the generated clock reaches both rising edges.

Clock skeleton not finishing?

  • Check that clk starts at 1'b0.
  • Confirm that always #5 clk = ~clk; appears outside the initial block.
  • Ask for help with a stalled simulation: Help me debug why ram_tb does not reach $finish.

The clock runs independently now. The next edit connects the RAM signals so the testbench can control the device under test.

  • In ram_tb.sv find this signal declaration:
  logic clk;

What are you replacing?

This is the testbench's current clock declaration. The replacement keeps that declaration while adding the complete RAM interface.

  • Replace that line with this declaration plus DUT block:
  localparam int DATA_WIDTH = 8;
  localparam int ADDR_WIDTH = 4;
  localparam int DEPTH = 16;

  logic clk;
  logic rst_n;
  logic wr_en;
  logic [ADDR_WIDTH-1:0] addr;
  logic [DATA_WIDTH-1:0] wr_data;
  logic [DATA_WIDTH-1:0] rd_data;
  integer errors;

  single_port_ram #(
    .DATA_WIDTH(DATA_WIDTH),
    .ADDR_WIDTH(ADDR_WIDTH),
    .DEPTH(DEPTH)
  ) dut (
    .clk(clk), .rst_n(rst_n), .wr_en(wr_en),
    .addr(addr), .wr_data(wr_data), .rd_data(rd_data)
  );

What does this testbench wiring do?

  • The local parameters match the RAM's 8-bit data width with its 16-address depth.
  • The logic signals carry clock control plus write data and read data.
  • The errors counter records failed comparisons.
  • The instance named dut connects every testbench signal to the matching RAM port.
  • In ram_tb.sv locate the current initial block:
  initial begin
    clk = 1'b0;
    repeat (2) @(posedge clk);
    $finish;
  end

What does the current block cover?

The current block only initializes the clock. It waits for two rising edges before ending the simulation.

  • Replace the current initial block with this reset sequence plus the first write and read check:
  initial begin
    clk = 1'b0;
    rst_n = 1'b0;
    wr_en = 1'b0;
    addr = '0;
    wr_data = '0;
    errors = 0;

    repeat (2) @(posedge clk);
    @(negedge clk);
    rst_n = 1'b1;

    @(negedge clk);
    wr_en = 1'b1;
    addr = 4'd2;
    wr_data = 8'h2A;
    @(posedge clk);

    @(negedge clk);
    wr_en = 1'b0;
    addr = 4'd2;
    @(posedge clk);
    #1;
    if (rd_data !== 8'h2A) errors++;

    $finish;
  end

How does the first transaction work?

  • The opening assignments give every testbench signal a known value.
  • The reset remains active for two rising edges so the RAM clears its storage.
  • The write controls change on a falling edge so they are stable before the next rising edge.
  • The read check waits until 1 ns after the rising edge so the registered output can settle.
  • A value other than 8'h2A increments errors.
  • Save ram_tb.sv.
  • Return to the Questa Transcript.
  • Recompile both modules and run the first directed transaction by entering these commands:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does this intermediate run prove?

This run executes the reset plus the address 2 write and read. Reaching $finish without a compile error proves that the connected testbench executes.

The Transcript should return after the address 2 comparison. The completed smoke test adds a visible pass result next.

Seeing a signal or port error?

  • Confirm that the instance name is dut.
  • Check that each testbench signal uses the same spelling as the corresponding RAM port.
  • Confirm that both filenames appear in the vlog command.
  • Ask for help with the connection error: Help me debug the DUT wiring in ram_tb.sv.
Add the second check and run the smoke test

A second transaction checks that the same interface works at another ordinary address. The testbench prints a pass message only when both clocked reads match their written values.

  • In ram_tb.sv find this line near the end of the initial block:
    $finish;

Where does the final check go?

The current line ends the simulation immediately after the address 2 check. Replacing it keeps the finish call after the new address 5 transaction.

  • Replace that line with the second write plus read check and final pass condition:
    @(negedge clk);
    wr_en = 1'b1;
    addr = 4'd5;
    wr_data = 8'h5A;
    @(posedge clk);

    @(negedge clk);
    wr_en = 1'b0;
    addr = 4'd5;
    @(posedge clk);
    #1;
    if (rd_data !== 8'h5A) errors++;

    if (errors == 0) $display("SMOKE TEST PASS");
    $finish;

How does the completed smoke test decide?

  • The second write stores 8'h5A at address 5.
  • The following read compares rd_data with the expected value.
  • The pass message appears only while errors remains zero.
  • The final $finish ends the completed directed simulation.
  • Save ram_tb.sv.

Before you run the completed test, do you expect both clocked read checks to preserve a zero error count?

  • Return to the Questa Transcript.
  • Run the completed directed regression by entering these commands:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does the final run verify?

Questa recompiles the current RAM plus testbench before loading a fresh ram_tb simulation. The run executes reset plus both directed transactions.

You should see SMOKE TEST PASS in the Transcript. Addresses 2 and 5 pass because this directed test does not exercise address 15.

Pass message missing?

  • Confirm that the first transaction uses address 4'd2 with value 8'h2A.
  • Confirm that the second transaction uses address 4'd5 with value 8'h5A.
  • Check that each read changes wr_en to 1'b0 before the rising edge.
  • Ask for help tracing the directed check: Help me find why SMOKE TEST PASS is missing from the Questa Transcript.

✔️ Awesome, I've got everything!

Your directed RAM test is working. Double check that both single_port_ram.sv and ram_tb.sv are saved in C:\nextwork\ram-verification.

ⓧ I'd like to double check the full code

Compare each file with the complete project state below. Keep hello.sv unchanged from the setup smoke test.

initial begin
  $display("QUESTA READY");
  $finish;
end

What should hello.sv contain?

This file remains the original Questa smoke test. No RAM code belongs in hello.sv.

module single_port_ram #(
  parameter int DATA_WIDTH = 8,
  parameter int ADDR_WIDTH = 4,
  parameter int DEPTH = 16
) (
  input  logic                  clk,
  input  logic                  rst_n,
  input  logic                  wr_en,
  input  logic [ADDR_WIDTH-1:0] addr,
  input  logic [DATA_WIDTH-1:0] wr_data,
  output logic [DATA_WIDTH-1:0] rd_data
);

  logic [DATA_WIDTH-1:0] mem [0:DEPTH-1];
  integer i;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      rd_data <= '0;
      for (i = 0; i < DEPTH; i = i + 1)
        mem[i] <= '0;
    end else if (wr_en && (addr < DEPTH - 1)) begin
      mem[addr] <= wr_data;
    end else begin
      rd_data <= mem[addr];
    end
  end

endmodule

What should the DUT contain?

The DUT contains the parameterized memory array plus one rising-edge process. Its reset, write, and registered-read branches should match this file exactly.

`timescale 1ns/1ps

module ram_tb;

  localparam int DATA_WIDTH = 8;
  localparam int ADDR_WIDTH = 4;
  localparam int DEPTH = 16;

  logic clk;
  logic rst_n;
  logic wr_en;
  logic [ADDR_WIDTH-1:0] addr;
  logic [DATA_WIDTH-1:0] wr_data;
  logic [DATA_WIDTH-1:0] rd_data;
  integer errors;

  single_port_ram #(
    .DATA_WIDTH(DATA_WIDTH),
    .ADDR_WIDTH(ADDR_WIDTH),
    .DEPTH(DEPTH)
  ) dut (
    .clk(clk), .rst_n(rst_n), .wr_en(wr_en),
    .addr(addr), .wr_data(wr_data), .rd_data(rd_data)
  );

  always #5 clk = ~clk;

  initial begin
    clk = 1'b0;
    rst_n = 1'b0;
    wr_en = 1'b0;
    addr = '0;
    wr_data = '0;
    errors = 0;

    repeat (2) @(posedge clk);
    @(negedge clk);
    rst_n = 1'b1;

    @(negedge clk);
    wr_en = 1'b1;
    addr = 4'd2;
    wr_data = 8'h2A;
    @(posedge clk);

    @(negedge clk);
    wr_en = 1'b0;
    addr = 4'd2;
    @(posedge clk);
    #1;
    if (rd_data !== 8'h2A) errors++;

    @(negedge clk);
    wr_en = 1'b1;
    addr = 4'd5;
    wr_data = 8'h5A;
    @(posedge clk);

    @(negedge clk);
    wr_en = 1'b0;
    addr = 4'd5;
    @(posedge clk);
    #1;
    if (rd_data !== 8'h5A) errors++;

    if (errors == 0) $display("SMOKE TEST PASS");
    $finish;
  end

endmodule

What should the testbench contain?

The testbench contains the DUT instance plus its clock, reset, and two directed transactions. The final comparison gate prints SMOKE TEST PASS only when both reads match.

That first verification loop is complete: your testbench drives the RAM and checks two registered reads automatically. Next, you will expand the checker across the full address range.

Create a Full Address Checker

Your two-address smoke test proves the RAM can store ordinary values in Questa. The untested addresses can still hide a defect.

A self-checking SystemVerilog testbench needs an independent reference model. A complete address sweep can compare every result against that model.

In this step, get ready to:
  • Add an independent memory model that records intended writes.
  • Create reusable tasks for reset, write, and checked read transactions.
  • Sweep every valid address to test the RAM boundary.
Add an independent reference model

The reference model stores what each address should contain. It follows the intended write transaction without copying the DUT's address condition.

  • Return to ram_tb.sv in Notepad.
  • Find integer errors; below the signal declarations.
  • Replace that line with this declaration group:
  logic [DATA_WIDTH-1:0] model_mem [0:DEPTH-1];
  integer errors;
  integer index;

What do these declarations track?

  • The model_mem array stores the expected value for every RAM address.
  • The errors counter records failed comparisons.
  • The index variable drives the full address sweep.
  • Find always #5 clk = ~clk; below the DUT instance.
  • Paste this reset task immediately below the clock generator:
  task automatic reset_dut;
    integer reset_index;
    begin
      rst_n = 1'b0;
      wr_en = 1'b0;
      addr = '0;
      wr_data = '0;

      for (reset_index = 0; reset_index < DEPTH; reset_index = reset_index + 1)
        model_mem[reset_index] = '0;

      repeat (2) @(posedge clk);
      @(negedge clk);
      rst_n = 1'b1;
      @(posedge clk);
      #1;

      if (rd_data !== '0) begin
        $error("RESET MISMATCH: expected=00 actual=%02h", rd_data);
        errors++;
      end
    end
  endtask

How does the reset task work?

  • The task clears every entry in model_mem so the expected state matches the RAM specification.
  • The task releases rst_n on a falling edge so the inputs remain stable before the next rising edge.
  • The #1 delay lets the clocked nonblocking assignment settle before the reset result is checked.
  • Save ram_tb.sv.
  • Recompile the files and run the current smoke test by entering these commands in the Questa Transcript:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does this check prove?

  • The vlog command confirms the new declarations and task use valid SystemVerilog syntax.
  • The vsim command loads the existing ram_tb simulation.
  • The run -all command runs the current directed test to completion.

You should see SMOKE TEST PASS again. This confirms the reference-model declarations and reset task compile cleanly.

Does the testbench stop compiling?

  • Check that model_mem appears inside ram_tb with the other declarations.
  • Check that reset_dut ends with endtask before the existing initial block.
  • Help me debug the new reference model and reset task.
Create reusable write and read tasks

A task packages one timed transaction behind a single call. Each task drives controls on a falling edge so the DUT receives stable inputs at the next rising edge.

  • Find the closing endtask for reset_dut.
  • Paste this write task directly below it:
  task automatic write_word(
    input logic [ADDR_WIDTH-1:0] write_addr,
    input logic [DATA_WIDTH-1:0] value
  );
    begin
      @(negedge clk);
      wr_en = 1'b1;
      addr = write_addr;
      wr_data = value;

      @(posedge clk);
      #1;
      model_mem[write_addr] = value;
    end
  endtask

What does the write task model?

  • The task presents write_addr and value before the DUT's active clock edge.
  • The DUT attempts its write at the next rising edge.
  • The independent model records the intended value without checking the DUT's faulty address guard.
  • Find the closing endtask for write_word.
  • Paste this checked-read task directly below it:
  task automatic read_check(
    input logic [ADDR_WIDTH-1:0] read_addr
  );
    begin
      @(negedge clk);
      wr_en = 1'b0;
      addr = read_addr;
      wr_data = '0;

      @(posedge clk);
      #1;
      if (rd_data !== model_mem[read_addr]) begin
        $error(
          "DATA MISMATCH: addr=%0d expected=%02h actual=%02h",
          read_addr,
          model_mem[read_addr],
          rd_data
        );
        errors++;
      end
    end
  endtask

How does the checked read work?

  • The task lowers wr_en to request a clocked read from read_addr.
  • The comparison happens after the next rising edge plus #1.
  • A mismatch prints the address plus both values. It also increments errors.
  • Save ram_tb.sv.
  • Recompile the task definitions and run the current directed test by entering:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What is being checked now?

Questa compiles both task definitions before loading the unchanged directed test. This catches missing task boundaries before the initial block starts using them.

You should see a clean compile followed by SMOKE TEST PASS. The same result confirms both reusable tasks are accepted by the simulator.

Seeing a task syntax problem?

  • Confirm that each task has its own begin and end pair.
  • Confirm that each task finishes with endtask.
  • Help me fix the write or read task.
Sweep every RAM address

The reusable tasks can now replace the repeated directed stimulus. The first refactor keeps the same two-address test so you can prove the transaction timing still works.

  • Find repeat (2) @(posedge clk); inside the initial block.
  • Replace everything from that line through the existing SMOKE TEST PASS statement with this task-based sequence:
    reset_dut();

    write_word(2, 8'h2A);
    read_check(2);
    write_word(5, 8'h5A);
    read_check(5);

    if (errors == 0)
      $display("SMOKE TEST PASS");

Why keep the directed checks?

Addresses 2 and 5 provide a fast regression for the new tasks. A passing result proves the refactor preserved the earlier behavior before the test expands.

  • Save ram_tb.sv.

Before you run the refactor, do you expect the same two addresses to pass? The next simulation tests that prediction.

  • Recompile and run the task-based directed test by entering:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does this run prove?

This run executes reset_dut, write_word, and read_check for the first time. The output proves the reusable timing sequence works for the original addresses.

You should still see SMOKE TEST PASS. Your testbench now performs the same directed test with reusable transactions.

Did the refactored smoke test fail?

  • Check that reset_dut() appears before the first write.
  • Check that each write_word call is followed by a matching read_check call.
  • Help me debug the task-based smoke test.

A complete checker needs to cover the whole address range. Two loops can write a unique pattern to every location before reading every location back.

  • Find the SMOKE TEST PASS display statement in the initial block.
  • Paste this full address sweep below that statement and above $finish;:
    for (index = 0; index < DEPTH; index = index + 1)
      write_word(index, index + 1);

    for (index = 0; index < DEPTH; index = index + 1)
      read_check(index);

    if (errors == 0)
      $display("TEST PASS: RAM address sweep completed with 0 errors");
    else
      $display("TEST FAIL: RAM address sweep completed with %0d errors", errors);

How does the sweep find boundary defects?

  • The first loop writes index + 1 to every address from 0 through 15.
  • The second loop compares every clocked read with the corresponding model_mem entry.
  • The final result depends on the accumulated errors count.
  • Save ram_tb.sv.

Before you run the sweep, do you expect every one of the 16 addresses to match? The next simulation checks the entire range.

  • Recompile and run the full address checker by entering:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does the final run execute?

The simulation runs the directed smoke test first. It then writes and checks all 16 addresses with the independent model.

You should see DATA MISMATCH: addr=15 expected=10 actual=00 followed by TEST FAIL. The checker has caught a defect that the two-address test missed.

That failure is a win for your verification environment. Your testbench found the blocked final address automatically.

Does the address sweep pass unexpectedly?

  • Check that both loops use index < DEPTH so address 15 is included.
  • Check that model_mem[write_addr] = value; has no copy of the DUT's address condition.
  • Help me find why the boundary mismatch is missing.
  • Return to single_port_ram.sv in Notepad.
  • Locate else if (wr_en && (addr < DEPTH - 1)) begin to identify why the final write was blocked.

The condition accepts addresses below 15. Address 15 therefore retains its reset value of 00 while the independent model expects 10.

✔️ Awesome, I've got everything!

Great. Confirm that ram_tb.sv is saved and the Questa Transcript shows the address-15 mismatch.

ⓧ I'd like to double check the full code

Compare your full ram_tb.sv with this full-address-checker version.

`timescale 1ns/1ps

module ram_tb;

  localparam int DATA_WIDTH = 8;
  localparam int ADDR_WIDTH = 4;
  localparam int DEPTH = 16;

  logic clk;
  logic rst_n;
  logic wr_en;
  logic [ADDR_WIDTH-1:0] addr;
  logic [DATA_WIDTH-1:0] wr_data;
  logic [DATA_WIDTH-1:0] rd_data;

  logic [DATA_WIDTH-1:0] model_mem [0:DEPTH-1];
  integer errors;
  integer index;

  single_port_ram #(
    .DATA_WIDTH(DATA_WIDTH),
    .ADDR_WIDTH(ADDR_WIDTH),
    .DEPTH(DEPTH)
  ) dut (
    .clk(clk),
    .rst_n(rst_n),
    .wr_en(wr_en),
    .addr(addr),
    .wr_data(wr_data),
    .rd_data(rd_data)
  );

  always #5 clk = ~clk;

  task automatic reset_dut;
    integer reset_index;
    begin
      rst_n = 1'b0;
      wr_en = 1'b0;
      addr = '0;
      wr_data = '0;

      for (reset_index = 0; reset_index < DEPTH; reset_index = reset_index + 1)
        model_mem[reset_index] = '0;

      repeat (2) @(posedge clk);
      @(negedge clk);
      rst_n = 1'b1;
      @(posedge clk);
      #1;

      if (rd_data !== '0) begin
        $error("RESET MISMATCH: expected=00 actual=%02h", rd_data);
        errors++;
      end
    end
  endtask

  task automatic write_word(
    input logic [ADDR_WIDTH-1:0] write_addr,
    input logic [DATA_WIDTH-1:0] value
  );
    begin
      @(negedge clk);
      wr_en = 1'b1;
      addr = write_addr;
      wr_data = value;

      @(posedge clk);
      #1;
      model_mem[write_addr] = value;
    end
  endtask

  task automatic read_check(
    input logic [ADDR_WIDTH-1:0] read_addr
  );
    begin
      @(negedge clk);
      wr_en = 1'b0;
      addr = read_addr;
      wr_data = '0;

      @(posedge clk);
      #1;
      if (rd_data !== model_mem[read_addr]) begin
        $error(
          "DATA MISMATCH: addr=%0d expected=%02h actual=%02h",
          read_addr,
          model_mem[read_addr],
          rd_data
        );
        errors++;
      end
    end
  endtask

  initial begin
    clk = 1'b0;
    rst_n = 1'b0;
    wr_en = 1'b0;
    addr = '0;
    wr_data = '0;
    errors = 0;

    reset_dut();

    write_word(2, 8'h2A);
    read_check(2);
    write_word(5, 8'h5A);
    read_check(5);

    if (errors == 0)
      $display("SMOKE TEST PASS");

    for (index = 0; index < DEPTH; index = index + 1)
      write_word(index, index + 1);

    for (index = 0; index < DEPTH; index = index + 1)
      read_check(index);

    if (errors == 0)
      $display("TEST PASS: RAM address sweep completed with 0 errors");
    else
      $display("TEST FAIL: RAM address sweep completed with %0d errors", errors);

    $finish;
  end

endmodule

Your checker now turns a hidden boundary defect into a repeatable failure. Next, you'll correct the RAM while keeping this test unchanged as proof.

Fix and Guard the RAM

Your SystemVerilog testbench has turned the hidden address 15 defect into a repeatable failure. The unchanged checker now gives you a precise target for the RTL correction.

The fix removes the faulty boundary restriction without weakening the test. Named concurrent assertions then guard the interface during future runs in Questa.

In this step, get ready to:
  • Correct the RAM write condition so every four-bit address can be written.
  • Add three named concurrent assertions to monitor the RAM interface.
  • Run the unchanged regression across every existing check.
Correct the RAM write guard

The address sweep already traced the failure to one condition. That condition excludes the final valid address because it accepts only values below DEPTH - 1.

  • Switch back to single_port_ram.sv in Notepad.
  • Find this line in the always_ff block:
    end else if (wr_en && (addr < DEPTH - 1)) begin

Why does this block address 15?

With DEPTH set to 16, the comparison accepts addresses below 15. A write to address 15 falls into the read branch instead.

  • Replace the faulty line with this corrected condition:
    end else if (wr_en) begin

What does the correction change?

The four-bit addr input already limits requests to addresses 0 through 15. Checking only wr_en allows every representable address to reach mem[addr].

  • Save single_port_ram.sv.
  • Return to the Questa Transcript from earlier.
  • Predict whether the unchanged address sweep now passes at address 15.
  • Test the corrected write guard by entering these commands in order:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does this run prove?

The same reference model still expects the pattern address + 1 at every location. A passing run proves the RTL now agrees with that independent expectation at address 15.

You should now see TEST PASS: RAM address sweep completed with 0 errors. The earlier address 15 mismatch is gone.

Still seeing the address 15 mismatch?

  • Confirm that single_port_ram.sv is saved with else if (wr_en).
  • Confirm that the compile command names single_port_ram.sv before ram_tb.sv.
  • Ask for help with the remaining mismatch through this debugging prompt.

✔️ Awesome, I've got everything!

Great. Double check that single_port_ram.sv is saved with the corrected write condition.

ⓧ I'd like to double check the full code

Compare your corrected RAM with this complete file.

module single_port_ram #(
  parameter int DATA_WIDTH = 8,
  parameter int ADDR_WIDTH = 4,
  parameter int DEPTH = 16
) (
  input  logic                  clk,
  input  logic                  rst_n,
  input  logic                  wr_en,
  input  logic [ADDR_WIDTH-1:0] addr,
  input  logic [DATA_WIDTH-1:0] wr_data,
  output logic [DATA_WIDTH-1:0] rd_data
);

  logic [DATA_WIDTH-1:0] mem [0:DEPTH-1];
  integer i;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      rd_data <= '0;
      for (i = 0; i < DEPTH; i = i + 1)
        mem[i] <= '0;
    end else if (wr_en) begin
      mem[addr] <= wr_data;
    end else begin
      rd_data <= mem[addr];
    end
  end

endmodule
Add concurrent assertion guards

A passing sweep checks the transactions generated by the testbench. Concurrent assertions add clock-based rules that monitor the interface throughout the simulation.

  • Switch back to ram_tb.sv in Notepad.
  • Find the clock generator line always #5 clk = ~clk;.
  • Add the first named assertion directly below the clock generator:
  ap_controls_known: assert property (
    @(posedge clk) disable iff (!rst_n)
    !$isunknown({wr_en, addr})
  ) else begin
    $error("ASSERTION FAILED: wr_en or addr is unknown");
    errors++;
  end

What does this assertion guard?

  • The name ap_controls_known identifies the rule in simulation reports.
  • The expression $isunknown({wr_en, addr}) detects unknown control or address values at a rising clock edge.
  • The clause disable iff (!rst_n) suspends the rule during active-low reset.
  • Save ram_tb.sv.
  • Return to the Questa Transcript.
  • Compile the new control assertion by entering:
vlog single_port_ram.sv ram_tb.sv

What did the compile check?

A clean compile confirms that the named property has valid SystemVerilog syntax. It also confirms that the assertion can access the existing errors counter.

You should see the compilation complete without an error.

Seeing an assertion syntax error?

  • Check that ap_controls_known is followed by a colon.
  • Check that the property closes with ) else begin.
  • Use this assertion debugging prompt to inspect the structure.

The control assertion catches invalid inputs. The next property checks the registered read result after a read cycle.

  • Switch back to ram_tb.sv in Notepad.
  • Add the read-result assertion directly below ap_controls_known.
  ap_read_result_known: assert property (
    @(posedge clk) disable iff (!rst_n)
    !wr_en |=> !$isunknown(rd_data)
  ) else begin
    $error("ASSERTION FAILED: read result is unknown");
    errors++;
  end

How does the read check work?

The sequence begins when wr_en is low at a rising edge. The |=> operator checks that rd_data is known at the following clock sample.

Reset disables the property through the same disable iff (!rst_n) clause. This keeps reset activity outside the rule.

  • Save ram_tb.sv.
  • Return to the Questa Transcript.
  • Compile the read-result assertion by entering:
vlog single_port_ram.sv ram_tb.sv

What did this compile confirm?

A clean compile confirms that Questa accepts the one-cycle implication. It also confirms that rd_data is visible to the property.

You should see the compilation complete without an error.

Does the read assertion fail to compile?

  • Confirm that the implication operator is |=>.
  • Confirm that $isunknown(rd_data) includes both parentheses.
  • Use this read-assertion debugging prompt to find the mismatch.

The final property protects the RAM's registered read output during write activity. A write cycle must preserve the previously registered value.

  • Switch back to ram_tb.sv in Notepad.
  • Add the write-stability assertion directly below ap_read_result_known.
  ap_write_holds_read_data: assert property (
    @(posedge clk) disable iff (!rst_n)
    wr_en |=> $stable(rd_data)
  ) else begin
    $error("ASSERTION FAILED: rd_data changed during a write cycle");
    errors++;
  end

How does the stability guard work?

The antecedent begins when wr_en is high at a rising edge. At the following clock sample, $stable(rd_data) checks that the registered read output has not changed.

A failure increments the same errors counter used by the reference-memory comparisons. Assertion failures therefore prevent a false pass.

  • Save ram_tb.sv.
  • Return to the Questa Transcript.
  • Compile all three assertion guards by entering:
vlog single_port_ram.sv ram_tb.sv

What does the final compile establish?

A clean compile confirms that all three named properties are part of ram_tb. The next run tests those properties against the complete regression.

You should see the compilation complete without an error.

Seeing an error around $stable?

  • Confirm that the function call is $stable(rd_data).
  • Confirm that the property uses wr_en |=> before the function call.
  • Use this stability-assertion debugging prompt to compare the property.

✔️ Awesome, I've got everything!

Excellent. Double check that ram_tb.sv is saved with all three named assertions.

ⓧ I'd like to double check the full code

Compare your completed testbench with this exact file.

`timescale 1ns/1ps

module ram_tb;

  localparam int DATA_WIDTH = 8;
  localparam int ADDR_WIDTH = 4;
  localparam int DEPTH = 16;

  logic clk;
  logic rst_n;
  logic wr_en;
  logic [ADDR_WIDTH-1:0] addr;
  logic [DATA_WIDTH-1:0] wr_data;
  logic [DATA_WIDTH-1:0] rd_data;

  logic [DATA_WIDTH-1:0] model_mem [0:DEPTH-1];
  integer errors;
  integer index;

  single_port_ram #(
    .DATA_WIDTH(DATA_WIDTH),
    .ADDR_WIDTH(ADDR_WIDTH),
    .DEPTH(DEPTH)
  ) dut (
    .clk(clk),
    .rst_n(rst_n),
    .wr_en(wr_en),
    .addr(addr),
    .wr_data(wr_data),
    .rd_data(rd_data)
  );

  always #5 clk = ~clk;

  ap_controls_known: assert property (
    @(posedge clk) disable iff (!rst_n)
    !$isunknown({wr_en, addr})
  ) else begin
    $error("ASSERTION FAILED: wr_en or addr is unknown");
    errors++;
  end

  ap_read_result_known: assert property (
    @(posedge clk) disable iff (!rst_n)
    !wr_en |=> !$isunknown(rd_data)
  ) else begin
    $error("ASSERTION FAILED: read result is unknown");
    errors++;
  end

  ap_write_holds_read_data: assert property (
    @(posedge clk) disable iff (!rst_n)
    wr_en |=> $stable(rd_data)
  ) else begin
    $error("ASSERTION FAILED: rd_data changed during a write cycle");
    errors++;
  end

  task automatic reset_dut;
    integer reset_index;
    begin
      rst_n = 1'b0;
      wr_en = 1'b0;
      addr = '0;
      wr_data = '0;
      for (reset_index = 0; reset_index < DEPTH; reset_index = reset_index + 1)
        model_mem[reset_index] = '0;
      repeat (2) @(posedge clk);
      @(negedge clk);
      rst_n = 1'b1;
      @(posedge clk);
      #1;
      if (rd_data !== '0) begin
        $error("RESET MISMATCH: expected=00 actual=%02h", rd_data);
        errors++;
      end
    end
  endtask

  task automatic write_word(input logic [ADDR_WIDTH-1:0] write_addr, input logic [DATA_WIDTH-1:0] value);
    begin
      @(negedge clk);
      wr_en = 1'b1;
      addr = write_addr;
      wr_data = value;
      @(posedge clk);
      #1;
      model_mem[write_addr] = value;
    end
  endtask

  task automatic read_check(input logic [ADDR_WIDTH-1:0] read_addr);
    begin
      @(negedge clk);
      wr_en = 1'b0;
      addr = read_addr;
      wr_data = '0;
      @(posedge clk);
      #1;
      if (rd_data !== model_mem[read_addr]) begin
        $error("DATA MISMATCH: addr=%0d expected=%02h actual=%02h", read_addr, model_mem[read_addr], rd_data);
        errors++;
      end
    end
  endtask

  initial begin
    clk = 1'b0;
    rst_n = 1'b0;
    wr_en = 1'b0;
    addr = '0;
    wr_data = '0;
    errors = 0;
    reset_dut();
    write_word(2, 8'h2A);
    read_check(2);
    write_word(5, 8'h5A);
    read_check(5);
    if (errors == 0) $display("SMOKE TEST PASS");
    for (index = 0; index < DEPTH; index = index + 1) write_word(index, index + 1);
    for (index = 0; index < DEPTH; index = index + 1) read_check(index);
    if (errors == 0) $display("TEST PASS: RAM address sweep completed with 0 errors");
    else $display("TEST FAIL: RAM address sweep completed with %0d errors", errors);
    $finish;
  end

endmodule
Run the guarded regression

The final run keeps every directed check intact. It also keeps the complete address sweep intact while the assertions monitor each clock sample.

  • Predict whether address 15 now matches 8'h10 without any assertion errors.
  • Run the complete guarded regression by entering these commands in the Questa Transcript in order:
vlog single_port_ram.sv ram_tb.sv
vsim ram_tb
run -all

What does the final regression cover?

  • The directed checks confirm that addresses 2 and 5 still behave correctly.
  • The complete sweep compares all 16 addresses with model_mem.
  • The named assertions monitor known controls, known read data, plus stable read data across write cycles.

You should see TEST PASS: RAM address sweep completed with 0 errors in the Transcript. The missing address 15 mismatch confirms that rd_data matched 8'h10.

The Transcript should remain free of assertion errors. Every existing test now passes under all three guards.

Does the final regression report an error?

  • Check that all three assertions appear between the clock generator and reset_dut.
  • Check that each property contains disable iff (!rst_n).
  • Use this regression debugging prompt to isolate the failing check.

Strong work. Your regression now proves the address 15 correction while three concurrent assertions protect the RAM interface on future runs.

Secret mission

Verify Back-to-Back Overwrites

One address can receive new data on consecutive clock cycles. Extend the existing testbench to prove that the second write replaces the first while every existing check still passes.

Clean Up Your Resources

Clean Up Your Resources

Your RAM project runs entirely on your Windows computer with Questa Sim-64 2021.2. It creates no new cloud charges or paid services.

Decide whether to keep the project ready for future regression runs, pause by closing the simulator, or delete the local files.

Resources you used:

  • Local project folder (C:\nextwork\ram-verification).
  • Local SystemVerilog files (hello.sv, single_port_ram.sv, ram_tb.sv).
  • Local Questa simulation library (work) plus generated simulation files inside the project folder.

Keep everything running

Keep the folder as it is for future regression runs. The source files remain ready for more testbench checks.

  • Leave C:\nextwork\ram-verification on your computer.
  • Keep hello.sv, single_port_ram.sv, ram_tb.sv available for future simulations.
  • Keep the work simulation library for the next regression run.
  • Your local project files do not accumulate usage charges.

Pause - I'll come back to this later

Pausing closes the running simulator while preserving every project file.

  • Close the simulator if it is still running.
  • Keep C:\nextwork\ram-verification unchanged.
  • Return to the same folder when you are ready to rerun the regression.

Delete - I don't want to use this again

Delete this project when you want a clean slate on your computer.

The folder deletion is final, which can feel drastic. Your existing simulator installation stays available.

  • Close the simulator if it is still running.
  • Close the Windows PowerShell window that is currently using C:\nextwork\ram-verification.
  • In File Explorer, navigate to C:\nextwork.
  • Select the ram-verification folder.
  • Press Shift+Delete to remove the selected folder permanently.
  • Approve the permanent deletion confirmation.
  • Confirm that ram-verification is no longer listed in C:\nextwork.

Nice Work!

Nice Work!

Excellent work. Your synchronous RAM now passes a self-checking SystemVerilog regression across all 16 addresses.

You've learned how to:

  • Design a parameterized 16-address by 8-bit RAM with deterministic reset behavior plus registered reads.
  • Build a self-checking testbench with reusable write and read tasks. Predict each result through an independent reference model.
  • Sweep all 16 addresses to expose the blocked final address. Correct the RTL condition. Protect the interface with concurrent assertions.
  • Secret Mission: Prove that consecutive writes to address 4'd3 preserve 8'h22 as the newest value.

Ready to quiz yourself?