`timescale 1ns/1ps
module tb_axil_spi;
    localparam logic [31:0] STATUS = 0, DIVIDER = 4, TXDATA = 8, RXDATA = 12;
    localparam logic [1:0] OKAY = 0, SLVERR = 2;
    logic aclk = 0, aresetn = 0;
    logic [31:0] s_axi_awaddr = 0, s_axi_wdata = 0, s_axi_araddr = 0, s_axi_rdata;
    logic [3:0] s_axi_wstrb = 0;
    logic [1:0] s_axi_bresp, s_axi_rresp;
    logic s_axi_awvalid = 0, s_axi_awready, s_axi_wvalid = 0, s_axi_wready;
    logic s_axi_bvalid, s_axi_bready = 0, s_axi_arvalid = 0, s_axi_arready;
    logic s_axi_rvalid, s_axi_rready = 0;
    logic spi_sck, spi_cs_n, spi_mosi, spi_miso = 0;

    logic [7:0] expected_tx = 0, reply = 0;
    time half_period = 20;
    int frames = 0;
    int expected_frames = 0;

    axil_spi dut (.*);
    always #5 aclk = !aclk;

    task automatic send_aw(input logic [31:0] address, input int delay_cycles);
        repeat (delay_cycles + 1) @(negedge aclk);
        s_axi_awaddr = address;
        s_axi_awvalid = 1;
        do @(posedge aclk); while (!s_axi_awready);
        @(negedge aclk);
        s_axi_awvalid = 0;
    endtask

    task automatic send_w(input logic [31:0] data, input logic [3:0] strobes, input int delay_cycles);
        repeat (delay_cycles + 1) @(negedge aclk);
        s_axi_wdata = data;
        s_axi_wstrb = strobes;
        s_axi_wvalid = 1;
        do @(posedge aclk); while (!s_axi_wready);
        @(negedge aclk);
        s_axi_wvalid = 0;
    endtask

    task automatic write_reg(
        input logic [31:0] address,
        input logic [31:0] data,
        input logic [3:0] strobes,
        input int skew,
        input int stalls,
        input logic [1:0] response
    );
        fork
            send_aw(address, skew > 0 ? skew : 0);
            send_w(data, strobes, skew < 0 ? -skew : 0);
        join
        while (!s_axi_bvalid) @(negedge aclk);
        repeat (stalls + 1) begin
            if (s_axi_bvalid !== 1 || s_axi_bresp !== response || s_axi_awready || s_axi_wready)
                $fatal(1, "Bad/stalled write response for %h: expected %b got %b",
                       address, response, s_axi_bresp);
            @(negedge aclk);
        end
        s_axi_bready = 1;
        @(negedge aclk);
        s_axi_bready = 0;
        if (s_axi_bvalid !== 0)
            $fatal(1, "Write response was not consumed");
    endtask

    task automatic read_reg(
        input logic [31:0] address,
        input logic [31:0] expected,
        input logic [1:0] response,
        input int stalls
    );
        @(negedge aclk);
        s_axi_araddr = address;
        s_axi_arvalid = 1;
        do @(posedge aclk); while (!s_axi_arready);
        @(negedge aclk);
        s_axi_arvalid = 0;
        while (!s_axi_rvalid) @(negedge aclk);
        repeat (stalls + 1) begin
            if (s_axi_rvalid !== 1 || s_axi_rdata !== expected || s_axi_rresp !== response || s_axi_arready)
                $fatal(1, "Bad/stalled read response for %h: expected %h/%b got %h/%b",
                       address, expected, response, s_axi_rdata, s_axi_rresp);
            @(negedge aclk);
        end
        s_axi_rready = 1;
        @(negedge aclk);
        s_axi_rready = 0;
        if (s_axi_rvalid !== 0)
            $fatal(1, "Read response was not consumed");
    endtask

    // Independent SPI slave model. Check the wire protocol as well as register results.
    always @(negedge spi_cs_n) begin
        time last_edge;
        if (!aresetn || frames >= expected_frames || spi_sck !== 0)
            $fatal(1, "Unexpected SPI frame start");
        last_edge = $time;
        for (int bit_index = 7; bit_index >= 0; bit_index--) begin
            spi_miso = reply[bit_index];
            @(posedge spi_sck);
            if (spi_cs_n !== 0 || spi_mosi !== expected_tx[bit_index] ||
                $time - last_edge != half_period)
                $fatal(1, "SPI rising edge check failed at bit %0d", bit_index);
            last_edge = $time;
            @(negedge spi_sck);
            if (spi_cs_n !== 0 || $time - last_edge != half_period)
                $fatal(1, "SPI falling edge check failed at bit %0d", bit_index);
            last_edge = $time;
        end
        @(posedge spi_cs_n);
        if ($time - last_edge != half_period || spi_sck !== 0)
            $fatal(1, "Incorrect SPI chip-select hold time");
        frames++;
        spi_miso = 0;
    end

    initial begin
        repeat (3) @(negedge aclk);
        aresetn = 1;
        read_reg(DIVIDER, 2, OKAY, 3);
        // Change a register while its earlier read response is backpressured.
        fork
            read_reg(DIVIDER, 2, OKAY, 10);
            write_reg(DIVIDER, 6, 4'b0001, 3, 0, OKAY);
        join
        read_reg(DIVIDER, 6, OKAY, 0);
        write_reg(DIVIDER, 'h0104, 4'b0011, 4, 5, OKAY); // W first
        read_reg(DIVIDER, 'h0104, OKAY, 0);
        write_reg(DIVIDER, 0, 4'b0010, -3, 0, OKAY); // AW first
        read_reg(DIVIDER, 4, OKAY, 0);
        write_reg(DIVIDER, 0, 4'b0001, 0, 0, SLVERR); // Merged value would be zero
        write_reg(DIVIDER, 0, 4'b0000, 0, 0, OKAY);
        read_reg(DIVIDER, 4, OKAY, 0);
        read_reg(32'h104, 0, SLVERR, 2); // No aliased higher addresses
        read_reg(32'h05, 0, SLVERR, 0); // Unaligned
        read_reg(TXDATA, 0, SLVERR, 0);
        write_reg(RXDATA, 32'hff, 4'b0001, 0, 0, SLVERR);
        write_reg(TXDATA, 32'hffff, 4'b0010, 0, 0, OKAY); // No low-byte strobe

        expected_tx = 8'ha6;
        reply = 8'h39;
        half_period = 40;
        expected_frames++;
        write_reg(TXDATA, 32'ha6, 4'b0001, 0, 4, OKAY);
        read_reg(STATUS, 1, OKAY, 2);
        write_reg(TXDATA, 32'hff, 4'b0001, 0, 1, SLVERR);
        write_reg(DIVIDER, 3, 4'b0001, 0, 0, OKAY); // Applies to the next frame
        wait (frames == 1);
        read_reg(STATUS, 2, OKAY, 3);
        read_reg(RXDATA, 32'h39, OKAY, 0);
        write_reg(STATUS, 2, 4'b0010, 0, 0, OKAY); // W1C requires byte zero
        read_reg(STATUS, 2, OKAY, 0);
        write_reg(STATUS, 2, 4'b0001, 0, 0, OKAY);
        read_reg(STATUS, 0, OKAY, 0);

        expected_tx = 8'hd2;
        reply = 8'hc5;
        half_period = 30;
        expected_frames++;
        write_reg(TXDATA, 32'hd2, 4'b0001, -2, 0, OKAY);
        wait (frames == 2);
        read_reg(RXDATA, 32'hc5, OKAY, 0);
        read_reg(STATUS, 2, OKAY, 0);

        send_aw(TXDATA, 0);
        aresetn = 0; // Reset a partially received write.
        repeat (3) @(negedge aclk);
        aresetn = 1;
        write_reg(DIVIDER, 5, 4'b0001, 3, 2, OKAY);
        read_reg(DIVIDER, 5, OKAY, 0);
        read_reg(STATUS, 0, OKAY, 0);
        if (frames != expected_frames || frames != 2)
            $fatal(1, "Expected exactly two SPI frames");
        $display("PASS AXI4-Lite register bank and SPI mode-0 peripheral");
        $finish;
    end

    initial begin
        #25000;
        $fatal(1, "AXI/SPI test timed out");
    end
endmodule
