An SPI controller helps a chip talk to other devices using the SPI communication method. It controls sending and receiving data bit by bit.
SPI controller implementation in Verilog
module spi_controller(
input wire clk,
input wire rst_n,
input wire start,
input wire [7:0] data_in,
output reg mosi,
output reg sclk,
output reg cs_n,
output reg [7:0] data_out,
output reg done
);
// Internal registers and states
reg [2:0] bit_cnt;
reg [7:0] shift_reg;
reg [1:0] state;
localparam IDLE = 2'b00, TRANSFER = 2'b01, DONE = 2'b10;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
cs_n <= 1'b1;
sclk <= 1'b0;
mosi <= 1'b0;
bit_cnt <= 3'd7;
done <= 1'b0;
data_out <= 8'd0;
shift_reg <= 8'd0;
end else begin
case(state)
IDLE: begin
done <= 1'b0;
sclk <= 1'b0;
cs_n <= 1'b1;
if (start) begin
cs_n <= 1'b0; // Activate chip select
shift_reg <= data_in;
bit_cnt <= 3'd7;
state <= TRANSFER;
end
end
TRANSFER: begin
sclk <= ~sclk; // Toggle clock
if (sclk == 1'b0) begin
// On falling edge, output next bit
mosi <= shift_reg[bit_cnt];
end else begin
// On rising edge, sample MISO (not implemented here, assume 0)
// shift_reg[bit_cnt] <= miso; // If MISO input existed
if (bit_cnt == 0) begin
state <= DONE;
end else begin
bit_cnt <= bit_cnt - 1;
end
end
end
DONE: begin
cs_n <= 1'b1; // Deactivate chip select
done <= 1'b1;
state <= IDLE;
end
endcase
end
end
endmoduleThis module uses a simple state machine with IDLE, TRANSFER, and DONE states.
The SPI clock (sclk) toggles every clock cycle to send bits out on MOSI.
spi_controller spi(
.clk(clk),
.rst_n(rst_n),
.start(start),
.data_in(8'hA5),
.mosi(mosi),
.sclk(sclk),
.cs_n(cs_n),
.data_out(data_out),
.done(done)
);always @(posedge clk) begin
if (done) begin
// SPI transfer finished
end
endThis testbench resets the SPI controller, starts a transfer of 0x3C, and waits until done is high. It then prints the last MOSI bit sent.
module test_spi; reg clk = 0; reg rst_n = 0; reg start = 0; reg [7:0] data_in = 8'h3C; wire mosi; wire sclk; wire cs_n; wire done; wire [7:0] data_out; spi_controller spi( .clk(clk), .rst_n(rst_n), .start(start), .data_in(data_in), .mosi(mosi), .sclk(sclk), .cs_n(cs_n), .data_out(data_out), .done(done) ); // Clock generation always #5 clk = ~clk; initial begin rst_n = 0; #10; rst_n = 1; #10; start = 1; #10; start = 0; // Wait for done wait(done == 1); $display("SPI transfer done. MOSI last bit: %b", mosi); $finish; end endmodule
The SPI controller here only sends data (MOSI) and does not read MISO data.
Chip select (cs_n) is active low and controls when the SPI device listens.
The clock toggles every main clock cycle, so the SPI clock is half the frequency of clk.
An SPI controller manages sending data bit by bit using MOSI, SCLK, and CS signals.
It uses a simple state machine to control the transfer process.
This example shows how to implement and test a basic SPI controller in Verilog.
Practice
cs (chip select) signal in an SPI controller?Solution
Step 1: Understand the function of
Thecsin SPIcssignal is used to select which SPI device the master wants to communicate with by enabling it.Step 2: Differentiate
The clock signal iscsfrom other signalssclk, data sent from master ismosi, and data received ismiso. Socsis not for clock or data transfer but device selection.Final Answer:
To enable communication with the specific SPI device -> Option AQuick Check:
cs= device enable [OK]
- Confusing cs with clock signal
- Thinking cs sends or receives data
- Mixing up mosi and miso signals
shift_reg?Solution
Step 1: Recall Verilog register declaration syntax
Registers are declared withreg [width-1:0] name;format. For 8 bits, it is[7:0].Step 2: Check each option
reg [7:0] shift_reg; uses correct syntax. reg shift_reg[7:0]; reverses the range and name. wire [7:0] shift_reg; uses wire, not reg. reg shift_reg:8; uses invalid colon syntax.Final Answer:
reg [7:0] shift_reg; -> Option DQuick Check:
reg [7:0] name; = correct reg declaration [OK]
- Using wire instead of reg for storage
- Incorrect bit range syntax
- Misplacing brackets around name
always @(posedge sclk) begin
if (cs == 0) begin
shift_reg <= {shift_reg[6:0], miso};
end
endWhat does this code do on each rising edge of
sclk when cs is low?Solution
Step 1: Analyze the shift operation
The code concatenatesshift_reg[6:0](bits 6 down to 0) withmisoat the right side, effectively shifting left by 1 bit and insertingmisoat the least significant bit (LSB).Step 2: Confirm behavior when
The shift happens only whencsis lowcs == 0, which means the device is selected and data is being received bit by bit.Final Answer:
Shifts shift_reg left by 1 and inserts miso at LSB -> Option CQuick Check:
shift_reg <= {shift_reg[6:0], miso} = left shift + miso LSB [OK]
- Thinking it shifts right instead of left
- Confusing MSB and LSB positions
- Ignoring the condition on cs signal
always @(posedge clk) begin
if (cs == 0) begin
sclk <= ~sclk;
end else begin
sclk <= 0;
end
endWhat is the main problem with this code?
Solution
Step 1: Understand clock toggling logic
The code togglessclkon every rising edge ofclkifcsis low, effectively dividing the clock frequency by 2 during active communication.Step 2: Identify the problem
This behavior is expected for SPI clock generation. The code also resetssclkto 0 whencsis high, which is correct. There is no asynchronous toggling or sensitivity list issue.Final Answer:
It toggles sclk on every clk edge, causing sclk frequency to be half of clk only when cs is low -> Option AQuick Check:
sclk toggles at half clk frequency when cs=0 [OK]
- Thinking sclk toggles asynchronously
- Expecting sclk to run continuously
- Confusing sensitivity list with always block usage
tx_data over mosi and receives 8 bits into rx_data from miso. Which approach correctly handles the bit counting and shifting in Verilog?Solution
Step 1: Determine counter size and control
For 8 bits, a 3-bit counter (0 to 7) is enough. It should increment onsclkrising edge only whencsis low (active communication).Step 2: Understand shifting direction and data lines
SPI usually sends MSB first, so shiftingtx_dataleft outputs MSB onmosi. Incoming bits frommisoare shifted intorx_data.Step 3: Evaluate options
Use a 3-bit counter that increments on eachsclkrising edge whencsis low, shifttx_dataleft to output MSB onmosi, and shift inmisointorx_data. matches correct counter size, control, and shifting direction. Use a 4-bit counter that increments on eachclkrising edge regardless ofcs, shifttx_dataright to output LSB onmosi, and ignoremiso. uses wrong counter size and ignoresmiso. Use an 8-bit counter that decrements on eachsclkfalling edge whencsis high, shifttx_dataright to output MSB onmosi, and shift inmisointorx_data. uses wrong edge and counter behavior. Use a 3-bit counter that resets only whencsis high, shifttx_dataleft to output LSB onmosi, and shift inmisointorx_data. incorrectly outputs LSB first and counter reset logic.Final Answer:
Use a 3-bit counter that increments on each sclk rising edge when cs is low, shift tx_data left to output MSB on mosi, and shift in miso into rx_data. -> Option BQuick Check:
3-bit counter + sclk rising + MSB first shift = correct SPI send/receive [OK]
- Using wrong counter size or clock edge
- Shifting in wrong direction for MSB first
- Ignoring chip select signal during counting
Ready to prove it?
Pass the test and this pattern moves unknown
