FPGA新手必看:手把手教你用Verilog实现SPI主从通信(附完整代码与仿真波形)
FPGA实战:从零构建SPI主从通信系统的Verilog实现指南
第一次接触SPI协议时,看着那些跳动的波形和晦涩的时序图,我完全摸不着头脑。直到亲手用Verilog实现了一个完整的SPI通信系统,才真正理解了"为什么数据要在下降沿切换,上升沿采样"。本文将带你完整走一遍这个学习历程,从SPI模式0的时序分析到可综合的Verilog代码实现,最后通过ModelSim仿真验证通信的正确性。
1. SPI协议核心要点解析
SPI(Serial Peripheral Interface)作为一种同步串行通信协议,在FPGA与外围设备交互中扮演着重要角色。与I2C不同,SPI采用全双工通信方式,通过四根信号线实现高速数据传输:
- SCK(Serial Clock):主设备产生的时钟信号
- MOSI(Master Out Slave In):主设备输出,从设备输入
- MISO(Master In Slave Out):从设备输出,主设备输入
- CS(Chip Select):从设备使能信号(低电平有效)
SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)组合决定。我们重点分析模式0(CPOL=0,CPHA=0)的时序特性:
| 时序特征 | 描述 |
|---|---|
| 空闲时钟电平 | SCK空闲时为低电平 |
| 数据采样边沿 | 在SCK上升沿采样数据 |
| 数据切换边沿 | 在SCK下降沿切换数据 |
| 数据传输顺序 | 高位(MSB)先发送 |
这种时序安排确保了数据在采样边沿(上升沿)到来时已经稳定,这是SPI可靠通信的关键。想象一下,如果在上升沿同时切换和采样数据,很可能会采样到正在变化的不稳定状态。
2. SPI主设备Verilog实现详解
让我们从主设备设计开始,逐步构建一个可发送8位数据的SPI主机。以下是完整的模块定义:
module SPI_Master ( input wire clk, // 系统时钟(50MHz) input wire reset_n, // 异步复位(低有效) input wire [7:0] tx_data, // 待发送数据 input wire start, // 发送启动信号 output reg sck, // SPI时钟 output reg cs, // 片选信号 output reg mosi, // 主出从入数据线 output reg busy // 发送忙标志 ); // 状态定义 typedef enum { IDLE, ASSERT_CS, SEND_BIT, DEASSERT_CS } state_t; reg [2:0] bit_counter; // 位计数器(0-7) reg [7:0] shift_reg; // 移位寄存器 state_t current_state;2.1 状态机设计
SPI主设备的核心是一个精确控制时序的状态机。我们采用三段式状态机实现:
always @(posedge clk or negedge reset_n) begin if (!reset_n) begin current_state <= IDLE; sck <= 1'b0; cs <= 1'b1; mosi <= 1'b0; busy <= 1'b0; end else begin case (current_state) IDLE: begin if (start) begin shift_reg <= tx_data; bit_counter <= 3'd7; // 从最高位开始发送 busy <= 1'b1; current_state <= ASSERT_CS; end end ASSERT_CS: begin cs <= 1'b0; sck <= 1'b0; // 确保第一个下降沿有效 current_state <= SEND_BIT; end SEND_BIT: begin // 下降沿切换数据 mosi <= shift_reg[bit_counter]; sck <= 1'b1; // 准备上升沿 if (bit_counter == 0) begin current_state <= DEASSERT_CS; end else begin bit_counter <= bit_counter - 1; end end DEASSERT_CS: begin cs <= 1'b1; busy <= 1'b0; current_state <= IDLE; end endcase end end关键点:状态机在每个系统时钟周期只改变SCK或数据线一次,确保信号稳定
2.2 时钟生成与数据同步
SPI时钟(SCK)由系统时钟分频得到。对于50MHz系统时钟和1MHz SPI时钟,我们需要50分频:
reg [5:0] clk_divider; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin clk_divider <= 6'd0; end else if (current_state == SEND_BIT) begin clk_divider <= (clk_divider == 6'd49) ? 6'd0 : clk_divider + 1; sck <= (clk_divider < 25) ? 1'b1 : 1'b0; end else begin sck <= 1'b0; end end这种实现方式既保证了SCK的精确50%占空比,又能灵活调整SPI时钟频率。
3. SPI从设备设计与实现
从设备的设计关键在于正确响应主设备的时钟信号。以下是完整的从设备模块:
module SPI_Slave ( input wire clk, // 系统时钟(用于内部处理) input wire reset_n, // 异步复位 input wire sck, // SPI时钟(来自主设备) input wire cs, // 片选信号 input wire mosi, // 主出从入数据线 output reg [7:0] rx_data, // 接收到的数据 output reg data_valid // 数据有效标志 ); reg [7:0] shift_reg; reg sck_prev; reg [2:0] bit_counter; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin rx_data <= 8'h00; shift_reg <= 8'h00; data_valid <= 1'b0; bit_counter <= 3'd7; sck_prev <= 1'b0; end else begin sck_prev <= sck; if (cs) begin // 片选无效 bit_counter <= 3'd7; data_valid <= 1'b0; end else begin // 检测SCK上升沿 if (!sck_prev && sck) begin shift_reg[bit_counter] <= mosi; if (bit_counter == 0) begin rx_data <= shift_reg; data_valid <= 1'b1; bit_counter <= 3'd7; end else begin bit_counter <= bit_counter - 1; data_valid <= 1'b0; end end end end end endmodule3.1 从设备同步机制
从设备需要特别注意时钟域的同步问题。我们采用双触发器同步技术处理跨时钟域信号:
reg sck_sync1, sck_sync2; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin sck_sync1 <= 1'b0; sck_sync2 <= 1'b0; end else begin sck_sync1 <= sck; sck_sync2 <= sck_sync1; end end这种同步方式有效避免了亚稳态问题,确保在系统时钟域内可靠检测SCK的边沿变化。
4. 仿真验证与波形分析
完整的验证环境包括测试平台(testbench)和波形分析。以下是使用ModelSim进行仿真的关键步骤:
4.1 测试平台设计
`timescale 1ns/1ps module SPI_TB; reg clk; reg reset_n; reg [7:0] master_tx_data; reg start; wire sck, cs, mosi; wire [7:0] slave_rx_data; wire data_valid; // 实例化主设备 SPI_Master u_master ( .clk(clk), .reset_n(reset_n), .tx_data(master_tx_data), .start(start), .sck(sck), .cs(cs), .mosi(mosi), .busy() ); // 实例化从设备 SPI_Slave u_slave ( .clk(clk), .reset_n(reset_n), .sck(sck), .cs(cs), .mosi(mosi), .rx_data(slave_rx_data), .data_valid(data_valid) ); // 时钟生成 initial begin clk = 0; forever #10 clk = ~clk; // 50MHz时钟 end // 测试流程 initial begin reset_n = 0; master_tx_data = 8'hA5; start = 0; #100 reset_n = 1; #50 start = 1; #20 start = 0; wait(data_valid); #200; if (slave_rx_data === master_tx_data) $display("Test PASSED: Received 0x%h", slave_rx_data); else $display("Test FAILED: Expected 0x%h, got 0x%h", master_tx_data, slave_rx_data); $finish; end endmodule4.2 关键波形解读
仿真波形中需要特别关注以下几个关键点:
- CS信号有效期间:只有当CS为低电平时,从设备才会响应SPI通信
- SCK与MOSI的时序关系:
- MOSI数据在SCK下降沿后立即变化
- MOSI数据在SCK上升沿前必须保持稳定
- 数据有效性:从设备的data_valid信号应在完整接收8位数据后拉高一个时钟周期
4.3 常见问题排查
初学者在SPI实现中常遇到以下问题:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 从设备接收数据全为0 | CS信号未正确连接 | 检查CS信号连接和极性 |
| 接收数据位序错误 | 位计数器方向错误 | 确认从最高位(MSB)开始发送 |
| 数据采样不稳定 | 未满足建立保持时间 | 调整数据相对SCK边沿的时序 |
| 通信完全失败 | SPI模式配置不一致 | 确认主从设备CPOL/CPHA设置相同 |
5. 实际应用与优化建议
在真实项目中实现SPI通信时,还需要考虑以下进阶问题:
5.1 多从设备连接
SPI总线可以连接多个从设备,通过不同的CS信号选择:
+-------+ +-------+ | 从设备1| | 从设备2| +-------+ +-------+ MOSI -----| DI | | DI | MISO -----| DO | | DO | SCK -----| SCK | | SCK | CS1 ----| /CS | | | CS2 ----| | | /CS | +-------+ +-------+5.2 时钟极性与相位配置
通过参数化设计支持所有四种SPI模式:
module SPI_Master #( parameter CPOL = 0, parameter CPHA = 0 ) ( // 端口定义... ); // 根据CPOL设置空闲时钟电平 assign sck_idle = CPOL ? 1'b1 : 1'b0; // 根据CPHA选择数据采样边沿 always @(posedge clk) begin if (CPHA == 0) begin // 在第一个边沿采样数据 end else begin // 在第二个边沿采样数据 end end5.3 性能优化技巧
- 时钟分频寄存器优化:使用格雷码计数器减少时钟切换时的毛刺
- 跨时钟域处理:对异步信号使用双寄存器同步
- 时序约束:在SDC文件中添加适当的时序约束
- IO缓冲:在FPGA管脚处添加IO缓冲器提高信号质量
// 示例:使用格雷码计数器 reg [5:0] clk_divider_gray; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin clk_divider_gray <= 6'd0; end else begin clk_divider_gray <= (clk_divider_gray + 1) ^ ((clk_divider_gray + 1) >> 1); end end在完成第一个SPI项目后,我强烈建议尝试以下扩展练习:
- 实现双向数据传输(同时使用MOSI和MISO)
- 添加DMA支持实现大数据块传输
- 设计支持可变数据长度的SPI控制器
- 集成错误检测机制(如CRC校验)
