1. 项目概述
SPI(Serial Peripheral Interface)作为嵌入式系统和数字电路设计中最常用的同步串行通信协议之一,其4线制Master端的Verilog实现是FPGA/ASIC开发中的基础技能。这个项目完整覆盖了从RTL代码编写、Testbench构建到功能仿真的全流程,特别适合正在学习数字电路设计或需要快速实现SPI主机控制器的工程师。
在实际项目中,我经常遇到需要与各种SPI从设备(如Flash存储器、ADC/DAC转换器、传感器等)通信的场景。一个稳定可靠的SPI Master控制器能显著降低系统复杂度,而Verilog实现的优势在于可以灵活适配不同时钟域和时序要求。本文将分享我在多个工业级项目中积累的SPI Master设计经验,包括时钟域交叉处理、可配置时钟分频等实战技巧。
2. 核心设计思路
2.1 SPI协议关键特性解析
4线制SPI包含以下信号线:
- SCLK:时钟信号,由Master产生
- MOSI:Master输出,Slave输入
- MISO:Master输入,Slave输出
- SS_n:片选信号(低有效)
协议核心参数包括:
- 时钟极性(CPOL):决定SCLK空闲状态电平
- 时钟相位(CPHA):决定数据采样边沿
- 传输位序(LSB/MSB first)
- 时钟频率(由系统时钟分频得到)
重要提示:CPOL和CPHA的组合形成4种SPI模式,必须与从设备严格匹配。工业设备中最常用的是Mode 0(CPOL=0,CPHA=0)和Mode 3(CPOL=1,CPHA=1)。
2.2 硬件架构设计
我们的SPI Master采用三级流水线结构:
- 配置寄存器组:存储控制参数(分频系数、传输模式等)
- 时钟生成单元:产生可配置的SCLK信号
- 数据移位单元:处理并行-串行转换
verilog复制module spi_master #(
parameter DATA_WIDTH = 8,
parameter CLK_DIV_WIDTH = 8
)(
input wire clk,
input wire rst_n,
// Configuration interface
input wire [CLK_DIV_WIDTH-1:0] clk_div,
input wire cpol, cpha,
input wire lsb_first,
// Data interface
input wire [DATA_WIDTH-1:0] tx_data,
input wire tx_valid,
output reg [DATA_WIDTH-1:0] rx_data,
output reg rx_ready,
// SPI physical interface
output reg sclk,
output reg mosi,
input wire miso,
output reg ss_n
);
3. 关键模块实现细节
3.1 可配置时钟生成器
时钟分频是SPI Master的核心功能之一。我们采用计数器+比较器的经典设计,支持动态调整SCLK频率:
verilog复制// Clock divider implementation
reg [CLK_DIV_WIDTH-1:0] clk_counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_counter <= 0;
sclk <= cpol; // Initialize to idle state
end else begin
if (clk_counter == clk_div) begin
clk_counter <= 0;
sclk <= ~sclk; // Toggle SCLK
end else begin
clk_counter <= clk_counter + 1;
end
end
end
设计技巧:将SCLK初始化为CPOL指定的空闲电平,避免上电时的毛刺。分频系数clk_div实际对应半个SCLK周期,因此实际SCLK频率为系统时钟频率/(2*(clk_div+1))。
3.2 数据移位控制逻辑
数据传输状态机是另一个关键模块,需要处理CPHA带来的时序差异:
verilog复制// State machine for data shifting
typedef enum logic [1:0] {
IDLE,
PREPARE,
SHIFT,
COMPLETE
} state_t;
state_t current_state;
reg [DATA_WIDTH-1:0] shift_reg;
reg [3:0] bit_counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_state <= IDLE;
shift_reg <= 0;
bit_counter <= 0;
mosi <= 1'b0;
ss_n <= 1'b1;
end else begin
case (current_state)
IDLE: begin
if (tx_valid) begin
shift_reg <= lsb_first ? tx_data : reverse_bits(tx_data);
ss_n <= 1'b0;
current_state <= PREPARE;
end
end
PREPARE: begin
if (sclk == cpol ^ cpha) begin
mosi <= shift_reg[DATA_WIDTH-1];
current_state <= SHIFT;
end
end
SHIFT: begin
if (sclk == cpol ^ ~cpha) begin
shift_reg <= {shift_reg[DATA_WIDTH-2:0], miso};
bit_counter <= bit_counter + 1;
if (bit_counter == DATA_WIDTH-1)
current_state <= COMPLETE;
end else if (sclk == cpol ^ cpha) begin
mosi <= shift_reg[DATA_WIDTH-1];
end
end
COMPLETE: begin
rx_data <= lsb_first ? shift_reg : reverse_bits(shift_reg);
rx_ready <= 1'b1;
ss_n <= 1'b1;
bit_counter <= 0;
current_state <= IDLE;
end
endcase
end
end
4. Testbench设计与仿真
4.1 自动化测试平台架构
完整的验证环境包含:
- 时钟与复位发生器
- SPI Slave行为模型
- 激励生成器
- 结果检查器
verilog复制module tb_spi_master;
reg clk = 0;
reg rst_n = 0;
// Instantiate DUT
spi_master #(
.DATA_WIDTH(8),
.CLK_DIV_WIDTH(8)
) dut (
.clk(clk),
.rst_n(rst_n),
// ...其他信号连接...
);
// Clock generation
always #5 clk = ~clk;
// Reset generation
initial begin
#100 rst_n = 1;
#2000 $finish;
end
// Test scenario
initial begin
wait(rst_n);
// 配置SPI模式0,分频系数=1
dut.cpol = 0;
dut.cpha = 0;
dut.clk_div = 1;
dut.lsb_first = 0;
// 发送测试数据
repeat(10) begin
@(posedge clk);
dut.tx_data = $random;
dut.tx_valid = 1;
@(posedge clk);
dut.tx_valid = 0;
wait(dut.rx_ready);
$display("TX: %h, RX: %h", dut.tx_data, dut.rx_data);
end
end
endmodule
4.2 关键测试用例
-
基础功能测试:
- 验证所有SPI模式(CPOL/CPHA组合)
- 测试MSB/LSB优先传输
- 检查不同时钟分频系数下的时序
-
边界条件测试:
- 最小/最大分频系数
- 连续背靠背传输
- 复位期间的信号稳定性
-
错误注入测试:
- 在传输过程中动态改变模式
- 模拟从设备响应超时
- 注入时钟抖动
5. 工程实践中的经验总结
5.1 跨时钟域处理技巧
当系统时钟与SPI时钟不同源时,需要特别注意:
verilog复制// 异步FIFO用于跨时钟域数据传输
async_fifo #(
.DATA_WIDTH(8),
.DEPTH(16)
) tx_fifo (
.wr_clk(system_clk),
.wr_data(tx_data_sys),
.wr_en(tx_valid_sys),
.rd_clk(clk),
.rd_data(tx_data),
.rd_en(tx_ready),
.full(),
.empty()
);
实际踩坑:曾遇到因跨时钟域处理不当导致的偶发数据丢失。解决方案是采用格雷码计数器+双触发器同步的异步FIFO设计。
5.2 时序收敛优化
在高速SPI应用(>50MHz)中,需特别关注:
-
对SCLK路径添加时序约束:
tcl复制create_generated_clock -name SPI_SCLK -source [get_pins clk_gen/Q] \ -divide_by 2 [get_ports sclk] set_output_delay -clock [get_clocks SPI_SCLK] 2.0 [get_ports mosi] -
在物理实现时:
- 将IO寄存器放置在靠近管脚的位置
- 对SPI信号线做等长匹配
- 使用差分时钟时需添加专用时钟缓冲器
5.3 调试技巧实录
-
信号完整性诊断:
- 使用示波器检查SCLK与数据信号的建立/保持时间
- 测量SS_n下降沿到第一个SCLK边沿的延迟(应满足从设备tCSS要求)
-
常见故障模式:
- 数据错位:检查LSB/MSB配置和采样边沿
- 偶发错误:检查跨时钟域同步和亚稳态防护
- 通信完全失败:验证CPOL/CPHA模式匹配
-
高级调试手段:
- 在FPGA中嵌入ILA(集成逻辑分析仪)
- 通过JTAG实时修改寄存器配置
- 注入伪随机测试序列进行压力测试
6. 性能优化进阶
6.1 批量传输优化
支持连续传输的增强设计:
verilog复制// 添加burst传输控制信号
input wire burst_en,
input wire [15:0] burst_len,
// 状态机新增BURST状态
BURST: begin
if (bit_counter == DATA_WIDTH-1) begin
if (burst_count < burst_len) begin
shift_reg <= next_data;
burst_count <= burst_count + 1;
bit_counter <= 0;
end else begin
current_state <= COMPLETE;
end
end
end
6.2 DMA接口集成
与DMA控制器协同工作的接口设计:
- 添加AXI4-Stream接口
- 实现Descriptor-based传输
- 支持中断通知机制
verilog复制// AXI4-Stream接口示例
output wire [31:0] m_axis_tdata,
output wire m_axis_tvalid,
input wire m_axis_tready,
input wire [31:0] s_axis_tdata,
input wire s_axis_tvalid,
output wire s_axis_tready
6.3 动态配置架构
支持运行时重配置的高级特性:
-
寄存器映射:
- 0x00: 控制寄存器(CPOL, CPHA, LSBF等)
- 0x04: 时钟分频系数
- 0x08: 传输数据长度
- 0x0C: 数据FIFO
-
基于APB/AXI-Lite的配置接口:
verilog复制input wire psel, input wire penable, input wire pwrite, input wire [31:0] pwdata, output reg [31:0] prdata
7. 实际应用案例
7.1 Flash存储器编程
与Winbond W25Q128JV的交互要点:
- 需实现Page Program (0x02)和Quad Read (0xEB)指令
- 注意tPP(页编程时间)和tRES(器件复位时间)要求
- 典型时序约束:
verilog复制// 上电后等待tPU initial begin power_on_reset(); #100us; // tPU=30us max release_reset(); end
7.2 高速ADC数据采集
与AD7980的配合设计:
- 18位分辨率,2MSPS采样率
- 采用Mode 3(CPOL=1, CPHA=1)
- 关键时序:
- CNV上升沿到第一个SCLK下降沿:≥tCNV
- SCLK高/低电平时间:≥tCH/tCL
verilog复制// ADC采集控制逻辑
always @(negedge adc_cnv) begin
adc_ss_n <= 0;
#tCNV;
start_spi_transfer();
end
7.3 多从设备管理
通过SS_n扩展支持多个SPI Slave:
- 片选译码逻辑设计
- 动态切换时序要求(tSSD)
- 共享总线信号处理
verilog复制// 片选译码示例
always @(*) begin
case (slave_addr)
2'b00: {ss_n1, ss_n2, ss_n3} = 3'b110;
2'b01: {ss_n1, ss_n2, ss_n3} = 3'b101;
2'b10: {ss_n1, ss_n2, ss_n3} = 3'b011;
default: {ss_n1, ss_n2, ss_n3} = 3'b111;
endcase
end
在多个工业项目中验证,这种SPI Master设计可稳定工作在100MHz系统时钟下(SCLK最高25MHz),资源占用约200个LUT和4个Block RAM(含FIFO)。通过参数化设计,可灵活适配不同位宽和性能要求的应用场景。