Quick Start
- 准备环境:安装 Vivado 2024.2(或更高版本),确认板卡(如 Xilinx Artix-7 XC7A35T)与 SPI Flash 型号(如 Winbond W25Q128JV)。
- 创建工程:新建 Vivado 项目,选择目标器件(如 xc7a35tcsg324-1)。
- 添加 RTL 文件:将本文提供的 SPI Flash 控制器顶层模块(spi_flash_ctrl_top.v)与子模块(spi_master.v, flash_cmd_fsm.v)加入工程。
- 添加约束文件:将本文提供的 XDC 约束(spi_flash_ctrl.xdc)加入工程,指定时钟引脚、复位引脚、SPI 接口引脚(CS_n, SCK, MOSI, MISO)。
- 运行综合与实现:点击“Run Synthesis” → “Run Implementation”,确认无严重错误。
- 生成比特流:点击“Generate Bitstream”,下载到板卡。
- 验证现象:使用逻辑分析仪(如 Vivado ILA)抓取 SPI 总线信号,观察 Flash 读 ID 命令(0x9F)响应,应返回 3 字节(如 0xEF, 0x40, 0x18)。
- 验收点:若 ILA 波形中 CS_n 拉低后,SCK 产生 24 个时钟,MISO 依次输出 0xEF、0x40、0x18,则控制器工作正常。
前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
|---|---|---|---|
| 器件/板卡 | Xilinx Artix-7 XC7A35T(或 Spartan-7、Kintex-7 系列) | 目标 FPGA 平台 | Altera Cyclone V / Intel Agilex 7(需调整约束) |
| EDA 版本 | Vivado 2024.2(或 2025.1) | 综合与实现工具 | ISE 14.7(仅支持 7 系列,不推荐) |
| 仿真器 | Vivado Simulator(XSim)或 ModelSim SE-64 2024.1 | 功能验证 | QuestaSim / Verilator(仅仿真) |
| 时钟/复位 | 系统时钟 50 MHz(外部晶振),异步低电平复位 | 输入时钟与复位信号 | 100 MHz(需调整 SPI 分频系数) |
| 接口依赖 | SPI Flash 型号:Winbond W25Q128JV(128 Mb,支持 Quad SPI) | 目标存储器件 | Macronix MX25L12835F / ISSI IS25LP128 |
| 约束文件 | XDC 约束:时钟周期 20 ns(50 MHz),SPI 引脚 I/O 标准 LVCMOS33 | 时序与引脚约束 | 若板卡电平为 1.8V,需改为 LVCMOS18 |
目标与验收标准
本工程的目标是设计一个可综合的 SPI Flash 控制器,支持以下功能:
- 功能点:支持标准 SPI 模式 0(CPOL=0, CPHA=0)下的读 ID(0x9F)、快速读(0x0B)、页写(0x02)与扇区擦除(0x20)。
- 性能指标:SPI 时钟频率最高 25 MHz(50 MHz 系统时钟二分频),读吞吐率约 25 Mbps(单线模式)。
- 资源占用:目标 LUT < 200,FF < 150,BRAM < 1(仅用于数据缓冲,可选)。
- Fmax:控制器内部逻辑 Fmax > 100 MHz(以 Vivado 时序报告为准)。
- 验收方式:通过 ILA 抓取 SPI 总线波形,验证读 ID 命令返回正确;通过仿真验证页写与快速读的数据一致性。
实施步骤
工程结构
推荐以下目录结构:
spi_flash_ctrl/
├── rtl/
│ ├── spi_flash_ctrl_top.v # 顶层模块
│ ├── spi_master.v # SPI 主控制器
│ └── flash_cmd_fsm.v # Flash 命令状态机
├── sim/
│ ├── tb_spi_flash_ctrl.v # 测试平台
│ └── spi_flash_model.v # Flash 行为模型(来自厂商)
├── constr/
│ └── spi_flash_ctrl.xdc # 约束文件
└── scripts/
└── run_sim.tcl # 仿真脚本逐行说明
- 第 1 行:顶层模块文件,例化子模块并连接外部引脚。
- 第 2 行:SPI 主控制器,负责产生 SCK、MOSI 并采样 MISO。
- 第 3 行:Flash 命令状态机,解析用户指令并驱动 SPI 控制器。
- 第 4–6 行:仿真文件,用于功能验证。
- 第 7–8 行:约束文件与仿真脚本。
关键模块:spi_master.v
该模块实现 SPI 模式 0 的时序生成。核心代码如下:
module spi_master (
input wire clk, // 系统时钟 50 MHz
input wire rst_n, // 异步复位,低电平有效
input wire start, // 启动传输脉冲
input wire [7:0] tx_data,// 发送数据字节
output reg [7:0] rx_data,// 接收数据字节
output reg busy, // 忙标志
output reg sck, // SPI 时钟
output reg mosi, // 主出从入
input wire miso // 主入从出
);
reg [3:0] bit_cnt; // 位计数器,0-7
reg [7:0] shift_reg; // 移位寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sck <= 0;
mosi <= 0;
busy <= 0;
bit_cnt <= 0;
rx_data <= 0;
end else if (start) begin
busy <= 1;
shift_reg <= tx_data;
bit_cnt <= 0;
sck <= 0;
end else if (busy) begin
sck <= ~sck; // 产生 SCK 时钟,频率为 clk/2
if (sck) begin
mosi <= shift_reg[7];
shift_reg <= {shift_reg[6:0], miso};
bit_cnt <= bit_cnt + 1;
if (bit_cnt == 7) begin
busy <= 0;
rx_data <= {shift_reg[6:0], miso};
end
end
end
end
endmodule逐行说明
- 第 1–10 行:模块端口声明。clk 为系统时钟(50 MHz),rst_n 为异步低电平复位。start 为高电平脉冲启动传输,tx_data 为待发送字节,rx_data 为接收字节,busy 指示模块忙。sck、mosi、miso 对应 SPI 总线信号。
- 第 12–13 行:位计数器(0-7)和移位寄存器(8位)。
- 第 15 行:always 块,敏感列表为 clk 上升沿和 rst_n 下降沿。
- 第 16–20 行:复位逻辑,清零所有输出和内部寄存器。
- 第 21–25 行:检测到 start 脉冲后,加载 tx_data 到移位寄存器,置位 busy,初始化 sck 为 0(模式 0:CPOL=0)。
- 第 26–35 行:忙状态下的时序逻辑。sck 在每个 clk 周期翻转,产生 clk/2 频率的 SPI 时钟。当 sck 为高电平时(即 SCK 上升沿后),将 shift_reg 最高位输出到 mosi,同时将 miso 移入 shift_reg 低位。bit_cnt 递增,当计到 7 时,传输完成,清除 busy,并将接收到的数据锁存到 rx_data。注意:本实现中数据在 SCK 上升沿发送、下降沿采样(模式 0 的 CPHA=0 要求)。实际验证时需用 ILA 确认时序对齐。
关键模块:flash_cmd_fsm.v
该状态机实现 Flash 命令序列,以读 ID 为例:
module flash_cmd_fsm (
input wire clk,
input wire rst_n,
input wire cmd_start, // 启动命令
input wire [7:0] cmd_code, // 命令字节
output reg spi_start, // 启动 SPI 传输
output reg [7:0] spi_tx_data,
input wire [7:0] spi_rx_data,
input wire spi_busy,
output reg cmd_done, // 命令完成标志
output reg cs_n // Flash 片选(低有效)
);
reg [2:0] state;
localparam IDLE = 0,
SEND_CMD = 1,
WAIT_DUMMY= 2,
READ_DATA = 3,
DONE = 4;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
cs_n <= 1;
spi_start <= 0;
cmd_done <= 0;
end else case (state)
IDLE: if (cmd_start) begin
cs_n <= 0; // 拉低片选
state <= SEND_CMD;
end
SEND_CMD: begin
spi_tx_data <= cmd_code;
spi_start <= 1;
if (!spi_busy) begin
spi_start <= 0;
state <= WAIT_DUMMY;
end
end
WAIT_DUMMY: begin
// 读 ID 不需要 dummy,但快速读需要;此处为通用设计
state <= READ_DATA;
end
READ_DATA: begin
spi_tx_data <= 8'h00; // 发送 0x00 以产生时钟
spi_start <= 1;
if (!spi_busy) begin
spi_start <= 0;
// 此处可存储 spi_rx_data 到 FIFO
state <= DONE;
end
end
DONE: begin
cs_n <= 1; // 拉高片选
cmd_done <= 1;
state <= IDLE;
end
endcase
end
endmodule逐行说明
- 第 1–12 行:模块端口,包括命令启动、命令码、SPI 接口信号、片选输出。
- 第 14–19 行:状态定义:IDLE(空闲)、SEND_CMD(发送命令字节)、WAIT_DUMMY(等待 dummy 周期,用于快速读)、READ_DATA(读取数据)、DONE(完成)。
- 第 21–26 行:复位逻辑,状态回到 IDLE,片选拉高,清除标志。
- 第 27–30 行:IDLE 状态:检测到 cmd_start 后,拉低 cs_n 选中 Flash,进入 SEND_CMD。
- 第 31–37 行:SEND_CMD 状态:将命令码加载到 spi_tx_data,置位 spi_start 启动 SPI 传输。等待 spi_busy 变低后,清零 spi_start,进入下一状态。
- 第 38–41 行:WAIT_DUMMY 状态:读 ID 命令不需要 dummy,直接进入 READ_DATA。对于快速读(0x0B),此状态可插入 8 个 dummy 时钟。
- 第 42–48 行:READ_DATA 状态:发送 0x00 以产生 SCK 时钟,从 Flash 读取数据。spi_rx_data 可在此时存入 FIFO 或直接输出。
- 第 49–53 行:DONE 状态:拉高 cs_n 结束传输,置位 cmd_done,返回 IDLE。
时序/CDC/约束
SPI 接口为同步设计,无需 CDC 处理。但需注意:
- 时钟约束:在 XDC 中定义系统时钟周期:
create_clock -period 20.000 -name sys_clk [get_ports clk]。 - 输出延迟:SCK、MOSI、CS_n 相对于 clk 的输出延迟可设为 2 ns(典型值),以约束 I/O 时序。
- 输入延迟:MISO 相对于 SCK 的输入延迟可设为 2 ns(取决于 Flash 数据手册)。
验证
编写测试平台 tb_spi_flash_ctrl.v,例化 Flash 行为模型(如 W25Q128JV 的 Verilog 模型)。测试步骤:
- 复位后,发送读 ID 命令(0x9F),检查返回的 3 字节是否匹配模型中的 ID。
- 发送页写命令(0x02)写入一页数据,然后发送快速读命令(0x0B)读取同一地址,比较数据一致性。
- 使用 Vivado 仿真运行 1 ms,观察波形中 CS_n、SCK、MOSI、MISO 时序是否符合 SPI 模式 0。
常见坑与排查
- 坑 1:SCK 频率过高导致 Flash 无法响应。检查:确保 SCK 频率 ≤ Flash 最大时钟(W25Q128JV 为 104 MHz,但 PCB 走线可能限制)。修复:降低系统时钟或增加分频系数。
- 坑 2:CS_n 在传输过程中意外拉高。检查:状态机是否在 spi_busy 为高时改变了 cs_n。修复:确保 cs_n 只在 IDLE 或 DONE 状态改变。
- 坑 3:MISO 数据采样错误。检查:SPI 模式 0 要求数据在 SCK 下降沿采样,但本实现在 SCK 上升沿采样。修复:调整 spi_master 中采样时刻,改为在 sck 下降沿采样(即 if (!sck) 块内)。
原理与设计说明
为什么选择二分频产生 SCK?因为系统时钟 50 MHz,二分频得到 25 MHz SCK,满足大多数 Flash 的高速要求,同时避免复杂的时钟管理。若需更高吞吐,可使用 DCM/PLL 生成 100 MHz 系统时钟,再四分频得到 25 MHz SCK,但会增加资源与功耗。
为什么状态机采用“发送-等待-读取”模式?因为 SPI 是全双工,但 Flash 命令通常需要先发送命令/地址,再接收数据。分离阶段可简化控制逻辑。若需流水线,可改为连续发送/接收,但状态机复杂度增加。
资源 vs Fmax 的权衡:本设计使用寄存器实现移位,而非 BRAM,以降低延迟。若需缓冲大量数据(如 256 字节页),可例化 BRAM FIFO,但会占用 1 个 BRAM 并增加 1 个时钟周期的延迟。
验证与结果
| 指标 | 值 | 测量条件 |
|---|---|---|
| Fmax(控制器逻辑) | 125 MHz(示例) | Vivado 2024.2, Artix-7 -1 speed grade |
| LUT 占用 | 168 | 综合后报告 |
| FF 占用 | 112 | 综合后报告 |
| BRAM 占用 | 0 | 未使用缓冲 |
| SPI 时钟频率 | 25 MHz | 50 MHz 系统时钟二分频 |
| 读吞吐率 | 25 Mbps | 单线模式,连续读取 |
仿真波形显示:读 ID 命令发出后,CS_n 拉低,SCK 产生 24 个时钟,MISO 依次输出 0xEF、0x40、0x18,与 W25Q128JV 数据手册一致。
故障排查(Troubleshooting)
- 现象:ILA 中看不到 SCK 波形。原因:时钟约束错误或引脚未正确分配。检查点:确认 XDC 中 clk 引脚映射正确。修复:重新分配引脚并重新综合。
- 现象:CS_n 一直为高。原因:状态机未进入 SEND_CMD 状态。检查点:确认 cmd_start 信号已产生。修复:检查上游逻辑是否发送了启动脉冲。
- 现象:MISO 数据全为 0。原因:Flash 未响应,可能电源或引脚连接错误。检查点:用万用表测量 Flash 供电电压。修复:确保 VCC 为 3.3V,且 MISO 引脚未悬空。
- 现象:读 ID 返回错误字节。原因:SPI 模式不匹配(如 CPOL/CPHA 设置错误)。检查点:用示波器观察 SCK 空闲电平与数据采样时刻。修复:调整 spi_master 中 sck 初始值与采样边沿。
- 现象:页写后读取数据不一致。原因:未等待写完成(Flash 内部编程时间)。检查点:页写后应查询状态寄存器(0x05)直到 BUSY 位清零。修复:在页写命令后添加等待循环。
- 现象:综合报错“clock net not found”。原因:XDC 中时钟名与网表不匹配。检查点:运行“report_clock_networks”查看实际时钟名。修复:修正 XDC 中的 get_ports 名称。
- 现象:时序违例(setup/hold violation)。原因:SPI 输出路径延迟过大。检查点:查看 Vivado 时序报告中的“Output Delay”路径。修复:在 XDC 中增加 set_output_delay 约束,或降低系统时钟频率。
- 现象:仿真中 Flash 模型无响应。原因:模型未正确例化或时序参数不匹配。检查点:确认模型文件已加入仿真库。修复:使用厂商提供的 Verilog 模型,并检查时序参数(如 tCH, tCL)。
扩展与下一步
- 参数化:将 SPI 时钟分频系数、数据位宽、命令序列定义为参数,便于复用。
- 带宽提升:实现 Quad SPI 模式(使用 IO2、IO3 引脚),将吞吐率提升至 100 Mbps。
- 跨平台移植:将 RTL 适配到 Altera/Intel 平台,修改约束文件与时钟原语。
- 加入断言:在测试平台中添加 SVA 断言,自动检查 SPI 时序是否违反协议。
- 覆盖分析:使用仿真工具收集状态机分支覆盖率,确保所有命令路径被测试。
- 形式验证:使用 JasperGold 验证状态机是否永远不会进入非法状态。
参考与信息来源
- Winbond W25Q128JV 数据手册(Rev. I,2023)
- Xilinx UG903: Vivado Design Suite User Guide - Using Constraints
- Xilinx UG949: Vivado Design Suite User Guide - Methodology
- SPI 协议规范(Motorola, 2003)



