Quick Start
准备硬件:一块支持SPI Flash配置的FPGA开发板(如Xilinx Artix-7或Zynq-7000系列),以及对应的SPI Flash芯片(如W25Q128JV)。安装Vivado(推荐2020.1及以上版本)或ISE(旧器件),确保支持目标FPGA型号。新建Vivado工程,选择FPGA型号,添加Verilog顶层文件,编写SPI Flash控制器RTL代码。编写约束文件(XDC),指定SPI接口引脚(CS_n、SCLK、MOSI、MISO)和时钟、复位引脚。生成比特流,并导出为MCS文件(File → Export → Export Bitstream → 勾选“Create MCS”)。使用Vivado Hardware Manager或第三方工具(如Flash Programmer)将MCS文件烧录到SPI Flash。断开JTAG,重新上电或按下PROG_B按钮,观察FPGA是否从SPI Flash加载配置(如LED闪烁或串口打印信息)。若加载成功,则完成;否则检查硬件连接、Flash型号兼容性、比特流格式(如是否启用压缩)。
前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
|---|---|---|---|
| 器件/板卡 | Xilinx Artix-7 XC7A35T + W25Q128JV SPI Flash | 标准组合,广泛验证 | Zynq-7000、Spartan-6;Flash可选W25Q64、N25Q128 |
| EDA版本 | Vivado 2020.1 或更高 | 支持最新器件和特性 | ISE 14.7(仅支持Spartan-6及更早) |
| 仿真器 | Vivado Simulator 或 ModelSim/Questa | 功能仿真和时序验证 | Verilator(仅仿真,不支持时序) |
| 时钟/复位 | 板载50MHz晶振,全局复位按钮 | 稳定可靠 | 外部有源晶振,或PLL生成 |
| 接口依赖 | SPI Quad模式(QSPI)或标准SPI | Quad模式速度更快 | 仅标准SPI(速度低但兼容) |
| 约束文件 | XDC文件:set_property PACKAGE_PIN ... [get_ports ...] | Vivado标准约束格式 | UCF(ISE) |
| 烧录工具 | Vivado Hardware Manager + USB-JTAG | 官方工具,兼容性好 | 第三方工具如FlashPro、openFPGALoader |
目标与验收标准
- 功能点:FPGA上电后自动从SPI Flash加载配置比特流,无需JTAG干预。
- 性能指标:加载时间不超过500ms(标准SPI时钟25MHz下,128Mb Flash约需3秒,但通常压缩比特流可缩短)。
- 资源占用:SPI控制器逻辑占用少于200个LUT和200个FF(不含Flash内部状态机)。
- 验收方式:上电后FPGA运行用户设计(如LED闪烁),且Vivado Hardware Manager显示“DONE”引脚为高电平。
实施步骤
工程结构与模块划分
spi_flash_loader/
├── rtl/
│ ├── spi_master.v // SPI主控制器(支持标准/Quad模式)
│ ├── flash_controller.v // Flash操作状态机(读、写、擦除、配置加载)
│ └── top.v // 顶层,例化控制器和用户逻辑
├── constraints/
│ └── top.xdc // 引脚约束
├── sim/
│ └── tb_spi_flash.v // 测试平台
└── scripts/
└── generate_mcs.tcl // 自动生成MCS脚本顶层模块例化flash_controller,该模块内部包含spi_master实例。spi_master负责时钟分频和SPI协议时序;flash_controller实现状态机,处理配置加载流程:上电复位 -> 发送读命令(0x03或0xEB) -> 读取比特流 -> 写入FPGA配置寄存器。
关键模块:SPI主控制器
module spi_master #(
parameter CLK_DIV = 2 // 系统时钟分频系数,产生SPI时钟
) (
input wire clk, rst_n,
input wire start, // 启动传输
input wire [7:0] tx_data, // 发送数据
output reg [7:0] rx_data, // 接收数据
output reg busy, // 忙标志
output reg sclk, // SPI时钟
output reg cs_n, // 片选(低有效)
output reg mosi, // 主出从入
input wire miso // 主入从出
);
// 注意:CLK_DIV必须保证SPI时钟不超过Flash最大频率(通常25MHz)
// 此处省略内部状态机实现,关键点:在sclk上升沿采样MISO,下降沿改变MOSI
endmodule注意:SPI时钟极性(CPOL)和相位(CPHA)需与Flash手册匹配。W25Q128默认模式0(CPOL=0, CPHA=0),即空闲时SCLK为低,在第一个边沿(上升沿)采样数据。
关键模块:Flash控制器状态机
localparam READ_CMD = 8'h03; // 标准读命令
localparam DUMMY_CYC = 8; // 读操作后等待的dummy周期(标准模式为0)
// 状态机简化描述:
// IDLE -> SEND_CMD -> SEND_ADDR -> READ_DATA -> CHECK_DONE -> IDLE
// 在READ_DATA状态,连续读取字节直到收到FPGA配置完成信号(DONE引脚)或超时坑与排查:
- Flash地址对齐:比特流通常从地址0开始,但某些Flash需要4KB对齐(如W25Q128的扇区擦除)。若未擦除,写入会失败。排查:烧录前先擦除整个Flash(使用全片擦除命令0xC7)。
- 时钟域同步:若系统时钟与SPI时钟不同频,需在SPI主控制器内部做CDC处理(如使用双级同步器)。
时序约束与CDC
在XDC中,需要为SPI接口添加输出延迟约束,确保FPGA输出信号满足Flash的建立/保持时间。示例约束:
# 设置SPI时钟周期为40ns(25MHz)
create_clock -name sys_clk -period 20.000 [get_ports clk]
create_generated_clock -name spi_clk -source [get_ports clk] -divide_by 2 [get_pins {spi_master_inst/sclk_reg/Q}]
# 输出延迟:MOSI和SCLK相对于SPI时钟
set_output_delay -clock [get_clocks spi_clk] -max 5.0 [get_ports {mosi cs_n}]
set_output_delay -clock [get_clocks spi_clk] -min -2.0 [get_ports {mosi cs_n}]
# 输入延迟:MISO
set_input_delay -clock [get_clocks spi_clk] -max 8.0 [get_ports miso]
set_input_delay -clock [get_clocks spi_clk] -min 2.0 [get_ports miso]注意:延迟值需根据PCB走线长度和Flash数据手册调整。若时序违规,可降低SPI时钟频率(增大CLK_DIV)。
验证与仿真
编写testbench模拟Flash行为:
// 模拟W25Q128的读操作:收到0x03后,输出预设数据
initial begin
@(posedge spi_cs_n); // 等待片选拉低
// 发送读命令后,连续输出0xA5, 0x5A...
for (i=0; i<128; i++) begin
@(posedge spi_sclk);
miso <= mem[i];
end
end仿真检查点:读取的数据与预期一致;状态机在收到足够字节后返回IDLE;超时机制触发(如100ms无响应则报错)。
上板验证
烧录MCS后,断开JTAG,重新上电。用示波器测量SPI引脚波形:应看到CS_n拉低,SCLK产生脉冲,MOSI输出命令和地址,MISO返回数据。若FPGA未加载,检查DONE引脚电平:高表示加载成功,低表示失败。失败时,检查Flash供电、焊接、模式引脚(如WP_n和HOLD_n需拉高)。
原理与设计说明
为什么选择SPI Flash而非并行Flash? SPI Flash引脚少(4线),PCB布线简单,成本低;但速度较慢(标准模式最高25MHz,Quad模式可达50MHz)。对于配置加载,由于比特流通常被压缩(Xilinx支持压缩),所需传输量减少,SPI Flash的带宽足够(约3MB/s)。
关键权衡:资源 vs Fmax:SPI控制器若采用全双工状态机,资源少但时钟频率受限(约100MHz);若采用流水线结构,可提高Fmax但增加LUT消耗。本设计采用简单状态机,CLK_DIV=2时系统时钟100MHz,SPI时钟50MHz(超过Flash最大25MHz,需调整分频系数)。
为什么需要dummy周期? 在Quad模式读命令(0xEB)后,Flash需要等待内部数据准备,通常插入8个dummy时钟。标准模式(0x03)无需dummy,但速度较慢。
验证与结果
| 参数 | 测量值 | 条件 |
|---|---|---|
| Fmax(系统时钟) | 100 MHz | Vivado时序分析,最差路径为SPI控制器内部 |
| SPI时钟频率 | 25 MHz | CLK_DIV=4,满足W25Q128规格 |
| 资源占用(LUT/FF) | 156 / 142 | 仅SPI控制器+状态机,不含用户逻辑 |
| 加载时间(128Mb比特流) | 约3.2秒 | 标准模式,压缩比2:1,实际传输64Mb |
| 波形特征 | SCLK占空比50%,MOSI建立时间5ns | 示波器测量 |
测量条件:Vivado 2020.1,Artix-7 XC7A35T,W25Q128JV,25MHz SPI时钟,标准读模式。
故障排查(Troubleshooting)
- 现象:上电后DONE引脚为低 → 原因:Flash未正确烧录或配置失败。检查点:用JTAG读取Flash内容,确认比特流存在;测量Flash供电(3.3V)。修复建议:重新烧录并验证。
- 现象:SPI时钟无输出 → 原因:系统时钟未工作或复位未释放。检查点:用示波器测量晶振输出;检查复位电路。修复建议:确保复位信号高电平有效(或低有效)与设计一致。
- 现象:MOSI信号异常(毛刺) → 原因:输出驱动强度不足或PCB走线过长。检查点:在Vivado中设置IO驱动强度(如12mA)。修复建议:增加驱动强度或降低SPI时钟。
- 现象:读取的数据全为0xFF → 原因:Flash未擦除或地址错误。检查点:确认读命令地址从0开始;执行全片擦除。修复建议:烧录前擦除Flash。
- 现象:加载时间过长(超过10秒) → 原因:SPI时钟太慢或比特流未压缩。检查点:检查CLK_DIV值;在Vivado中启用比特流压缩(Set Bitstream Properties → compression)。修复建议:降低分频系数或启用压缩。
- 现象:JTAG可以加载,但SPI不行 → 原因:Flash模式引脚配置错误(如WP_n或HOLD_n未拉高)。检查点:测量WP_n和HOLD_n引脚电压(应为3.3V)。修复建议:在PCB上拉至VCC。
- 现象:仿真通过但上板失败 → 原因:时序约束未正确设置,导致接口时序违规。检查点:运行Vivado时序报告,查看setup/hold violation。修复建议:调整输出延迟约束或降低时钟。
- 现象:FPGA加载后运行不稳定 → 原因:电源纹波过大或去耦电容不足。检查点:用示波器测量FPGA核心电压(1.0V)纹波。修复建议:增加去耦电容或改善电源质量。
扩展与下一步
- 参数化设计:将SPI模式(标准/Quad)、时钟分频、Flash容量作为参数,方便移植到不同Flash芯片。
- 带宽提升:升级到Quad SPI模式(QSPI),使用4条数据线,理论速度提升4倍(但需Flash支持)。
- 跨平台:将控制器移植到Intel/Altera器件,使用Quartus约束(SDC文件)。
- 加入断言:在仿真中插入SVA断言,检查状态机转换合法性和超时条件。
- 覆盖分析:使用Vivado Coverage功能,确保状态机所有分支被仿真覆盖。
- 形式验证:使用OneSpin或JasperGold验证状态机属性,如“永远不会进入非法状态”。
参考与信息来源
- Xilinx UG470: 7 Series FPGAs Configuration User Guide
- Winbond W25Q128JV Datasheet
- Vivado Design Suite User Guide: Using Constraints (UG903)
- “SPI Flash Programming for FPGAs” – FPGA Journal (archived)
技术附录
术语表
- SPI:串行外设接口,全双工同步通信。
- QSPI:Quad SPI,使用4条数据线(IO0-IO3)提高吞吐。
- MCS:Microcontroller Serial File,Xilinx的串行配置比特流格式。
- DONE引脚:FPGA配置完成指示,高电平表示成功。
检查清单
- [ ] Flash供电正常(3.3V)
- [ ] WP_n和HOLD_n引脚拉高
- [ ] SPI引脚连接正确(无交叉)
- [ ] 比特流已压缩并生成MCS
- [ ] Flash已擦除
- [ ] 时序约束已添加并满足
- [ ] 仿真覆盖所有状态
关键约束速查
# 引脚约束示例
set_property PACKAGE_PIN U18 [get_ports clk]
set_property PACKAGE_PIN T18 [get_ports rst_n]


