Module 10: Capstone Projects · Project 4 — Synchronous SRAM Controller

Project 4, Part 3: A Testbench That Enforces the Timing

The previous article built the FSM. This one verifies it, and the verification trap to avoid is this: test the controller against the idealised Section 7.1 memory, which returns correct data whenever you read it, and a controller with a real timing bug (reading a cycle early, say) passes every test. The bug would only appear against a real chip whose timing does not forgive an early read. The fix is a memory model that enforces the contract and fails loudly when the controller cheats.

A timing-accurate memory model

module timing_accurate_sram #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 256,
    parameter T_AA_CYCLES = 2   // matches this project's READ_WAIT_CYCLES
) (
    input  logic                  clk,
    input  logic                  ce_n, oe_n, we_n,
    input  logic [$clog2(DEPTH)-1:0] addr,
    input  logic [DATA_WIDTH-1:0] data_in,
    output logic [DATA_WIDTH-1:0] data_out
);
    logic [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    logic [$clog2(DEPTH)-1:0] addr_latched;
    logic access_active = 0;
    integer cycles_since_access = 0;

    always_ff @(posedge clk) begin
        if (!ce_n && !oe_n) begin
            if (!access_active) begin
                access_active <= 1'b1;
                cycles_since_access <= 1;
                addr_latched <= addr;
            end else begin
                cycles_since_access <= cycles_since_access + 1;
            end
        end else begin
            access_active <= 1'b0;
            cycles_since_access <= 0;
        end

        if (!ce_n && !we_n) begin
            mem[addr] <= data_in;   // writes rely on the controller's t_WP enforcement
        end
    end

    always_comb begin
        if (access_active && cycles_since_access >= T_AA_CYCLES)
            data_out = mem[addr_latched];   // timing satisfied: real data
        else
            data_out = {DATA_WIDTH{1'bx}};  // timing NOT satisfied: X, deliberately
    end
endmodule

The critical line is the else of the final always_comb. If the controller samples data_out before T_AA_CYCLES have elapsed since the access began, the model returns X on purpose, not a plausible wrong value that might coincidentally match, but an unmistakable X that a self-checking testbench flags immediately. That is the X-propagation behaviour from Section 8.4, deliberately engineered into a verification model rather than being an accidental bug in one.

Assembling and verifying correct operation

module tb_sram_controller;
    logic clk = 0, rst_n;
    logic read_req, write_req, ready;
    logic [7:0] addr, write_data, read_data;

    sram_controller dut (.clk(clk), .rst_n(rst_n), .addr(addr),
                          .read_req(read_req), .write_req(write_req),
                          .write_data(write_data), .read_data(read_data), .ready(ready));

    timing_accurate_sram #(.T_AA_CYCLES(2)) mem (
        .clk(clk), .ce_n(dut.ce_n), .oe_n(dut.oe_n), .we_n(dut.we_n),
        .addr(addr), .data_in(write_data), .data_out(read_data)
    );

    always #5 clk = ~clk;

    task automatic write_and_verify(bit [7:0] a, bit [7:0] d);
        addr = a; write_data = d; write_req = 1'b1;
        @(posedge clk); write_req = 1'b0;
        wait (ready); @(posedge clk);

        addr = a; read_req = 1'b1;
        @(posedge clk); read_req = 1'b0;
        wait (ready);

        if (read_data === d)
            $display("PASS: addr=%0d wrote %h, read back %h", a, d, read_data);
        else
            $display("FAIL: addr=%0d wrote %h, read back %h (X means the timing was violated)", a, d, read_data);
        @(posedge clk);
    endtask

    initial begin
        rst_n = 0; read_req = 0; write_req = 0;
        repeat (3) @(posedge clk); rst_n = 1;

        write_and_verify(8'd10, 8'hA5);
        write_and_verify(8'd20, 8'h3C);
        write_and_verify(8'd0,  8'hFF);

        $finish;
    end
endmodule

Note the === comparison. The Section 6.2 distinction matters directly here: === distinguishes a matching value from an X, whereas == would evaluate to X (neither true nor false) if read_data were X, silently failing to flag the violation the model was built to catch.

The test that proves the enforcement works

This is what the project has been building to: not only “does the controller work” but “would this test infrastructure catch a controller that cheated on timing”:

// A deliberately BROKEN controller variant for this one test: reads
// one cycle too early, violating T_AA_CYCLES
module sram_controller_broken_for_test (/* same ports as sram_controller */);
    // Identical to sram_controller EXCEPT:
    // localparam READ_WAIT_CYCLES = 1;  <-- BUG: should be 2, per Part 1
endmodule
initial begin
    // ... instantiate sram_controller_broken_for_test against the SAME
    // timing_accurate_sram model, T_AA_CYCLES=2 ...
    write_and_verify(8'd10, 8'hA5);
    // Expected: this SHOULD report FAIL with read_data reading X,
    // confirming the timing-accurate model catches an early read
    // that an idealised memory never would
end

Running this negative test and confirming it fails as expected is arguably the most important step in the project. It is direct proof that the testbench is not passing by coincidence, that it can catch the class of bug (an early read, a timing violation) the project exists to prevent. A verification environment that cannot demonstrably catch a deliberately introduced bug is not proven to be catching anything. This technique, injecting a known bug and confirming the environment catches it, is called mutation testing or bug injection, and it is a standard way of measuring whether a verification environment is doing its job rather than merely running without complaint.

An idealised memory that always returns correct data would let a broken controller pass every test. The timing-accurate model closes that gap by returning X whenever the controller samples before the access time has elapsed, turning an invisible timing bug into a ===-checkable failure. Proving the environment itself works, by injecting a known bug and confirming it is caught, is the most convincing evidence the testing is real. That closes Project 4. In the next article, Project 5, the last of the course, moves to a standardized bus protocol, AXI4-Lite, bringing together Module 9’s interfaces and assertions with everything this project taught about respecting an external contract.

← Project 4, Part 2: An FSM Built From Wait States Project 5, Part 1: AXI4-Lite, Five Independent Handshakes →