Source / Read the circuit

tb_spi_master.sv

lessons/01_spi_master/tb/tb_spi_master.sv

`timescale 1ns/1ps
module tb_spi_master;
    parameter integer HALF_PERIOD_CYCLES = 25;
    localparam integer HALF_NS = HALF_PERIOD_CYCLES * 20;
    logic clk = 0;
    always #10 clk = ~clk;
    logic rst_n = 0, start = 0;
    logic [7:0] tx_data = 0, rx_data;
    logic busy, done, cs_n, sclk, mosi, miso = 0;
    logic [7:0] expected_tx = 0, slave_reply = 0;
    integer samples = 0, falls = 0, completed = 0;
    integer done_pulses = 0;
    logic previous_done = 0;
    time cs_asserted, last_edge;

    spi_master #(.HALF_PERIOD_CYCLES(HALF_PERIOD_CYCLES)) dut (.*);

    // Independent SPI slave: its response does not depend on MOSI.
    always @(negedge cs_n) begin
        if (rst_n) begin
            samples = 0;
            falls = 0;
            cs_asserted = $time;
            last_edge = $time;
            miso = slave_reply[7];
            if (sclk !== 0 || busy !== 1)
                $fatal(1, "Bad state at CS assertion");
        end
    end

    always @(posedge sclk) begin
        if (rst_n) begin
            if (cs_n !== 0 || busy !== 1 || samples >= 8)
                $fatal(1, "Unexpected SCLK sampling edge");
            if (($time - last_edge) != HALF_NS)
                $fatal(1, "Bad SCLK rising half-period");
            if (mosi !== expected_tx[7-samples])
                $fatal(1, "MOSI mismatch bit %0d: expected %b got %b",
                       samples, expected_tx[7-samples], mosi);
            samples = samples + 1;
            last_edge = $time;
        end
    end

    always @(negedge sclk) begin
        if (rst_n && cs_n === 0) begin
            if (($time - last_edge) != HALF_NS)
                $fatal(1, "Bad SCLK falling half-period");
            falls = falls + 1;
            if (falls > 8) $fatal(1, "Extra SCLK falling edge");
            if (samples < 8) miso = slave_reply[7-samples];
            last_edge = $time;
        end
    end

    always @(posedge cs_n) begin
        if (rst_n) begin
            if (samples != 8 || falls != 8)
                $fatal(1, "Wrong SPI edge count: rises=%0d falls=%0d", samples, falls);
            if (sclk !== 0 || busy !== 0 || done !== 1)
                $fatal(1, "Bad completion outputs");
            if (($time - last_edge) != HALF_NS)
                $fatal(1, "Bad CS hold time");
            if (($time - cs_asserted) != 17 * HALF_NS)
                $fatal(1, "Bad frame duration");
            completed = completed + 1;
        end
    end

    // Sample after sequential nonblocking updates.
    always @(posedge clk) begin
        #1;
        if (!rst_n) begin
            if (cs_n !== 1 || sclk !== 0 || busy !== 0 || done !== 0 ||
                mosi !== 0 || rx_data !== 0)
                $fatal(1, "Reset outputs incorrect");
            previous_done = 0;
        end else begin
            if (busy !== !cs_n) $fatal(1, "busy/CS contract broken");
            if (done && previous_done) $fatal(1, "done is longer than one clock");
            if (done) done_pulses = done_pulses + 1;
            previous_done = done;
        end
    end

    task automatic transfer(input logic [7:0] send_byte,
                            input logic [7:0] reply_byte,
                            input logic inject_busy_start);
        integer count_before;
        begin
            count_before = completed;
            @(negedge clk);
            expected_tx = send_byte;
            slave_reply = reply_byte;
            tx_data = send_byte;
            start = 1;
            @(negedge clk);
            start = 0;
            if (busy !== 1) $fatal(1, "start not accepted");
            // Corrupt the live input after acceptance; latched TX must survive.
            tx_data = ~send_byte;
            if (inject_busy_start) begin
                repeat (3) @(negedge clk);
                start = 1;
                @(negedge clk);
                start = 0;
            end
            wait (done === 1);
            #2;
            if (rx_data !== reply_byte)
                $fatal(1, "RX mismatch: expected %02x got %02x", reply_byte, rx_data);
            if (completed != count_before + 1)
                $fatal(1, "Completion count wrong");
            repeat (3) @(negedge clk);
            if (busy !== 0 || cs_n !== 1 || sclk !== 0 || done !== 0)
                $fatal(1, "Unexpected re-start or bad idle");
        end
    endtask

    integer n;
    integer before_abort, pulses_before_abort;
    initial begin
        if (HALF_PERIOD_CYCLES < 1) $fatal(1, "Divider must be positive");
        $dumpfile("spi_master.vcd");
        $dumpvars(0, tb_spi_master);
        repeat (4) @(negedge clk);
        rst_n = 1;
        transfer(8'hA5, 8'h3C, 1);
        for (n = 0; n < 256; n = n + 1)
            transfer(n[7:0], n[7:0] ^ 8'h69, (n % 17) == 0);

        // Abort an active frame by synchronous reset.
        before_abort = completed;
        pulses_before_abort = done_pulses;
        @(negedge clk);
        expected_tx = 8'h96;
        slave_reply = 8'h5A;
        tx_data = expected_tx;
        start = 1;
        @(negedge clk);
        start = 0;
        repeat (HALF_PERIOD_CYCLES * 3 + 1) @(negedge clk);
        if (busy !== 1) $fatal(1, "Abort test did not reach active frame");
        rst_n = 0;
        repeat (3) @(negedge clk);
        if (completed != before_abort || done_pulses != pulses_before_abort)
            $fatal(1, "Reset abort incorrectly completed a transaction");
        rst_n = 1;
        transfer(8'h81, 8'h7E, 0);
        if (completed != 258 || done_pulses != 258)
            $fatal(1, "Final accounting mismatch: completed=%0d done=%0d",
                   completed, done_pulses);
        $display("PASS divider=%0d transactions=%0d exhaustive_tx=256 busy_rejection=ok reset_abort=ok",
                 HALF_PERIOD_CYCLES, completed);
        $finish;
    end

    initial begin
        #10000000;
        $fatal(1, "Global test timeout");
    end
endmodule