FPGA新手必看:手把手教你用Verilog实现SPI主从通信(附完整代码与仿真波形)

张开发
2026/4/16 19:31:08 15 分钟阅读

分享文章

FPGA新手必看:手把手教你用Verilog实现SPI主从通信(附完整代码与仿真波形)
FPGA实战从零构建SPI主从通信系统的Verilog实现指南第一次接触SPI协议时看着那些跳动的波形和晦涩的时序图我完全摸不着头脑。直到亲手用Verilog实现了一个完整的SPI通信系统才真正理解了为什么数据要在下降沿切换上升沿采样。本文将带你完整走一遍这个学习历程从SPI模式0的时序分析到可综合的Verilog代码实现最后通过ModelSim仿真验证通信的正确性。1. SPI协议核心要点解析SPISerial Peripheral Interface作为一种同步串行通信协议在FPGA与外围设备交互中扮演着重要角色。与I2C不同SPI采用全双工通信方式通过四根信号线实现高速数据传输SCKSerial Clock主设备产生的时钟信号MOSIMaster Out Slave In主设备输出从设备输入MISOMaster In Slave Out从设备输出主设备输入CSChip Select从设备使能信号低电平有效SPI有四种工作模式由时钟极性CPOL和时钟相位CPHA组合决定。我们重点分析模式0CPOL0CPHA0的时序特性时序特征描述空闲时钟电平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 1b0; cs 1b1; mosi 1b0; busy 1b0; end else begin case (current_state) IDLE: begin if (start) begin shift_reg tx_data; bit_counter 3d7; // 从最高位开始发送 busy 1b1; current_state ASSERT_CS; end end ASSERT_CS: begin cs 1b0; sck 1b0; // 确保第一个下降沿有效 current_state SEND_BIT; end SEND_BIT: begin // 下降沿切换数据 mosi shift_reg[bit_counter]; sck 1b1; // 准备上升沿 if (bit_counter 0) begin current_state DEASSERT_CS; end else begin bit_counter bit_counter - 1; end end DEASSERT_CS: begin cs 1b1; busy 1b0; 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 6d0; end else if (current_state SEND_BIT) begin clk_divider (clk_divider 6d49) ? 6d0 : clk_divider 1; sck (clk_divider 25) ? 1b1 : 1b0; end else begin sck 1b0; 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 8h00; shift_reg 8h00; data_valid 1b0; bit_counter 3d7; sck_prev 1b0; end else begin sck_prev sck; if (cs) begin // 片选无效 bit_counter 3d7; data_valid 1b0; 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 1b1; bit_counter 3d7; end else begin bit_counter bit_counter - 1; data_valid 1b0; 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 1b0; sck_sync2 1b0; 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 8hA5; 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实现中常遇到以下问题问题现象可能原因解决方案从设备接收数据全为0CS信号未正确连接检查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 ? 1b1 : 1b0; // 根据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 6d0; end else begin clk_divider_gray (clk_divider_gray 1) ^ ((clk_divider_gray 1) 1); end end在完成第一个SPI项目后我强烈建议尝试以下扩展练习实现双向数据传输同时使用MOSI和MISO添加DMA支持实现大数据块传输设计支持可变数据长度的SPI控制器集成错误检测机制如CRC校验

更多文章