Design and Implementation of SPI Interface Using Verilog
Overview of SPI Communication Protocol
The Serial Peripheral Interface (SPI) is a synchronous serial communication protocol developed by Motorola. It facilitates high-speed data exchange between a master device and one or more slave devices in full-duplex mode. The interface requires only four signal lines for operation: serial clock (SCLK), master out slave in (MOSI), master in slave out (MISO), and chip select (CS).
SPI Communication Fundamentals
Master-Slave Architecture
SPI operates under a master-slave configuration where one device acts as the master and controls communication with multiple slave devices. The master generates the clock signal (SCLK) that synchronizes data transfers. Slaves cannot produce their own clock and must rely on the master's clock signal.
Synchronous Data Transfer
Data transmission occurs synchronized with the clock signal. Each bits transferred on either the rising or falling edge of the clock depending on the clock polarity (CPOL) and clock phase (CPHA) settings. This ensures proper timing alignment between communicating devices.
Data Exchange Mechanism
During each clock cycle, both devices simultaneously transmit and receive one bit of data. The master selects a specific slave using the CS signal before initiating communication. All received data must be read promptly to prevent loss during subsequent transfers.
SPI Signal Definitions
- SCLK: Clock signal generated by the master
- MOSI: Master output, slave input - data transmission from master to slave
- MISO: Master input, slave output - data trensmission from slave to master
- CS: Chip select - enables communication with a specific slave device
Clock Configuration Parameters
Clock polarity (CPOL) defines the idle state of the clock:
- CPOL = 0: Clock idles low
- CPOL = 1: Clock idles high
Clock phase (CPHA) determines when data is sampled:
- CPHA = 0: Data sampled on the first clock edge
- CPHA = 1: Data sampled on the second clock edge
Hardware Implementation Example
The following Verilog module implements an SPI interface capable of handling 24-bit transactions (16-bit command + 8-bit data):
module spi (
input reset,
input cs,
input sclk,
input sdi,
output reg sdo,
output reg wr_rd,
output reg [14:0] addr,
input [7:0] rdata,
output reg [7:0] wdata
);
reg [2:0] cnt_bit;
always @(posedge sclk or negedge reset) begin
if (!reset)
cnt_bit <= 3'b0;
else if (!cs)
cnt_bit <= cnt_bit + 3'b1;
else
cnt_bit <= 3'b0;
end
wire cnt_full = (cnt_bit == 3'd7);
parameter IDLE = 2'd0;
parameter INSTR_H = 2'd1;
parameter INSTR_L = 2'd2;
parameter DATA = 2'd3;
reg [1:0] current_state, next_state;
always @(posedge sclk or negedge reset) begin
if (!reset)
current_state <= IDLE;
else if (cs)
current_state <= IDLE;
else
current_state <= next_state;
end
always @(*) begin
case (current_state)
IDLE: begin
if (cs)
next_state = IDLE;
else
next_state = INSTR_H;
end
INSTR_H: begin
if (cnt_full)
next_state = INSTR_L;
else
next_state = INSTR_H;
end
INSTR_L: begin
if (cnt_full)
next_state = DATA;
else
next_state = INSTR_L;
end
DATA: begin
if (cs)
next_state = IDLE;
else
next_state = DATA;
end
default: next_state = IDLE;
endcase
end
reg [15:0] shift_reg_din;
always @(posedge sclk or negedge reset) begin
if (!reset)
shift_reg_din <= 15'd0;
else if (cs)
shift_reg_din <= 15'd0;
else
shift_reg_din <= {shift_reg_din[14:0], sdi};
end
wire cstate = (current_state == INSTR_L);
reg cnt_full_d, cstate_d;
always @(posedge sclk or negedge reset) begin
if (!reset) begin
cnt_full_d <= 0;
cstate_d <= 0;
end else begin
cnt_full_d <= cnt_full;
cstate_d <= cstate;
end
end
always @(negedge sclk or negedge reset) begin
if (!reset)
addr <= 15'd0;
else if (cnt_full_d && cstate_d)
addr <= shift_reg_din[14:0];
else
addr <= addr;
end
always @(negedge sclk or negedge reset) begin
if (!reset)
wr_rd <= 1'b0;
else if (cnt_full_d && cstate_d)
wr_rd <= shift_reg_din[15];
end
always @(posedge cs or negedge reset) begin
if (!reset)
wdata <= 8'd0;
else
wdata <= shift_reg_din[7:0];
end
reg [7:0] shift_reg_dout;
always @(negedge sclk or negedge reset) begin
if (!reset)
shift_reg_dout <= 8'b0;
else if ((cnt_bit == 3'd0) && (current_state == DATA))
shift_reg_dout <= rdata;
else
shift_reg_dout <= {shift_reg_dout[6:0], 1'b0};
end
assign sdo = ((!wr_rd) && (!cs)) ? shift_reg_dout[7] : 1'b0;
endmodule
Testbench Implementation
The testbench simulates both read and write operations:
module spi_tb();
reg reset;
reg cs;
reg sclk;
reg sdi;
wiresdo;
reg [7:0] rdata;
wires wr_rd;
wires [14:0] addr;
wires [7:0] wdata;
spi u0(reset, cs, sclk, sdi, sdo, wr_rd, addr, rdata, wdata);
always #5 sclk = ~sclk;
initial begin
sclk = 0;
reset = 0;
cs = 1;
sdi = 0;
rdata = 8'b00001111;
#27 reset = 1;
#14 cs = 0;
sdi = 1; // Write operation
repeat(1) @(negedge sclk);
sdi = 0;
repeat(11) @(negedge sclk);
sdi = 1;
repeat(1) @(negedge sclk);
sdi = 0;
repeat(1) @(negedge sclk);
sdi = 1;
repeat(1) @(negedge sclk);
sdi = 0;
repeat(2) @(negedge sclk);
sdi = 1;
repeat(1) @(negedge sclk);
sdi = 0;
repeat(1) @(negedge sclk);
sdi = 1;
repeat(1) @(negedge sclk);
sdi = 0;
repeat(1) @(negedge sclk);
sdi = 1;
repeat(1) @(negedge sclk);
sdi = 0;
repeat(1) @(negedge sclk);
sdi = 1;
#11 cs = 1;
repeat(5) @(negedge sclk);
cs = 0;
sdi = 0; // Read operation
repeat(1) @(negedge sclk);
sdi = 0;
repeat(11) @(negedge sclk);
sdi = 1;
repeat(1) @(negedge sclk);
sdi = 0;
repeat(1) @(negedge sclk);
sdi = 1;
repeat(1) @(negedge sclk);
sdi = 0;
repeat(1) @(negedge sclk);
sdi = 1'bz;
repeat(7) @(negedge sclk);
#11 cs = 1;
#200 $stop;
end
endmodule
The simulation demonstrates successful execution of both read and write operations through proper state machine control and bit-level data handling.