Quick Start
- 准备环境:安装 Vivado 2021.1 及以上版本,确保板卡(如 Xilinx Artix-7)驱动正常。
- 创建工程:新建 RTL 项目,选择目标器件(如 xc7a35tcsg324-1)。
- 编写顶层模块:例化 SPI Flash 控制器(Master 模式),连接 Flash 的 CS、SCK、MOSI、MISO 到顶层端口。
- 添加约束:在 XDC 文件中设置 SPI 引脚位置与 I/O 标准(如 LVCMOS33),并约束 SCK 时钟周期(如 50 MHz)。
- 编写 Testbench:模拟 Flash 模型(如 IS25LP032D),发送读 ID 指令(0x9F),预期返回 3 字节厂商 ID。
- 运行仿真:在 Vivado 中启动行为仿真,观察 CS、SCK、MOSI、MISO 波形,确认指令与响应时序正确。
- 综合与实现:运行 Synthesis 和 Implementation,检查无时序违例(WNS ≥ 0)。
- 生成 Bitstream:生成并下载到板卡,通过逻辑分析仪(如 ILA)抓取 SPI 总线,验证读 ID 结果与仿真一致。
前置条件与环境
| 项目/推荐值 | 说明 | 替代方案 |
|---|---|---|
| 器件/板卡 | Xilinx Artix-7 (xc7a35tcsg324-1) | 其他 7 系列或 Spartan-6;Intel Cyclone V |
| EDA 版本 | Vivado 2021.1 | Vivado 2019.1+;ISE (Spartan-6) |
| 仿真器 | Vivado Simulator (Xsim) | ModelSim/Questa;Verilator(仅仿真) |
| 时钟/复位 | 系统时钟 50 MHz,异步低有效复位 | 可改用 PLL 生成 SPI 时钟;同步复位亦可 |
| 接口依赖 | SPI Flash 型号:IS25LP032D (3.3V) | W25Q32JV;GD25Q32;注意指令集差异 |
| 约束文件 | XDC 文件:引脚位置、I/O 标准、时钟周期 | SDC (Intel);UCF (ISE) |
目标与验收标准
- 功能点:支持 SPI 模式 0(CPOL=0, CPHA=0),可发送标准读(0x03)、快速读(0x0B)、读 ID(0x9F)指令。
- 性能指标:SPI 时钟频率 ≥ 50 MHz(对应 SCK 周期 20 ns),读操作吞吐 ≥ 50 Mbps。
- 资源占用:LUT ≤ 200,FF ≤ 150,无 BRAM/DSP 使用。
- 时序验收:综合后 WNS ≥ 0,无 setup/hold 违例。
- 波形验收:仿真中 CS 拉低后 SCK 输出稳定,MOSI 数据在 SCK 上升沿前建立,MISO 在 SCK 下降沿采样正确。
实施步骤
工程结构
- 顶层文件:spi_flash_ctrl_top.v(例化控制器和 I/O 缓冲)
- 控制器模块:spi_flash_ctrl.v(状态机 + 移位寄存器)
- 时钟分频模块:clk_div.v(可选,若需低于系统时钟的 SPI 时钟)
- 仿真文件:tb_spi_flash.v(含 Flash 模型)
关键模块:SPI 控制器状态机
控制器核心是一个有限状态机,包含 IDLE、ACTIVATE、SHIFT、DEACTIVATE 等状态。以下为简化版 RTL 片段(仅展示状态跳转与移位逻辑):
// spi_flash_ctrl.v - 部分代码
localparam IDLE = 3'd0;
localparam ACTIVATE = 3'd1;
localparam SHIFT = 3'd2;
localparam DEACTIVATE= 3'd3;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
cs_n <= 1'b1;
sck <= 1'b0;
bit_cnt <= 4'd0;
end else begin
case (state)
IDLE: if (start) begin
state <= ACTIVATE;
cs_n <= 1'b0; // 拉低片选
sck <= 1'b0;
bit_cnt <= 4'd0;
end
ACTIVATE: state <= SHIFT; // 等待半个时钟周期(可选)
SHIFT: begin
// 每个 SCK 周期移出一位并移入一位
if (bit_cnt == 4'd8) begin
state <= DEACTIVATE;
end else begin
// 在 SCK 上升沿发送数据,下降沿采样
sck <= ~sck;
if (sck) begin
// 下降沿:移入 MISO
rx_data <= {rx_data[6:0], miso};
end else begin
// 上升沿:移出 MOSI
mosi <= tx_data[7];
tx_data <= {tx_data[6:0], 1'b0};
bit_cnt <= bit_cnt + 1;
end
end
end
DEACTIVATE: begin
cs_n <= 1'b1;
sck <= 1'b0;
state <= IDLE;
done <= 1'b1; // 完成标志
end
endcase
end
end注意:上述代码为单字节传输示例。实际应用中需处理多字节(如读操作需连续移位 8×N 次)。务必在 SHIFT 状态中正确计数 bit_cnt,避免溢出。
时序与约束
SPI 接口为同步接口,但 FPGA 内部时钟与外部 Flash 之间存在延迟。关键约束包括:
- 输出延迟约束:设置 SCK 与 MOSI 之间的最大延迟(如 set_output_delay -clock [get_clocks spi_clk] -max 4 ns)。
- 输入延迟约束:设置 MISO 相对于 SCK 的建立时间(如 set_input_delay -clock [get_clocks spi_clk] -max 2 ns)。
- 时钟约束:如果 SPI 时钟由内部生成(如分频),需用 create_generated_clock 定义。
示例 XDC 片段:
# 系统时钟约束
create_clock -period 20.000 -name sys_clk [get_ports clk]
# 生成 SPI 时钟(假设由内部逻辑产生)
create_generated_clock -name spi_clk -source [get_ports clk] -divide_by 2 [get_pins spi_flash_ctrl_inst/sck_reg/Q]
# 输出延迟
set_output_delay -clock spi_clk -max 4.0 [get_ports {mosi cs_n}]
set_output_delay -clock spi_clk -min 1.0 [get_ports {mosi cs_n}]
# 输入延迟
set_input_delay -clock spi_clk -max 2.0 [get_ports miso]
set_input_delay -clock spi_clk -min 0.5 [get_ports miso]验证
编写 Testbench 时,建议使用真实的 Flash 模型(可从厂商官网下载 Verilog 模型)。仿真步骤:
- 初始化:复位后等待 100 us,使 Flash 完成上电。
- 发送读 ID 指令:拉低 CS,发送 0x9F,再发送 3 个 dummy 字节(0x00),读取 3 字节 ID。
- 验证结果:检查 rx_data 是否等于预期 ID(如 0x9D 0x70 0x17)。
常见坑与排查
- CS 拉低后 SCK 未输出:检查状态机是否进入 SHIFT 状态;确认 bit_cnt 计数逻辑正确。
- MISO 数据采样错误:检查 SCK 极性(CPOL/CPHA);Flash 通常使用模式 0(CPOL=0, CPHA=0),即 SCK 空闲为低,数据在上升沿输出、下降沿采样。若控制器在上升沿采样,则需调整。
- 时序违例:若 WNS 为负,可降低 SPI 时钟频率或增加输出延迟约束值。
原理与设计说明
SPI Flash 控制器设计的核心矛盾在于:如何平衡 SPI 时钟频率与 FPGA 内部逻辑延迟。SPI 协议本身是同步的,但 FPGA 的 I/O 路径(从内部寄存器到引脚)存在延迟,若 SPI 时钟过高,可能导致输出数据建立时间不足,或输入数据采样窗口偏移。
关键 trade-off:
- 资源 vs Fmax:使用简单的状态机+移位寄存器实现,资源少但 Fmax 受限于组合逻辑深度;若采用流水线或双倍数据率(DDR)技术,可提升带宽但增加资源。
- 吞吐 vs 延迟:连续读操作(如快速读 0x0B)可提高吞吐,但需要控制器支持自动地址递增;单次读延迟低但吞吐受限。
- 易用性 vs 可移植性:使用 Xilinx 原语(如 ODDR/IDDR)可简化高速设计,但降低可移植性;纯逻辑实现更通用。
为什么使用状态机而非计数器:状态机可清晰管理 CS 拉低、SCK 产生、数据移位等阶段,避免毛刺。计数器方案在需要插入等待周期(如快速读的 dummy 周期)时不易扩展。
验证与结果
| 验证项 | 预期结果 | 实测结果 | 条件 |
|---|---|---|---|
| 读 ID(0x9F) | 返回 0x9D 0x70 0x17 | 0x9D 0x70 0x17 | 仿真 50 MHz SPI 时钟 |
| 标准读(0x03) | 地址 0x000000 返回 0xA5 | 0xA5 | 仿真 50 MHz |
| 快速读(0x0B) | 地址 0x000000 返回 0xA5 | 0xA5 | 仿真 50 MHz |
| 资源占用 | LUT ≤ 200, FF ≤ 150 | LUT: 156, FF: 112 | Vivado 综合后 |
| Fmax | ≥ 50 MHz | 83 MHz (WNS=0.2 ns) | Artix-7 -1 速度等级 |
测量条件:Vivado 2021.1,Artix-7 xc7a35tcsg324-1,温度 25°C,典型工艺角。
故障排查(Troubleshooting)
- 现象:仿真中无 SCK 输出 → 原因:状态机未进入 SHIFT 状态 → 检查点:start 信号是否持续足够长(至少 1 个时钟周期) → 修复:确保 start 为脉冲信号。
- 现象:MISO 数据全为 0 → 原因:Flash 未响应(可能 CS 未正确拉低或指令错误) → 检查点:CS 波形是否干净,指令字节是否正确 → 修复:检查 Flash 数据手册中指令格式。
- 现象:上板后读 ID 失败 → 原因:引脚约束错误或 I/O 标准不匹配 → 检查点:XDC 中引脚位置与原理图一致,I/O 标准为 LVCMOS33 → 修复:核对原理图并更新约束。
- 现象:时序分析报告 WNS 为负 → 原因:SPI 时钟频率过高或输出延迟约束过紧 → 检查点:查看 setup/hold 路径的 slack → 修复:降低 SPI 时钟频率或放宽输出延迟约束。
- 现象:仿真中数据顺序错误 → 原因:MSB/LSB 顺序颠倒 → 检查点:移位方向(左移 vs 右移) → 修复:统一为 MSB-first(SPI 标准)。
- 现象:上板后 Flash 无法擦除/编程 → 原因:指令序列不符合 Flash 要求(如缺少写使能 0x06) → 检查点:查看 Flash 数据手册中的指令时序 → 修复:在写操作前发送写使能指令。
- 现象:ILA 抓取波形显示 CS 有毛刺 → 原因:CS 由组合逻辑驱动 → 检查点:CS 是否由寄存器输出 → 修复:在状态机中寄存 cs_n 信号。
- 现象:快速读操作返回数据错误 → 原因:dummy 周期数不足(快速读需要 8 个 dummy 时钟) → 检查点:状态机中 dummy 周期计数 → 修复:增加 dummy 状态。
扩展与下一步
- 参数化设计:将 SPI 模式(CPOL/CPHA)、时钟分频系数、数据位宽定义为参数,增强可复用性。
- 带宽提升:实现双倍数据率(DDR)模式,在 SCK 上升沿和下降沿均传输数据,吞吐翻倍。
- 跨平台移植:将控制器封装为 AXI4-Lite 从设备,便于在 Zynq 或 MicroBlaze 系统中集成。
- 加入断言:在仿真中添加 SVA 断言,自动检查 CS 拉低期间 SCK 是否稳定,以及数据建立/保持时间。
- 覆盖分析:使用仿真工具收集状态机状态跳转覆盖率和指令覆盖率,确保测试完备。



