SPI (Serial Peripheral Interface)
概述 串行外设接口,由摩托罗拉提出,是一种同步、全双工、主从模式的串行通信协议。通常支持一个主设备(Master)和多个从设备(Slaves)。因其高速、简单的特性,广泛用于与闪存(Flash)、ADC/DAC、传感器等外设的通信。
物理层与信号线
4根线 (典型):
`SCLK` (Serial Clock): 时钟信号,由主设备产生,驱动整个通信过程。
`MOSI` (Master Out Slave In): 主设备输出、从设备输入的数据线。
`MISO` (Master In Slave Out): 主设备输入、从设备输出的数据线。
`CS` / `SS` (Chip Select / Slave Select): 片选信号,由主设备控制,用于选择与之通信的从设备。通常低电平有效。
工作原理
SPI是同步的,所有数据传输都由SCLK的边沿触发。主从设备内部都有一个移位寄存器。在SCLK的驱动下,主设备的数据通过MOSI线移入从设备的寄存器,同时从设备的数据通过MISO线移入主设备的寄存器,实现数据的同时交换。
通信协议/传输步骤
1. 片选: 主设备将目标从设备的CS线拉低,选中该从设备。
2. 时钟生成: 主设备开始在SCLK线上产生时钟脉冲。
3. 数据交换: 在SCLK的某个边沿(由CPOL/CPHA模式决定),主设备将要发送的数据位放在MOSI线上。在同一个时钟边沿,从设备读取MOSI线上的数据位,并将其锁存到移位寄存器的最低位。同时,从设备将自己移位寄存器最高位的数据放在MISO线上,供主设备读取。
4. 重复: 重复步骤3,直到一个字节(或一个字)的数据交换完成。
5. 结束: 主设备停止SCLK,并将CS线拉高,结束本次通信。
SPI模式 (CPOL/CPHA): 这是SPI的一个关键且易错点。
`CPOL` (Clock Polarity): 定义SCLK空闲时的电平状态 (0: 低电平, 1: 高电平)。
`CPHA` (Clock Phase): 定义数据采样的时钟边沿 (0: 第一个边沿采样, 1: 第二个边沿采样)。这两种组合构成了4种SPI工作模式,主从设备必须工作在相同的模式下。
| CPOL | CPHA |
| 模式0:CPOL=0,CPHA =0 | SCK空闲为高电平,数据在SCK的上升沿被采样(提取数据) |
| 模式1:CPOL=0,CPHA =1 | SCK空闲为低电平,数据在SCK的下降沿被采样(提取数据) |
| 模式2:CPOL=1,CPHA =0 | SCK空闲为高电平,数据在SCK的下降沿被采样(提取数据) |
| 模式3:CPOL=1,CPHA =1 | SCK空闲为高电平,数据在SCK的上升沿被采样(提取数据) |
系统时钟是50MHz,SPI时钟是20MHz,模式1。
第一部分:verilog代码
1. `spi_master.v`
`timescale 1ns / 1ps
// --- MODIFIED: spi_master.v ---
module spi_master #(
parameter SYS_CLK_FREQ = 50_000_000,
// MODIFIED: Default SPI frequency updated
parameter SPI_CLK_FREQ = 20_000_000
)(
input wire i_clk,
input wire i_rst_n,
// ... (ports remain the same) ...
input wire i_tx_dv,
input wire [7:0] i_tx_byte,
output wire o_tx_busy,
output reg o_rx_dv,
output reg [7:0] o_rx_byte,
output reg o_cs_n,
output wire o_sclk,
output reg o_mosi,
input wire i_miso
);
localparam DIVIDER_HI = 2; // High for 2 sys_clk cycles
localparam DIVIDER_LO = 3; // Low for 3 sys_clk cycles
localparam DIVIDER_TOTAL = DIVIDER_HI + DIVIDER_LO;
// FSM states
localparam [1:0] S_IDLE = 2'b00;
localparam [1:0] S_TRANSFER= 2'b01;
localparam [1:0] S_DONE = 2'b10;
reg [1:0] r_state = S_IDLE;
// Counters
reg [$clog2(DIVIDER_TOTAL)-1:0] r_clk_cnt = 0;
reg [3:0] r_bit_cnt = 0;
// Shift registers
reg [7:0] r_tx_shift_reg;
reg [7:0] r_rx_shift_reg;
// Internal SCLK generation
reg r_sclk = 0;
assign o_sclk = r_sclk;
assign o_tx_busy = (r_state != S_IDLE);
// MODIFIED: SCLK generation block for 20MHz from 50MHz
always @(posedge i_clk or negedge i_rst_n) begin
if (!i_rst_n) begin
r_clk_cnt <= 0;
r_sclk <= 0; // CPOL=0, idle low
end else begin
if (r_state == S_TRANSFER) begin
if (r_sclk == 0) begin // If SCLK is currently low
if (r_clk_cnt == DIVIDER_LO - 1) begin
r_clk_cnt <= 0;
r_sclk <= 1'b1; // Go high
end else begin
r_clk_cnt <= r_clk_cnt + 1;
end
end else begin // If SCLK is currently high
if (r_clk_cnt == DIVIDER_HI - 1) begin
r_clk_cnt <= 0;
r_sclk <= 1'b0; // Go low
end else begin
r_clk_cnt <= r_clk_cnt + 1;
end
end
end else begin
r_clk_cnt <= 0;
r_sclk <= 0; // SCLK is low when idle (CPOL=0)
end
end
end
// MODIFIED: Main FSM logic for MODE 1
always @(posedge i_clk or negedge i_rst_n) begin
if (!i_rst_n) begin
r_state <= S_IDLE;
o_cs_n <= 1'b1;
o_mosi <= 1'b0;
o_rx_byte <= 8'b0;
o_rx_dv <= 1'b0;
r_bit_cnt <= 0;
r_tx_shift_reg <= 8'b0;
r_rx_shift_reg <= 8'b0;
end else begin
o_rx_dv <= 1'b0;
case (r_state)
S_IDLE: begin
o_cs_n <= 1'b1;
if (i_tx_dv) begin
r_tx_shift_reg <= i_tx_byte;
r_bit_cnt <= 0;
o_cs_n <= 1'b0;
r_state <= S_TRANSFER;
end
end
S_TRANSFER: begin
// MODIFIED: For CPHA=1, data is changed on first edge (rising)
// and sampled on second edge (falling).
// This logic detects the moment just before the rising edge to change MOSI.
if (r_sclk == 0 && r_clk_cnt == DIVIDER_LO - 1) begin
o_mosi <= r_tx_shift_reg[7];
r_tx_shift_reg <= r_tx_shift_reg << 1;
end
// This logic detects the moment just before the falling edge to sample MISO.
if (r_sclk == 1 && r_clk_cnt == DIVIDER_HI - 1) begin
r_rx_shift_reg <= {r_rx_shift_reg[6:0], i_miso};
if (r_bit_cnt == 7) begin
r_state <= S_DONE;
end else begin
r_bit_cnt <= r_bit_cnt + 1;
end
end
end
S_DONE: begin
o_cs_n <= 1'b1;
o_rx_byte <= r_rx_shift_reg;
o_rx_dv <= 1'b1;
r_state <= S_IDLE;
end
default: r_state <= S_IDLE;
endcase
end
end
endmodule1. `spi_slave.v`
`timescale 1ns / 1ps
// --- MODIFIED: spi_slave.v ---
module spi_slave(
input wire i_sclk,
input wire i_cs_n,
input wire i_mosi,
output reg o_miso
);
reg [7:0] r_tx_shift_reg;
reg [7:0] r_rx_shift_reg;
reg [7:0] r_internal_data;
// MODIFIED: For MODE 1 (CPHA=1), data is sampled on the falling edge.
always @(negedge i_sclk or negedge i_cs_n) begin
if (!i_cs_n) begin // Chip is selected
// Shift in data from master
r_rx_shift_reg <= {r_rx_shift_reg[6:0], i_mosi};
// Shift out data to master
o_miso <= r_tx_shift_reg[7];
r_tx_shift_reg <= r_tx_shift_reg << 1;
end else begin // Chip is deselected
o_miso <= 1'bz; // High impedance
end
end
// Latch logic remains the same, triggered by CS
always @(posedge i_cs_n) begin
r_internal_data <= r_rx_shift_reg;
r_tx_shift_reg <= r_rx_shift_reg + 1;
end
initial begin
r_internal_data = 8'h00;
r_tx_shift_reg = 8'h01;
o_miso = 1'bz;
end
endmodule3. `spi_top.v`
`timescale 1ns / 1ps module spi_top( input wire i_clk, input wire i_rst_n, input wire i_start_transfer, input wire [7:0] i_data_to_send, output wire o_transfer_busy, output wire o_data_received_valid, output wire [7:0] o_data_received ); wire w_cs_n; wire w_sclk; wire w_mosi; wire w_miso; // The master is instantiated with the default 20MHz parameter now spi_master u_spi_master ( .i_clk(i_clk), .i_rst_n(i_rst_n), .i_tx_dv(i_start_transfer), .i_tx_byte(i_data_to_send), .o_tx_busy(o_transfer_busy), .o_rx_dv(o_data_received_valid), .o_rx_byte(o_data_received), .o_cs_n(w_cs_n), .o_sclk(w_sclk), .o_mosi(w_mosi), .i_miso(w_miso) ); spi_slave u_spi_slave ( .i_sclk(w_sclk), .i_cs_n(w_cs_n), .i_mosi(w_mosi), .o_miso(w_miso) ); endmodule
4. `tb_spi_top.v`
`timescale 1ns / 1ps
module tb_spi_top;
localparam CLK_FREQ = 50_000_000;
localparam CLK_PERIOD = 20;
reg r_clk;
reg r_rst_n;
reg r_start_transfer;
reg [7:0] r_data_to_send;
wire w_transfer_busy;
wire w_data_received_valid;
wire [7:0] w_data_received;
spi_top u_dut (
.i_clk(r_clk),
.i_rst_n(r_rst_n),
.i_start_transfer(r_start_transfer),
.i_data_to_send(r_data_to_send),
.o_transfer_busy(w_transfer_busy),
.o_data_received_valid(w_data_received_valid),
.o_data_received(w_data_received)
);
initial begin
r_clk = 0;
forever #(CLK_PERIOD / 2) r_clk = ~r_clk;
end
initial begin
r_rst_n = 0;
r_start_transfer = 0;
r_data_to_send = 8'h00;
#200;
r_rst_n = 1;
end
initial begin
@(posedge r_rst_n);
#1000;
r_data_to_send <= 8'hA5;
r_start_transfer <= 1;
@(posedge r_clk);
r_start_transfer <= 0;
@(posedge w_transfer_busy);
@(negedge w_transfer_busy);
#1000;
r_data_to_send <= 8'h00;
r_start_transfer <= 1;
@(posedge r_clk);
r_start_transfer <= 0;
@(posedge w_data_received_valid);
if (w_data_received == 8'hA6) begin
$display("TB PASS: Test Case 2 Passed. Received expected response: 0x%0h", w_data_received);
end else begin
$display("TB FAIL: Test Case 2 Failed. Expected 0xA6, but received 0x%0h", w_data_received);
end
$finish;
end
endmodule
