1. Home
  2. Docs
  3. VSDSquadron FM (FPGA Mini...
  4. Projects
  5. vsdsquadron-fpga-ip-spi

vsdsquadron-fpga-ip-spi

IP Overview

The SPI Master IP (Mode-0) is a memory-mapped peripheral designed for the VSDSquadron RISC-V SoC. It enables communication with external SPI-compatible devices using SPI Mode-0 timing.

Typical Use Cases

  • SPI sensors (IMU, temperature, ADC)
  • SPI Flash memory
  • DAC / ADC devices
  • Expansion header peripherals

Why Use This IP

  • Provides deterministic SPI timing in hardware
  • Reduces software complexity compared to bit-banging
  • Simple register-based control model
  • Plug-and-play integration with VSDSquadron SoC

Feature Summary

  • SPI Master operation
  • SPI Mode-0 (CPOL=0, CPHA=0)
  • 8-bit full-duplex transfer
  • Single slave select
  • Programmable clock divider
  • Polling-based status control

Clock Assumptions

  • Uses system clock clk
  • SPI clock derived from programmable divider

Limitations

  • Only Mode-0 supported
  • Single SPI slave
  • No interrupts
  • No FIFO support

Block Diagram

The following block diagram shows the logical structure of the SPI Master IP and its interaction with the RISC-V CPU and external SPI device.

Block Description

  • Register Decode
    Decodes memory-mapped accesses from the CPU and selects SPI registers.
  • Control Registers
    Hold enable, start, clock divider, and transmit data.
  • SPI Control FSM
    Generates SPI clock, controls chip-select, shifts data, and captures received bits.
  • SPI Signals
    Standard SPI Master signals connected to an external peripheral.

Hardware Usage

Terms

  • ICESTORM_LC – Logic Cell Usage
  • ICESTORM_RAM – BRAM Usage
  • ICESTORM_HFOSC – Internal High Frequency Oscillator
  • SB_IO – FPGA Pins Usage
  • SB_GB – Global Buffer Usage

Key Observation

  • SoC frequency – 12 MHz , produced by internal oscillator
  • ALU and Counters are optimised using carry chains.
  • 100% utilization of Global Buffer suggests we cannot add extra clock domains

RTL And Register Map

SPI Master IP Module is built seperatln and instantiated in SoC Module. You can find the RTL Code for SPI Master IP below

SPI Master IP RTL

module SPI(
//Global clk and rst
    input clk,
    input rst,
//Control and data signals
    input sel,
    input w_en,
    input r_en,
    input [31:0] wdata,
    output reg [31:0] rdata,
    input [1:0] offset,
//SPI Master signals
    output reg sclk,
    output reg mosi,
    input miso,
    output reg cs_n
    );

//register file
localparam CNTRL=2'b00;
localparam TXDATA=2'b01;
localparam RXDATA=2'b10;
localparam STATUS=2'b11;

//control register fields
reg en;
reg start;
reg [7:0]clkdiv; 

//status reg
reg busy;
reg done;

//counter for clkddivider
reg [7:0]clk_cnt;


//bit counter for transfering 8 bits
reg [2:0]bit;

//Data registers
reg [7:0]tx;
reg [7:0]rx;
reg [7:0]rxdata; //rx data will be stored in done phase to avoid corruption
//REGISTER write 
always @(posedge clk) begin
    if(rst) begin
        en<=0;
        start<=0;
        clkdiv<=0;
        bit<=0;
        busy<=0;
        done<=0;
        clkdiv<=0;
        tx<=0;
        rx<=0;
        rxdata<=0;
        end
        
     else if(sel && w_en) begin
        case(offset) 
            CNTRL: begin en<=wdata[0];
                            clkdiv<=wdata[15:8];
                            if(~busy)
                                start<=wdata[1];
                   end
            TXDATA: begin tx<=wdata[7:0];
                    end
            STATUS: begin if(wdata[1])
                            done<=0;
                    end
         endcase
      end       
end
  
//FSM
reg [1:0]state;

localparam IDLE=2'b00;
localparam  DATA_STATE=2'b01;
localparam DONE=2'b10;


always @(posedge clk) begin
    if(rst) begin
            state   <= IDLE;
            sclk    <= 1'b0;
            cs_n    <= 1'b1;
            mosi    <= 1'b0;
            busy    <= 1'b0;
            done    <= 1'b0;
            clk_cnt <= 8'd0;
            bit <= 3'd0;
            rx<= 8'd0;
            rxdata  <= 8'd0;
        end
        
      
     else begin
        case(state)
            IDLE:begin sclk<=1'b0;
                        cs_n<=1'b1;
                        busy<=1'b0;
                        if(en && start && ~busy) begin
                            cs_n<=1'b0;
                            busy<=1'b1;
                            clk_cnt<=8'd0;
                            bit<=3'd7;
                            mosi<=tx[7];
                            rx<=0;
                            done<=1'b0;
                            state<=DATA_STATE;
                         end                             
                   end
      DATA_STATE: begin if(clk_cnt == clkdiv)begin
                            clk_cnt<=8'd0;
                            sclk<=~sclk;
                          //Sample MISO at falling eddge according to mode 0  
                            if(sclk == 1'b0)
                                rx<={rx[6:0],miso};
                         //Shift at Rising Edge       
                            else begin 
                                    if(bit != 0) begin
                                        bit<=bit - 1;
                                        mosi<=tx[bit - 1];
                                    end
                                    else state<=DONE;
                            end
                         end
                         else clk_cnt<=clk_cnt + 1;                                                       
                    end
            DONE: begin cs_n<=1'b1;
                        sclk<=1'b0;
                        busy<=1'b0;
                        done<=1'b1;
                        rxdata<=rx;
                        state<=IDLE;
                    end    
         default: state<=IDLE;
        endcase
    end
end

//READ
always @(*) begin
    if(sel && r_en) begin
        case(offset)
            CNTRL: rdata = {16'd0,clkdiv , 6'd0, start, en};
            TXDATA: rdata = {24'd0, tx};
            RXDATA: rdata = {24'd0,rxdata};
            STATUS: rdata = {29'd0, 1'b1, done, busy};
            default: rdata= 32'b0;
         endcase
      end
end             
endmodule

Use the following instantiation template inside the SoC top:

wire [31:0] spi_rdata;

SPI SPI (
  .clk      (clk),
  .rst      (!resetn),
  .sel      (isIO & mem_addr[12]),
  .w_en     (mem_wstrb),
  .r_en     (mem_rstrb),
  .offset   (mem_addr[3:2]),
  .wdata    (mem_wdata),
  .rdata    (spi_rdata),

  // SPI pins
  .sclk     (sclk),
  .mosi     (mosi),
  .miso     (miso),
  .cs_n     (cs_n)
);

The SoC Has the instantiated IP ,so there is are no changes required for this file. It can be directly use to Generate Bitstream after download. All requied dependencies are provide in /RTL.

Register Map

Address Mapping

SPI IP is selected when:

  • mem_addr[22] == 1 (IO space)
  • mem_addr[12] == 1 (SPI offset)

SPI IP base address: 0x00401000

Register Summary

OffsetRegisterR/WDescription
0x00CTRLR/WEnable, start, clock divider
0x04TXDATAR/WTransmit data
0x08RXDATARReceived data
0x0CSTATUSR/WBusy and done flags

CTRL Register (0x00)

BitsNameAccessDescription
0ENR/WEnable SPI
1STARTR/WStart transfer
15:8CLKDIVR/WSPI clock divider
OthersReserved

Reset value: 0x00000000

TXDATA Register (0x04)

BitsNameAccessDescription
7:0TXR/WTransmit byte

RXDATA Register (0x08)

BitsNameAccessDescription
7:0RXRReceived byte

STATUS Register (0x0C)


How to Use IP

1. Create a C program with address mapping file included Or use a Pre made C Program SPI_MASTER.

    2. Convert it to .hex file using command

    cd Firmware/
    make SPI_MASTER.bram.hex

    The make file copies your Hex file to firmware.h in your RTL folder.

    3. Generating Bitstream

    cd ..
    cd RTL/
    make build

    4. Flash to your FPGA

    make flash

    5. To check UART output

    make terminal

    Pins

    InterfaceSignalDirectionFPGA PinNotes
    ClockCLKInput28System clock input
    ResetRESETInput10Active-high reset input
    UARTTXDOutput4UART transmit (FPGA → PC)
    UARTRXDInput3UART receive (PC → FPGA)
    GPIO LEDsGPIO[0]Input/Output38Digital GPIO 0
    GPIO LEDsGPIO[1]Input/Output43Digital GPIO 1
    GPIO LEDsGPIO[2]Input/Output45Digital GPIO 2
    GPIO LEDsGPIO[3]Input/Output47Digital GPIO 3
    GPIO LEDsGPIO[4]Input/Output6Digital GPIO 4
    SPIsclkOutput34SPI clock
    SPImosiOutput35SPI MOSI
    SPImisoInput36SPI MISO
    SPIcs_nOutput37SPI chip select (active low)

    Demo

    LED Order (From left)

    • LED 1 – CS_N
    • LED 2 – MOSI
    • LED 3 – SCLK

    The above Program is running in a loop, hence when reset is clicked CS_N goes to logic state 1. Later to Logic 0 indicating that slave is selected.

    SCLK frequency is 23.5 KHz

    UART Output

    — SPI MASTER IP TEST (MODE-0) —

    A5->XX

    A5->XX

    A5->XX