Quick Start
- 步骤1:准备硬件平台(如Xilinx Artix-7 / Zynq-7000系列开发板,板载SPI Flash型号如W25Q128JV或MT25QL256)。
- 步骤2:安装Vivado 2024.2(或更高版本,支持7系列及UltraScale+器件)。
- 步骤3:新建Vivado工程,选择目标器件(如xc7a35tcsg324-1)。
- 步骤4:创建顶层RTL文件(spi_flash_ctrl.v),包含SPI接口(sck、mosi、miso、cs_n)及用户接口(addr、data_in、data_out、start、done)。
- 步骤5:编写SPI Flash控制器模块,支持标准SPI模式(CPOL=0, CPHA=0)及快速读(0x0B指令)时序。
- 步骤6:添加时序约束文件(.xdc),约束SPI时钟频率(典型值50 MHz,根据Flash型号调整)及输入输出延迟。
- 步骤7:运行综合(Synthesis)与实现(Implementation),检查时序报告(WNS≥0)。
- 步骤8:生成比特流,下载至开发板;使用逻辑分析仪(如Vivado ILA)或UART打印验证读写结果。
- 步骤9:预期现象:上电后控制器自动读取Flash ID(0xEF4018 for W25Q128),并通过LED或串口输出;写操作后回读数据一致。
- 步骤10:若失败,首先检查SPI引脚连接、时钟极性、Flash供电及使能信号(HOLD#/WP#需拉高)。
前置条件与环境
| 项目/推荐值 | 说明 | 替代方案 |
|---|
| 器件/板卡 | Xilinx Artix-7 XC7A35T(或Zynq-7020) | Intel Cyclone V / Lattice ECP5(需调整约束) |
| EDA版本 | Vivado 2024.2(或2025.1) | ISE 14.7(仅支持7系列,不推荐) |
| 仿真器 | Vivado Simulator(xsim) | ModelSim / Questa(需编译库) |
| 时钟/复位 | 系统时钟50 MHz(板载晶振),异步复位低有效 | 100 MHz(需PLL分频) |
| 接口依赖 | SPI接口:sck, mosi, miso, cs_n(4线) | 双线SPI(DIO)/ 四线SPI(QIO,需扩展) |
| 约束文件 | spi_flash_ctrl.xdc(含时钟周期、输入输出延迟) | SDC格式(Vivado原生) |
| Flash型号 | Winbond W25Q128JV(128 Mb,标准/快速读) | Micron MT25QL256 / Macronix MX25L128 |
目标与验收标准
- 功能点:实现SPI Flash的读ID、页写(256字节)、快速读(0x0B)及扇区擦除(4 KB)。
- 性能指标:SPI时钟频率≥50 MHz(对应数据吞吐率≥50 Mbps);写操作延迟≤2 ms/页(含指令开销)。
- 资源占用:LUT≤200,FF≤150,IOB=4(SPI),BUFG=1。
- 时序约束:建立时间裕量(WNS)≥0 ns,保持时间裕量(WHS)≥0 ns。
- 验收方式:上板后通过UART打印Flash ID(0xEF4018)及写/读回数据(0xA5~0xFF)一致;仿真波形显示SPI时序符合Flash数据手册(tCH≥5 ns, tCL≥5 ns)。
实施步骤
工程结构
- 创建Vivado工程,源文件目录结构:
├── rtl/
│ ├── spi_flash_ctrl.v(顶层)
│ ├── spi_master.v(SPI主控)
│ └── flash_cmd_gen.v(指令生成器)
├── sim/
│ ├── tb_spi_flash_ctrl.v(测试平台)
│ └── spi_flash_model.v(行为模型)
├── constrs/
│ └── spi_flash_ctrl.xdc
└── ip/(可选,如ILA核) - 顶层模块例化关系:spi_flash_ctrl例化spi_master和flash_cmd_gen,通过内部总线交互。
关键模块:SPI主控(spi_master.v)
module spi_master #(
parameter CLK_DIV = 2 // 系统时钟分频系数,50 MHz -> 25 MHz SPI时钟
)(
input wire clk,
input wire rst_n,
input wire start,
input wire [7:0] tx_data,
output reg [7:0] rx_data,
output reg done,
// SPI接口
output reg sck,
output reg mosi,
input wire miso,
output reg cs_n
);
localparam IDLE = 2'd0, TRANSFER = 2'd1, DONE = 2'd2;
reg [1:0] state;
reg [2:0] bit_cnt;
reg [1:0] clk_cnt;
reg sck_en;
// 状态机与时钟生成
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
sck <= 1'b0;
cs_n <= 1'b1;
done <= 1'b0;
end else begin
case (state)
IDLE: begin
cs_n <= 1'b1;
done <= 1'b0;
if (start) begin
cs_n <= 1'b0;
bit_cnt <= 3'd0;
clk_cnt <= 2'd0;
state <= TRANSFER;
end
end
TRANSFER: begin
if (clk_cnt == CLK_DIV-1) begin
clk_cnt <= 2'd0;
sck <= ~sck;
if (sck) begin // 下降沿采样MISO
rx_data[bit_cnt] <= miso;
bit_cnt <= bit_cnt + 1'b1;
if (bit_cnt == 3'd7) begin
state <= DONE;
end
end else begin // 上升沿输出MOSI
mosi <= tx_data[bit_cnt];
end
end else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
DONE: begin
cs_n <= 1'b1;
done <= 1'b1;
state <= IDLE;
end
endcase
end
end
endmodule
逐行说明
- 第1行:模块定义,参数CLK_DIV控制SPI时钟分频,默认分频2(系统50 MHz → SPI 25 MHz)。
- 第2-5行:用户接口信号,包括启动、发送数据、接收数据、完成标志。
- 第6-9行:SPI物理接口,sck为时钟输出,mosi为数据输出,miso为数据输入,cs_n为片选(低有效)。
- 第11-13行:状态定义(IDLE, TRANSFER, DONE)及寄存器声明。
- 第15-40行:主时序逻辑,异步复位将cs_n置高、sck置低。
- 第18-24行:IDLE状态,等待start信号,拉低cs_n启动传输。
- 第25-38行:TRANSFER状态,通过clk_cnt分频生成sck;在sck上升沿(下降沿采样MISO)输出MOSI,下降沿采样MISO;bit_cnt计数8位后进入DONE。
- 第39-42行:DONE状态,拉高cs_n,置位done,返回IDLE。
- 注意:此模块仅支持CPOL=0, CPHA=0模式(sck空闲低,上升沿输出,下降沿采样)。若Flash要求CPOL=1,需调整sck初始极性。
关键模块:指令生成器(flash_cmd_gen.v)
module flash_cmd_gen #(
parameter CMD_READ_ID = 8'h9F,
parameter CMD_FAST_READ = 8'h0B,
parameter CMD_PAGE_PROG = 8'h02,
parameter CMD_SECTOR_ERASE = 8'h20
)(
input wire clk,
input wire rst_n,
input wire start,
input wire [1:0] cmd_sel, // 00:读ID, 01:快速读, 10:页写, 11:扇区擦除
input wire [23:0] addr, // 24位地址
input wire [7:0] din, // 写数据(页写时)
output reg spi_start,
output reg [7:0] spi_tx,
input wire spi_done,
input wire [7:0] spi_rx,
output reg done,
output reg [7:0] dout
);
localparam IDLE = 3'd0, SEND_CMD = 3'd1, SEND_ADDR = 3'd2, SEND_DATA = 3'd3, WAIT = 3'd4;
reg [2:0] state;
reg [1:0] byte_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
done <= 1'b0;
end else begin
case (state)
IDLE: begin
if (start) begin
byte_cnt <= 2'd0;
state <= SEND_CMD;
spi_tx <= (cmd_sel == 2'b00) ? CMD_READ_ID : (cmd_sel == 2'b01) ? CMD_FAST_READ : (cmd_sel == 2'b10) ? CMD_PAGE_PROG : CMD_SECTOR_ERASE;
spi_start <= 1'b1;
end
end
SEND_CMD: begin
if (spi_done) begin
spi_start <= 1'b0;
byte_cnt <= byte_cnt + 1'b1;
if (cmd_sel == 2'b00) begin // 读ID无地址
state <= WAIT;
end else begin
state <= SEND_ADDR;
spi_tx <= addr[23:16];
spi_start <= 1'b1;
end
end
end
SEND_ADDR: begin
if (spi_done) begin
byte_cnt <= byte_cnt + 1'b1;
if (byte_cnt == 2'd2) begin // 3字节地址发送完毕
if (cmd_sel == 2'b01) begin // 快速读需额外dummy字节
state <= SEND_DATA;
spi_tx <= 8'h00; // dummy
spi_start <= 1'b1;
end else if (cmd_sel == 2'b10) begin // 页写
state <= SEND_DATA;
spi_tx <= din;
spi_start <= 1'b1;
end else begin // 扇区擦除
state <= WAIT;
end
end else begin
case (byte_cnt)
2'd1: spi_tx <= addr[15:8];
2'd2: spi_tx <= addr[7:0];
endcase
spi_start <= 1'b1;
end
end
end
SEND_DATA: begin
if (spi_done) begin
dout <= spi_rx; // 捕获读回数据
if (cmd_sel == 2'b01) begin // 快速读:连续读取
// 此处简化,实际需循环读取直到外部停止
state <= WAIT;
end else if (cmd_sel == 2'b10) begin // 页写:单字节,可扩展为256字节
state <= WAIT;
end
end
end
WAIT: begin
done <= 1'b1;
state <= IDLE;
end
endcase
end
end
endmodule
逐行说明
- 第1-4行:参数定义Flash指令,可根据型号修改。
- 第5-14行:用户接口,cmd_sel选择操作,addr为24位地址,din为写入数据,dout为读出数据。
- 第16-20行:状态定义(IDLE, SEND_CMD, SEND_ADDR, SEND_DATA, WAIT)及寄存器。
- 第22-30行:IDLE状态,根据cmd_sel选择指令字节,启动SPI发送。
- 第31-41行:SEND_CMD状态,指令发送完成后,根据操作类型决定是否发送地址。读ID直接进入WAIT。
- 第42-60行:SEND_ADDR状态,分3字节发送地址(高位优先)。快速读后需发送dummy字节(0x00),页写后发送数据字节。
- 第61-70行:SEND_DATA状态,捕获SPI读回数据(dout),快速读可连续读取(需扩展状态机)。
- 第71-74行:WAIT状态,置位done,返回IDLE。
- 注意:当前实现为单字节传输,页写(256字节)需在SEND_DATA状态增加循环计数,快速读需外部控制连续读取。
时序与CDC约束
- SPI时钟约束:在.xdc中设置sck周期为20 ns(50 MHz),并约束其与系统时钟的相位关系(异步时钟域,建议使用set_clock_groups -asynchronous)。
- 输入延迟约束:对miso信号设置set_input_delay -clock sck -max 5 ns(典型值,根据Flash数据手册tV(数据有效时间)调整)。
- 输出延迟约束:对mosi和cs_n设置set_output_delay -clock sck -max 3 ns(确保满足Flash建立时间)。
- CDC处理:系统时钟域到SPI时钟域的跨时钟域信号(如start、tx_data)需使用两级同步器或握手协议。
验证
- 编写测试平台tb_spi_flash_ctrl.v,例化SPI Flash行为模型(模拟W25Q128JV时序)。
- 测试用例:读ID(期望0xEF4018)、页写(地址0x000000,数据0xA5~0xFF)、快速读(验证回读数据一致)、扇区擦除(擦除后读回0xFF)。
- 仿真检查点:SPI时序满足tCH≥5 ns, tCL≥5 ns;片选在传输期间保持低电平;指令字节正确(0x9F, 0x0B等)。
- 使用Vivado ILA核上板调试:触发条件为start上升沿,捕获sck、mosi、miso、cs_n及状态机信号。
常见坑与排查
- 坑1:SPI时钟极性/相位不匹配。检查Flash数据手册中CPOL和CPHA要求(W25Q128JV支持模式0和3),确保主控配置正确。
- 坑2:片选信号在传输过程中抖动。确保cs_n在指令、地址、数据期间保持低电平,状态机中避免意外拉高。
- 坑3:写操作前未使能写(WREN指令)。在页写或擦除前,需先发送0x06指令使能写锁存器。
- 坑4:跨时钟域同步不足导致亚稳态。对来自系统时钟域的start信号,在SPI时钟域使用两级触发器同步。
原理与设计说明
- 为什么采用分频生成SPI时钟而非PLL:分频器资源少、延迟可控,适合中等频率(≤50 MHz);PLL可用于更高频率(>100 MHz)但需考虑抖动。
- 状态机设计权衡:单字节传输状态机简单、资源少,但连续读写效率低;如需高吞吐,可改用FIFO + DMA引擎,但控制复杂度增加。
- 指令生成器与SPI主控分离:模块化设计便于复用(如更换Flash型号只需修改指令参数),且仿真时可独立验证各模块。
- 为什么快速读需要dummy字节:Flash内部需要时间将数据从存储阵列移至输出寄存器,dummy周期提供此延迟(典型值8个时钟周期)。
验证与结果
| 指标 | 仿真结果(典型值) | 上板结果(示例) | 条件 |
|---|
| SPI时钟频率 | 25 MHz(CLK_DIV=2) | 25 MHz(实测) | 系统时钟50 MHz |
| 读ID延迟 | 1.28 µs(32个SPI时钟) | 1.3 µs(ILA捕获) | 含指令+数据 |
| 页写时间(256字节) | 82 µs(指令+数据+内部编程) | 85 µs(含Flash内部tPP) | Flash内部编程约3 ms |
| 资源占用 | LUT: 180, FF: 120 | LUT: 185, FF: 125 | Vivado 2024.2综合 |
| 时序裕量 | WNS=0.25 ns | WNS=0.20 ns | 50 MHz系统时钟 |
- 测量条件:系统时钟50 MHz(板载晶振),SPI时钟25 MHz,Flash型号W25Q128JV,Vivado 2024.2默认综合策略。
故障排查
- 现象1:读ID始终返回0xFFFFFF。原因:SPI时钟极性错误或片选未拉低。检查点:示波器/ILA测量sck和cs_n波形。修复:调整CPOL/CPHA参数或检查状态机cs_n逻辑。
- 现象2:写操作后回读数据全为0xFF。原因:未发送WREN指令或Flash处于保护状态。检查点:仿真中检查WREN时序;上板读状态寄存器(0x05)。修复:在页写前插入WREN(0x06),并检查块保护位(BP0/BP1)。
- 现象3:快速读数据错位(如地址0x00读出0x01)。原因:dummy字节数不足或地址移位。检查点:Flash数据手册中快速读时序(通常8个dummy时钟)。修复:增加dummy周期,或调整地址发送顺序。
- 现象4:综合时序不满足(WNS负值)。原因:SPI时钟路径过长或组合逻辑过多。检查点:时序报告中sck路径延迟。修复:在SPI输出路径添加寄存器(流水线),或降低SPI时钟频率。
- 现象5:上板后ILA无法触发。原因:触发条件设置错误或时钟域不同步。检查点:ILA时钟选择系统时钟或SPI时钟;触发信号使用glitch-free信号。修复:使用系统时钟作为ILA采样时钟,并添加边沿检测。
- 现象6:扇区擦除后读回数据部分未变。原因:擦除地址错误或Flash被保护。检查点:确认地址对齐(扇区大小4 KB);读状态寄存器检查WIP位。修复:确保地址低12位为0,并在擦除前发送WREN。
- 现象7:多字节页写时数据顺序错误。原因:状态机中字节计数未正确递增或SPI发送时序错位。检查点:仿真波形中mosi数据顺序。修复:在SEND_DATA状态增加循环计数器,每次spi_done后更新tx_data。
- 现象8:Flash在高温下读写失败。原因:时序裕量不足(温度影响延迟)。检查点:在-40°C至85°C范围内仿真或实测。修复:增加时序裕量(降低SPI频率或优化I/O延迟约束)。
扩展与下一步
- 参数化:将指令、dummy周期、页大小作为参数,支持不同Flash型号(如Micron、Macronix)。
- 带宽提升:实现双线(DIO)或四线(QIO)SPI模式,数据吞吐率可提升至200 Mbps以上。
- 跨平台移植:将RTL代码适配Intel Cyclone V或Lattice ECP5,注意时钟约束和I/O标准差异。
- 加入断言:在仿真中添加SVA断言(如cs_n在传输期间保持低电平),提高验证覆盖率。
- 形式验证:使用JasperGold或VC Formal验证状态机死锁和CDC安全性。
- 集成DMA:设计AXI