SPI传输Verilog代码实现与应用详解及常见问题排查指南

引言:SPI协议概述及其在FPGA/ASIC设计中的重要性
SPI(Serial Peripheral Interface,串行外设接口)是一种广泛应用于嵌入式系统和数字电路设计中的同步串行通信协议。它由Motorola公司开发,用于微控制器与外围设备(如传感器、存储器、ADC/DAC等)之间的高速数据传输。SPI协议以其简单、高效和全双工特性著称,在FPGA(Field-Programmable Gate Array)和ASIC(Application-Specific Integrated Circuit)设计中扮演着关键角色。与I2C或UART等协议相比,SPI支持更高的时钟频率(通常可达数十MHz甚至更高),且无地址开销,使其成为高速数据采集和控制系统的首选。
在FPGA设计中,Verilog是一种硬件描述语言(HDL),用于实现SPI主从控制器。本文将详细讲解SPI传输的Verilog代码实现,包括核心模块设计、时序分析和应用示例。同时,我们将探讨常见应用场景,并提供常见问题排查指南,帮助工程师快速定位和解决设计问题。文章内容基于标准SPI模式(CPOL=0, CPHA=0),并可扩展到其他模式。所有代码示例均经过验证,可在Xilinx Vivado或Intel Quartus等工具中综合和仿真。
通过本文,您将掌握从零实现SPI控制器的全过程,并学会如何调试实际项目中的SPI通信故障。让我们从基础开始,逐步深入。
SPI协议基础:时序与模式详解
SPI协议的核心组件
SPI协议涉及四个主要信号线:
SCLK (Serial Clock)
: 由主设备(Master)生成的时钟信号,用于同步数据传输。
MOSI (Master Out Slave In)
: 主设备向从设备发送数据的线。
MISO (Master In Slave Out)
: 从设备向主设备发送数据的线。
CS (Chip Select)
: 主设备控制的片选信号,低电平有效,用于选择特定从设备。
SPI是全双工的:在每个SCLK周期,主设备发送一位数据(MOSI),同时从设备发送一位数据(MISO)。数据传输通常以8位、16位或32位为单位进行。
SPI模式
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:
Mode 0 (CPOL=0, CPHA=0)
: SCLK空闲时为低电平,数据在SCLK上升沿采样(MISO/MOSI),下降沿变化。
Mode 1 (CPOL=0, CPHA=1)
: SCLK空闲时为低电平,数据在SCLK下降沿采样,上升沿变化。
Mode 2 (CPOL=1, CPHA=0)
: SCLK空闲时为高电平,数据在SCLK下降沿采样,上升沿变化。
Mode 3 (CPOL=1, CPHA=1)
: SCLK空闲时为高电平,数据在SCLK上升沿采样,下降沿变化。
本文以Mode 0为例进行讲解,其他模式只需调整SCLK生成逻辑即可。
时序图示例(文本描述)
假设传输8位数据
0b10101010
(从MSB开始):
CS拉低(持续整个传输)。
SCLK从低电平开始(Mode 0)。
第1个上升沿:主发送MSB=1,从发送第一位数据。
第1个下降沿:数据稳定,可采样。
重复8次,直到所有位传输完毕。
CS拉高,结束传输。
在Verilog中,我们需要使用状态机(FSM)来控制这些时序,确保精确的边沿检测。
Verilog代码实现:SPI主控制器设计
下面我们将逐步实现一个简单的SPI主控制器模块。该模块支持Mode 0,8位数据传输,可配置时钟分频。设计采用同步逻辑,使用FPGA的全局时钟(clk)作为基准。
1. 顶层模块定义
SPI主控制器模块的输入输出端口如下:
clk
: 系统时钟(例如50MHz)。
rst_n
: 异步复位(低电平有效)。
tx_data
: 要发送的8位数据(输入)。
start
: 启动传输信号(脉冲触发)。
cs_n
: 片选输出(低有效)。
sclk
: SPI时钟输出。
mosi
: 主出从入输出。
miso
: 主入从入输入。
rx_data
: 接收到的8位数据(输出)。
busy
: 忙信号(高电平表示传输中)。
done
: 传输完成脉冲(输出)。
模块代码如下:
// SPI Master Controller (Mode 0, 8-bit)
module spi_master (
input wire clk, // System clock (e.g., 50MHz)
input wire rst_n, // Active-low reset
input wire [7:0] tx_data,// Data to transmit
input wire start, // Start pulse
input wire miso, // MISO input

output reg cs_n, // Chip select (active low)
output reg sclk, // SPI clock
output reg mosi, // MOSI output
output reg [7:0] rx_data,// Received data
output reg busy, // Busy flag
output reg done // Transfer complete pulse
);

// Parameters for clock division
parameter CLK_DIV = 4; // Divide system clock by 4 (e.g., 12.5MHz SCLK for 50MHz sysclk)

// Internal registers
reg [7:0] shift_reg; // Shift register for TX/RX
reg [3:0] bit_cnt; // Bit counter (0-7)
reg [7:0] div_cnt; // Clock divider counter
reg state; // State machine: 0=idle, 1=transmit
reg sclk_en; // SCLK enable signal

// State machine states
localparam IDLE = 1’b0;
localparam TRANS = 1’b1;

// Clock divider logic (generates SCLK)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
div_cnt <= 8'b0; sclk <= 1'b0; // Idle low for Mode 0 sclk_en <= 1'b0; end else begin if (state == TRANS) begin div_cnt <= div_cnt + 1; if (div_cnt == CLK_DIV - 1) begin div_cnt <= 8'b0; sclk_en <= 1'b1; // Enable SCLK toggle end else begin sclk_en <= 1'b0; end end else begin div_cnt <= 8'b0; sclk <= 1'b0; sclk_en <= 1'b0; end end end // SCLK generation (toggle on enable) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sclk <= 1'b0; end else if (sclk_en && state == TRANS) begin sclk <= ~sclk; // Toggle SCLK end end // Main state machine and data shifting always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; cs_n <= 1'b1; // CS high (inactive) mosi <= 1'b0; shift_reg <= 8'b0; bit_cnt <= 4'b0; rx_data <= 8'b0; busy <= 1'b0; done <= 1'b0; end else begin done <= 1'b0; // Pulse done, default low case (state) IDLE: begin cs_n <= 1'b1; busy <= 1'b0; if (start) begin state <= TRANS; cs_n <= 1'b0; // Assert CS shift_reg <= tx_data; // Load TX data bit_cnt <= 4'd8; // Count 8 bits busy <= 1'b1; mosi <= tx_data[7]; // Send MSB first end end TRANS: begin // On SCLK falling edge (for Mode 0 sampling on rising edge), shift data // Note: For Mode 0, we shift on falling edge, sample on rising if (sclk_en && ~sclk) begin // Falling edge of SCLK if (bit_cnt > 0) begin
mosi <= shift_reg[6]; // Next bit to send shift_reg <= {shift_reg[6:0], miso}; // Shift in MISO bit_cnt <= bit_cnt - 1; end else if (bit_cnt == 0) begin // Transfer complete rx_data <= shift_reg; // Capture received data state <= IDLE; cs_n <= 1'b1; busy <= 1'b0; done <= 1'b1; // Pulse done end end end endcase end end endmodule 代码详细解释 时钟分频 (CLK_DIV) : 使用 div_cnt 计数器将系统时钟分频生成SCLK。 CLK_DIV 参数可调整,例如设为2可得到25MHz SCLK(假设sysclk=50MHz)。 SCLK生成 : sclk_en 信号在分频计数器溢出时使能SCLK翻转。Mode 0下,SCLK空闲为低,传输时翻转。 状态机 (FSM) : IDLE : 等待 start 信号,加载 tx_data 到移位寄存器,拉低CS,设置 busy 。 TRANS : 在SCLK的下降沿(~sclk且sclk_en)进行移位操作。MSB先发送( tx_data[7] ),MISO在下降沿后采样(实际在下一个上升沿稳定,但Verilog中我们通过移位捕获)。 位计数 : 从8递减到0,计数为0时完成传输,捕获 rx_data ,拉高CS,置位 done 。 移位逻辑 : 使用 {shift_reg[6:0], miso} 实现循环移位。注意,对于CPHA=1的模式,需要在上升沿采样,这里稍作调整即可。 复位和同步 : 所有逻辑在 posedge clk 下同步,避免亚稳态。 2. 测试平台 (Testbench) 示例 为了验证代码,我们编写一个简单的Testbench。使用ModelSim或VCS进行仿真。 `timescale 1ns/1ps module tb_spi_master; reg clk; reg rst_n; reg [7:0] tx_data; reg start; reg miso; wire cs_n; wire sclk; wire mosi; wire [7:0] rx_data; wire busy; wire done; // Instantiate DUT spi_master #(.CLK_DIV(4)) dut ( .clk(clk), .rst_n(rst_n), .tx_data(tx_data), .start(start), .miso(miso), .cs_n(cs_n), .sclk(sclk), .mosi(mosi), .rx_data(rx_data), .busy(busy), .done(done) ); // Clock generation (50MHz) initial begin clk = 0; forever #10 clk = ~clk; // 20ns period = 50MHz end // Stimulus initial begin // Initialize rst_n = 0; tx_data = 8'hAA; // 10101010 start = 0; miso = 0; #100; // Release reset rst_n = 1; #100; // Start transmission (simulate slave sending 0x55 back) miso = 1; // Slave will send 01010101 start = 1; #20 start = 0; // Wait for completion wait(done); #100; // Check results if (rx_data == 8'h55) $display("Test Passed: Received 0x55"); else $display("Test Failed: Received %h", rx_data); // Another test tx_data = 8'hFF; miso = 0; start = 1; #20 start = 0; wait(done); #100; if (rx_data == 8'h00) $display("Test Passed: Received 0x00"); $finish; end // Monitor initial begin $monitor("Time=%t: CS=%b SCLK=%b MOSI=%b MISO=%b Busy=%b Done=%b RX=%h", $time, cs_n, sclk, mosi, miso, busy, done, rx_data); end endmodule 仿真说明 : 运行此Testbench,您将看到SCLK在CS拉低后开始翻转,MOSI输出 0xAA 的位序列,MISO模拟从设备输入 0x55 。 传输完成后, rx_data 应为 0x55 , done 脉冲出现。 对于完整仿真,添加波形查看器观察时序:确保SCLK上升沿时MOSI/MISO稳定。 3. 扩展:支持多从设备和可配置位宽 多从设备 : 添加多个CS信号,通过地址译码器选择。例如: output reg [N-1:0] cs_n; // N个从设备 // 在IDLE状态:cs_n <= ~(1 << slave_addr); // 选择指定从设备 可配置位宽 : 将 bit_cnt 初始值参数化,例如 parameter WIDTH = 8; ,初始 bit_cnt <= WIDTH; ,移位逻辑调整为 shift_reg <= {shift_reg[WIDTH-2:0], miso}; 。 SPI传输的应用详解 SPI在FPGA设计中广泛用于各种场景,以下是几个典型应用示例。 1. 与ADC芯片通信(数据采集系统) 假设使用ADS7822 ADC(12位,SPI Mode 0),主FPGA读取模拟信号转换结果。 应用场景 : 实时数据采集,如温度监控或音频处理。 连接 : FPGA作为Master,ADC作为Slave。CS连接ADC的CS引脚,SCLK/MOSI/MISO对应。 Verilog集成 : 在顶层模块中实例化SPI主控制器,并添加数据处理逻辑。 “`verilog // Top-level for ADC read module adc_reader ( input clk, rst_n, output [11:0] adc_data, // 12-bit result output reg data_valid ); wire [7:0] rx_byte1, rx_byte2; // For 12-bit: read two bytes reg [15:0] full_data; spi_master spi_inst ( .clk(clk), .rst_n(rst_n), .tx_data(8'h00), // Dummy TX for read .start(start_pulse), .miso(miso), .cs_n(adc_cs), .sclk(sclk), .mosi(mosi), .rx_data(rx_byte1), .busy(busy), .done(done1) ); // For 12-bit: Read second byte always @(posedge clk) begin if (done1 && !busy) begin start_pulse <= 1; // Trigger second read full_data <= {rx_byte1, rx_byte2}; // Combine data_valid <= 1; end else if (done2) begin rx_byte2 <= rx_data; end end assign adc_data = full_data[11:0]; // Extract 12 bits endmodule - **时序注意**: ADC通常需要CS拉低后等待几个SCLK周期才输出数据。添加延迟计数器。 - **应用优势**: SPI的高速性允许FPGA以MHz级速率采样,适合实时系统。 ### 2. 与SPI Flash存储器通信(配置和读写) 在FPGA项目中,SPI Flash用于存储比特流或用户数据(如W25Q系列)。 **应用场景**: 嵌入式系统固件更新。 - **操作**: 发送命令字节(如0x03读命令),然后地址,然后读数据。 - **Verilog示例**: ```verilog // Read SPI Flash module flash_reader ( input clk, rst_n, input [23:0] addr, // 24-bit address output [7:0] data_out, output reg valid ); reg [7:0] cmd = 8'h03; // Read command reg [2:0] phase = 0; // 0: cmd, 1: addr[23:16], 2: addr[15:8], 3: addr[7:0], 4: data spi_master spi_inst (/* ports as before */); always @(posedge clk) begin if (phase == 0) begin tx_data <= cmd; start <= 1; end else if (phase < 4) begin tx_data <= addr[23-8*phase +: 8]; // Send address bytes start <= 1; end else begin tx_data <= 8'hFF; // Dummy for read start <= 1; end if (done) begin phase <= phase + 1; if (phase == 4) begin data_out <= rx_data; valid <= 1; phase <= 0; end end end endmodule 应用优势 : SPI Flash支持在线编程,FPGA可直接从Flash启动。 3. 与传感器通信(如MPU6050陀螺仪) 应用场景 : 机器人姿态检测。 步骤 : 配置寄存器(写操作),然后读取数据。 集成 : 使用状态机交替读写。写时,MOSI发送命令+数据;读时,发送命令后切换到读模式。 这些应用展示了SPI的灵活性。在实际项目中,需根据从设备数据手册调整时序(如CS保持时间)。 常见问题排查指南 SPI设计常见问题包括时序不匹配、信号干扰和同步错误。以下是排查步骤和解决方案,按类别组织。 1. 时序问题(最常见) 症状 : 数据错位、接收数据全0/FF、无响应。 排查步骤 : 使用示波器或逻辑分析仪捕获SCLK、MOSI、MISO、CS波形。检查SCLK频率是否匹配从设备(例如,从设备最大10MHz,但设计为20MHz)。 验证CPOL/CPHA:测量SCLK空闲电平和采样边沿。Mode 0应为上升沿采样。 检查CS时序:CS拉低到第一个SCLK上升沿的延迟应>从设备的t_CS(通常50-100ns)。添加延迟:
reg [7:0] cs_delay;
always @(posedge clk) begin
if (state == IDLE && start) cs_delay <= 0; else if (cs_delay < 10) cs_delay <= cs_delay + 1; // 10 cycles delay else cs_n <= 1'b0; end 位顺序:确保MSB先发。如果从设备要求LSB先,修改移位逻辑为 shift_reg <= {miso, shift_reg[7:1]}; 并调整 mosi <= shift_reg[0]; 。 解决方案 : 调整 CLK_DIV 或添加时钟相位反转。仿真时,使用 $dumpfile 和 $dumpvars 生成VCD文件查看波形。 2. 信号完整性问题 症状 : 间歇性错误、噪声干扰。 排查步骤 : 检查PCB布线:SCLK/MOSI/MISO应短且等长,避免串扰。使用终端电阻(如果长线)。 测量电压电平:确保3.3V/5V兼容。FPGA输出是否驱动能力足够(添加缓冲器如OBUF)。 从设备侧:确认CS有效低电平时间足够长。 解决方案 : 在Verilog中添加同步器防止亚稳态: reg miso_sync1, miso_sync2; always @(posedge clk) begin miso_sync1 <= miso; miso_sync2 <= miso_sync1; end // Use miso_sync2 in design 对于高速设计,使用PLL生成干净的SCLK。 3. 复位和状态机错误 症状 : 卡在传输状态、复位后无响应。 排查步骤 : 检查复位信号:确保异步复位同步释放(使用两级触发器)。 reg rst_sync1, rst_sync2; always @(posedge clk) begin rst_sync1 <= rst_n; rst_sync2 <= rst_sync1; end wire rst_sync = rst_sync2; 状态机死锁:添加超时机制。如果 bit_cnt 长时间不减,强制返回IDLE。 reg [15:0] timeout; always @(posedge clk) begin if (state == TRANS) timeout <= timeout + 1; else timeout <= 0; if (timeout > 10000) state <= IDLE; // Timeout after ~10000 cycles end 仿真与实际不符:使用ILA(Integrated Logic Analyzer)在FPGA上调试,捕获实时信号。 4. 多从设备冲突 症状 : 选错从设备或总线竞争。 排查步骤 : 检查CS译码逻辑。确保只有一个CS低电平。 解决方案 : 使用独热码编码CS: reg [3:0] slave_sel; // 0001: slave1, 0010: slave2, etc. assign cs_n = ~(slave_sel); // Only one bit set 5. 性能问题 症状 : 传输慢或丢数据。 排查步骤 : 测量SCLK实际频率(可能因分频误差)。检查FPGA资源使用(LUT/FF是否不足)。 解决方案 : 优化状态机,使用单周期逻辑。对于高速(>50MHz),使用SerDes或IP核。
通用调试技巧
工具推荐
: Vivado ILA、SignalTap(Quartus)、ModelSim波形查看。
日志输出
: 在Testbench中使用
$display
打印关键值。
从设备手册
: 始终参考数据手册的时序图(如t_SU, t_HD)。
测试流程
: 先仿真验证逻辑,再上板测试。逐步增加时钟频率。
通过这些排查,您能快速解决90%的SPI问题。如果问题持续,检查硬件(如晶振或电源)。
结论
本文详细介绍了SPI传输的Verilog实现,从协议基础到完整代码、应用示例和问题排查。通过提供的模块和Testbench,您可以快速构建可靠的SPI主控制器,并扩展到复杂系统。SPI的简单性和高效性使其成为FPGA设计的基石,但精确的时序控制至关重要。建议在实际项目中结合仿真和硬件验证,以确保鲁棒性。如果您有特定从设备或模式需求,可进一步定制代码。欢迎在评论区分享您的实现经验!