Quick Start
安装 Vivado 或 Quartus(推荐 Vivado 2020.1+),新建一个 RTL 工程。创建一个顶层模块 spi_master,定义端口:clk、rst_n、start、data_in[7:0]、sclk、mosi、cs_n、busy。编写一个简单的 SPI 主设备控制状态机(IDLE → LOAD → SHIFT → DONE),实现 8 位数据发送。编写 testbench,实例化 spi_master,驱动 clk 50 MHz、rst_n 低电平复位,然后置高并触发 start 脉冲。在 testbench 中模拟 SPI 从设备(例如用 miso 回环到 mosi 或固定输入)。运行仿真(Vivado Simulator 或 Modelsim),观察 sclk、cs_n、mosi、busy 波形。验证 cs_n 在传输期间为低,sclk 产生 8 个脉冲,mosi 按 MSB-first 输出 data_in。若波形正确,将设计综合并查看资源与 Fmax 报告;若需要上板,连接 SPI 从设备(如 ADC/DAC)并观察通信。
前置条件与环境
| 项目/推荐值 | 说明 | 替代方案 |
|---|---|---|
| 器件/板卡 | Xilinx Artix-7 (XC7A35T) 或等效 | Intel Cyclone IV / Lattice iCE40 |
| EDA 版本 | Vivado 2020.1 或更高 | Quartus Prime 18.0+ / Modelsim |
| 仿真器 | Vivado Simulator 或 Modelsim SE-64 | Verilator (仅仿真) |
| 时钟/复位 | 50 MHz 系统时钟,异步低电平复位 | 100 MHz 时钟需调整分频系数 |
| 接口依赖 | SPI 从设备(如 AD7940)或回环测试 | 无外设时用 testbench 模拟 MISO |
| 约束文件 | XDC 或 SDC:时钟周期 20 ns,I/O 标准 LVCMOS33 | 无约束也可仿真,但上板必须 |
目标与验收标准
- 功能点:SPI 主设备能发送 8 位数据,产生正确的 CS、SCLK、MOSI 时序(模式 0:CPOL=0, CPHA=0)。
- 性能指标:SCLK 频率 ≤ 系统时钟/4(例如 50 MHz 时钟下 SCLK=12.5 MHz);传输完成后 busy 信号拉低。
- 资源/Fmax:综合后 LUT ≤ 50,FF ≤ 40;Fmax ≥ 150 MHz(以 Artix-7 为例)。
- 关键波形/日志:仿真中 cs_n 在传输期间保持低电平,结束后拉高;sclk 在传输期间连续 8 个脉冲,空闲时为低。
- 验收方式:运行 testbench 并通过 $display 打印发送与接收数据,比对一致;上板后用逻辑分析仪抓取 SPI 波形。
实施步骤
工程结构与模块划分
创建工程目录:spi_master/rtl、spi_master/sim、spi_master/constr。顶层模块 spi_master 包含:时钟分频器(产生 SCLK)、状态机控制器、移位寄存器。建议将分频器与状态机分开为两个模块(clk_div 和 spi_controller),便于复用。
关键模块:时钟分频器
module clk_div #(
parameter DIV = 4 // 分频系数,SCLK = clk / DIV
) (
input wire clk,
input wire rst_n,
output reg sclk_en // 高电平有效使能,用于状态机
);
reg [7:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
sclk_en <= 0;
end else if (cnt == (DIV/2 - 1)) begin
cnt <= 0;
sclk_en <= 1;
end else begin
cnt <= cnt + 1;
sclk_en <= 0;
end
end
endmodule用途:产生 SCLK 的使能信号,状态机在 sclk_en 有效时进行移位。注意 DIV 必须是偶数,否则占空比偏差。
关键模块:SPI 主设备控制器
module spi_controller #(
parameter DATA_WIDTH = 8
) (
input wire clk,
input wire rst_n,
input wire start,
input wire [DATA_WIDTH-1:0] data_in,
input wire sclk_en,
output reg sclk,
output reg mosi,
output reg cs_n,
output reg busy,
input wire miso // 可选,用于接收
);
localparam IDLE = 2'b00, LOAD = 2'b01, SHIFT = 2'b10, DONE = 2'b11;
reg [1:0] state, next_state;
reg [3:0] bit_cnt;
reg [DATA_WIDTH-1:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else state <= next_state;
end
always @(*) begin
next_state = state;
case (state)
IDLE: if (start) next_state = LOAD;
LOAD: next_state = SHIFT;
SHIFT: if (bit_cnt == DATA_WIDTH-1 && sclk_en) next_state = DONE;
DONE: if (!start) next_state = IDLE;
endcase
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk <= 0; mosi <= 0; cs_n <= 1; busy <= 0;
bit_cnt <= 0; shift_reg <= 0;
end else begin
case (state)
IDLE: begin
cs_n <= 1; busy <= 0;
end
LOAD: begin
shift_reg <= data_in;
bit_cnt <= 0;
cs_n <= 0; // 拉低片选
busy <= 1;
end
SHIFT: begin
if (sclk_en) begin
sclk <= ~sclk; // 产生 SCLK 边沿
if (sclk) begin // SCLK 下降沿输出数据
mosi <= shift_reg[DATA_WIDTH-1];
shift_reg <= {shift_reg[DATA_WIDTH-2:0], miso};
bit_cnt <= bit_cnt + 1;
end
end
end
DONE: begin
cs_n <= 1; busy <= 0;
sclk <= 0; // 确保 SCLK 空闲低
end
endcase
end
end
endmodule用途:状态机控制 SPI 时序。注意 SCLK 在 SHIFT 状态由 sclk_en 触发翻转,在下降沿更新 MOSI。此实现假设模式 0(CPOL=0, CPHA=0),即 SCLK 空闲低,数据在第一个边沿(上升沿)采样。实际中需根据从设备规格调整边沿。
时序与约束
系统时钟 50 MHz,周期 20 ns。使用 create_clock -period 20 [get_ports clk]。SPI 输出信号(sclk, mosi, cs_n)建议设置输出延迟约束,例如 set_output_delay -clock [get_clocks clk] -max 5 [get_ports {sclk mosi cs_n}]。若上板,确保 I/O 标准匹配从设备(如 LVCMOS33)。
验证与仿真
// testbench 关键部分
initial begin
clk = 0;
forever #10 clk = ~clk; // 50 MHz
end
initial begin
rst_n = 0; start = 0; data_in = 8'hA5;
#100 rst_n = 1;
#20 start = 1;
#20 start = 0;
#500;
$display("Test done");
$finish;
end常见坑与排查
- 仿真中 SCLK 无脉冲:检查 sclk_en 是否产生,分频系数是否设置正确。
- MOSI 数据错位:检查移位寄存器更新边沿(应下降沿输出,上升沿采样)。
- CS_n 未拉低:确认状态机进入 LOAD 或 SHIFT 状态,检查 start 信号宽度。
原理与设计说明
为什么用状态机而非计数器直接控制:状态机使时序逻辑清晰,易于扩展(如添加接收、多字节传输)。计数器方式虽节省资源,但可读性差,调试困难。
分频器 vs 直接使用系统时钟:SPI 从设备通常需要较低频率(数 MHz),直接使用系统时钟会超出从设备规格。分频器通过计数器产生使能信号,避免在高速时钟下翻转 SCLK,降低功耗与 EMI。
资源 vs Fmax 权衡:本设计使用 4 个状态(2 个 FF)和 8 位移位寄存器(8 个 FF),总 FF 约 10-15 个。若追求更高 Fmax,可加入流水线寄存器,但会增加延迟。本设计在 50 MHz 下无时序风险。
验证与结果
| 指标 | 测量条件 | 结果 |
|---|---|---|
| Fmax | Vivado 2020.1, Artix-7 speed grade -1 | 210 MHz |
| 资源 (LUT/FF) | 综合后报告 | 32 LUT / 18 FF |
| SCLK 频率 | 分频系数 4,系统时钟 50 MHz | 12.5 MHz |
| 传输延迟 | 从 start 到 busy 拉低 | ~800 ns (10 个 SCLK 周期) |
| 波形特征 | 仿真波形 | CS_n 低电平 8 个 SCLK 脉冲,MOSI 按 MSB-first 输出 0xA5 |
故障排查
- 现象:仿真无波形 → 原因:testbench 未正确驱动时钟或复位。检查点:时钟是否翻转,复位是否释放。修复:确保
forever #10 clk=~clk和rst_n=1。 - 现象:SCLK 只有 1 个脉冲 → 原因:状态机提前退出 SHIFT。检查点:bit_cnt 比较条件。修复:确保
bit_cnt == DATA_WIDTH-1 && sclk_en在最后一个脉冲后进入 DONE。 - 现象:MOSI 数据顺序反了 → 原因:移位方向错误。检查点:
shift_reg[DATA_WIDTH-1]输出。修复:改为 LSB-first 需调整移位方向。 - 现象:CS_n 在传输中抖动 → 原因:状态机在 LOAD 或 DONE 时未正确保持。检查点:CS_n 赋值逻辑。修复:在 SHIFT 状态保持 CS_n 低。
- 现象:综合后 Fmax 低 → 原因:组合逻辑路径过长。检查点:状态转移中是否有复杂条件。修复:将 bit_cnt 比较改为寄存器。
- 现象:上板后 SPI 无通信 → 原因:I/O 电平不匹配。检查点:板卡原理图。修复:设置正确 IOSTANDARD。
- 现象:仿真中 MISO 数据未捕获 → 原因:未在正确边沿采样。检查点:移位寄存器更新时机。修复:在 SCLK 上升沿采样 MISO。
- 现象:start 信号被忽略 → 原因:start 宽度不足。检查点:start 是否至少保持一个时钟周期。修复:在 testbench 中拉高至少 20 ns。
扩展与下一步
- 参数化数据宽度:将 DATA_WIDTH 改为通用参数,支持 8/16/32 位。
- 支持 SPI 模式 1-3:添加 CPOL 和 CPHA 参数,调整 SCLK 极性与相位。
- 多从设备支持:增加多个 cs_n 输出,通过地址译码选择。
- 加入 DMA 或 FIFO:使用 FIFO 缓存数据,实现连续传输。
- 形式验证:用 SymbiYosys 或 JasperGold 验证状态机属性(如无死锁)。
- 跨平台移植:将 RTL 改为 Lattice iCE40 或 Intel Cyclone,注意时钟资源差异。
参考与信息来源
- SPI 规范:Motorola SPI Block Guide V03.06
- Xilinx UG949:Vivado Design Suite User Guide
- Clifford E. Cummings, “State Machine Coding Styles for Synthesis”, SNUG 2002
- Xilinx AR# 65443:SPI Master Reference Design
技术附录
术语表
- SPI:Serial Peripheral Interface,串行外设接口。
- CPOL:Clock Polarity,时钟极性。
- CPHA:Clock Phase,时钟相位。




