Source / Read the circuit

spi_master.sv

lessons/01_spi_master/rtl/spi_master.sv

`timescale 1ns/1ps
// SPI master: Mode 0, 8-bit, MSB first. All storage uses clk.
// rst_n is synchronous active-low. start is a one-cycle clk-domain pulse.
module spi_master #(
    parameter integer HALF_PERIOD_CYCLES = 25
) (
    input  logic       clk,
    input  logic       rst_n,
    input  logic       start,
    input  logic [7:0] tx_data,
    output logic [7:0] rx_data,
    output logic       busy,
    output logic       done,
    output logic       cs_n,
    output logic       sclk,
    output logic       mosi,
    input  logic       miso
);
    localparam integer DIV_BITS =
        (HALF_PERIOD_CYCLES > 1) ? $clog2(HALF_PERIOD_CYCLES) : 1;
    localparam logic [DIV_BITS-1:0] DIV_LAST = HALF_PERIOD_CYCLES - 1;
    typedef enum logic [1:0] {IDLE, TRANSFER, FINISH} state_t;
    state_t state;
    logic [DIV_BITS-1:0] div_count;
    logic [2:0] bit_count;
    logic [7:0] tx_shift, rx_shift;

    // HALF_PERIOD_CYCLES must be a positive integer (checked by the runner).
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            state     <= IDLE;
            div_count <= '0;
            bit_count <= '0;
            tx_shift  <= '0;
            rx_shift  <= '0;
            rx_data   <= '0;
            busy      <= 1'b0;
            done      <= 1'b0;
            cs_n      <= 1'b1;
            sclk      <= 1'b0;
            mosi      <= 1'b0;
        end else begin
            done <= 1'b0;
            case (state)
                IDLE: begin
                    if (start) begin
                        tx_shift  <= tx_data;
                        rx_shift  <= '0;
                        bit_count <= '0;
                        div_count <= '0;
                        mosi      <= tx_data[7]; // First bit before first rising edge.
                        cs_n      <= 1'b0;
                        sclk      <= 1'b0;
                        busy      <= 1'b1;
                        state     <= TRANSFER;
                    end
                end
                TRANSFER: begin
                    if (div_count == DIV_LAST) begin
                        div_count <= '0;
                        if (!sclk) begin
                            sclk     <= 1'b1;
                            rx_shift <= {rx_shift[6:0], miso};
                            if (bit_count == 3'd7)
                                rx_data <= {rx_shift[6:0], miso};
                        end else begin
                            sclk <= 1'b0;
                            if (bit_count == 3'd7) begin
                                state <= FINISH;
                            end else begin
                                bit_count <= bit_count + 1'b1;
                                tx_shift  <= {tx_shift[6:0], 1'b0};
                                mosi      <= tx_shift[6]; // Old register value.
                            end
                        end
                    end else begin
                        div_count <= div_count + 1'b1;
                    end
                end
                FINISH: begin
                    // Hold CS for one full half-period after final falling edge.
                    if (div_count == DIV_LAST) begin
                        div_count <= '0;
                        cs_n      <= 1'b1;
                        busy      <= 1'b0;
                        done      <= 1'b1;
                        mosi      <= 1'b0;
                        state     <= IDLE;
                    end else begin
                        div_count <= div_count + 1'b1;
                    end
                end
                default: begin
                    state <= IDLE;
                    cs_n  <= 1'b1;
                    sclk  <= 1'b0;
                    busy  <= 1'b0;
                    mosi  <= 1'b0;
                end
            endcase
        end
    end
endmodule