`timescale 1ns/1ps
module axil_spi (
    input  logic        aclk,
    input  logic        aresetn,
    input  logic [31:0] s_axi_awaddr,
    input  logic        s_axi_awvalid,
    output logic        s_axi_awready,
    // No writable field occupies byte lanes two or three.
    /* verilator lint_off UNUSEDSIGNAL */
    input  logic [31:0] s_axi_wdata,
    input  logic [3:0]  s_axi_wstrb,
    /* verilator lint_on UNUSEDSIGNAL */
    input  logic        s_axi_wvalid,
    output logic        s_axi_wready,
    output logic [1:0]  s_axi_bresp,
    output logic        s_axi_bvalid,
    input  logic        s_axi_bready,
    input  logic [31:0] s_axi_araddr,
    input  logic        s_axi_arvalid,
    output logic        s_axi_arready,
    output logic [31:0] s_axi_rdata,
    output logic [1:0]  s_axi_rresp,
    output logic        s_axi_rvalid,
    input  logic        s_axi_rready,
    output logic        spi_sck,
    output logic        spi_cs_n,
    output logic        spi_mosi,
    input  logic        spi_miso
);
    localparam logic [31:0] STATUS  = 32'h00;
    localparam logic [31:0] DIVIDER = 32'h04;
    localparam logic [31:0] TXDATA  = 32'h08;
    localparam logic [31:0] RXDATA  = 32'h0c;
    localparam logic [1:0] OKAY = 2'b00, SLVERR = 2'b10;

    logic aw_pending, w_pending;
    logic [31:0] awaddr;
    logic [15:0] wdata;
    logic [1:0] wstrb;
    logic write_commit;
    logic [15:0] divider, merged_divider;
    logic spi_start, spi_busy, spi_done, done_latched;
    logic [7:0] spi_rx;

    // Capture AW and W independently. Block further requests until the
    // response is consumed so each buffered pair commits exactly once.
    assign s_axi_awready = aresetn && !aw_pending && !s_axi_bvalid;
    assign s_axi_wready  = aresetn && !w_pending && !s_axi_bvalid;
    assign s_axi_arready = aresetn && !s_axi_rvalid;
    assign write_commit = aw_pending && w_pending;
    assign spi_start = write_commit && awaddr == TXDATA && wstrb[0] && !spi_busy;

    assign merged_divider = {
        wstrb[1] ? wdata[15:8] : divider[15:8],
        wstrb[0] ? wdata[7:0]  : divider[7:0]
    };

    always_ff @(posedge aclk or negedge aresetn) begin
        if (!aresetn) begin
            aw_pending <= 1'b0;
            w_pending <= 1'b0;
            awaddr <= '0;
            wdata <= '0;
            wstrb <= '0;
            s_axi_bresp <= OKAY;
            s_axi_bvalid <= 1'b0;
            divider <= 16'd2;
            done_latched <= 1'b0;
        end else begin
            if (s_axi_awvalid && s_axi_awready) begin
                awaddr <= s_axi_awaddr;
                aw_pending <= 1'b1;
            end
            if (s_axi_wvalid && s_axi_wready) begin
                wdata <= s_axi_wdata[15:0];
                wstrb <= s_axi_wstrb[1:0];
                w_pending <= 1'b1;
            end
            if (s_axi_bvalid && s_axi_bready)
                s_axi_bvalid <= 1'b0;

            if (write_commit) begin
                aw_pending <= 1'b0;
                w_pending <= 1'b0;
                s_axi_bvalid <= 1'b1;
                s_axi_bresp <= OKAY;
                case (awaddr)
                    STATUS: if (wstrb[0] && wdata[1])
                        done_latched <= 1'b0;
                    DIVIDER: if (|wstrb) begin
                        if (merged_divider == 0)
                            s_axi_bresp <= SLVERR;
                        else
                            divider <= merged_divider;
                    end
                    TXDATA: if (wstrb[0]) begin
                        if (spi_busy)
                            s_axi_bresp <= SLVERR;
                        else
                            done_latched <= 1'b0;
                    end
                    default: s_axi_bresp <= SLVERR;
                endcase
            end

            // A completion wins over W1C. A new accepted frame clears the old
            // completion even when it starts as the previous pulse is observed.
            if (spi_done && !spi_start)
                done_latched <= 1'b1;
        end
    end

    always_ff @(posedge aclk or negedge aresetn) begin
        if (!aresetn) begin
            s_axi_rvalid <= 1'b0;
            s_axi_rresp <= OKAY;
            s_axi_rdata <= '0;
        end else begin
            if (s_axi_rvalid && s_axi_rready)
                s_axi_rvalid <= 1'b0;

            if (s_axi_arvalid && s_axi_arready) begin
                s_axi_rvalid <= 1'b1;
                s_axi_rresp <= OKAY;
                case (s_axi_araddr)
                    STATUS:  s_axi_rdata <= {30'b0, done_latched || spi_done, spi_busy};
                    DIVIDER: s_axi_rdata <= {16'b0, divider};
                    RXDATA:  s_axi_rdata <= {24'b0, spi_rx};
                    default: begin
                        s_axi_rresp <= SLVERR;
                        s_axi_rdata <= '0;
                    end
                endcase
            end
        end
    end

    spi_master_mode0 spi (
        .clk(aclk), .rst_n(aresetn), .start(spi_start),
        .half_divider(divider), .tx_data(wdata[7:0]),
        .busy(spi_busy), .done(spi_done), .rx_data(spi_rx),
        .spi_sck, .spi_cs_n, .spi_mosi, .spi_miso
    );
endmodule
