FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

基于FPGA的SPI Flash控制器:2026年高速读写时序设计

二牛学FPGA二牛学FPGA
技术分享
10小时前
0
0
3

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: 120LUT: 185, FF: 125Vivado 2024.2综合
时序裕量WNS=0.25 nsWNS=0.20 ns50 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延迟约束)。

扩展与下一步

集成DMA:设计AXI
  • 参数化:将指令、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
标签:
本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/40894.html
二牛学FPGA

二牛学FPGA

初级工程师
这家伙真懒,几个字都不愿写!
91919.27W3.99W3.67W
分享:
成电国芯FPGA赛事课即将上线
国产EDA工具链2026年进展:FPGA设计仿真验证上手指南
国产EDA工具链2026年进展:FPGA设计仿真验证上手指南上一篇
基于FPGA的SPI Flash控制器设计:高速读写时序实现指南下一篇
基于FPGA的SPI Flash控制器设计:高速读写时序实现指南
相关文章
总数:944
FIFO深度计算与异步FIFO设计实践指南

FIFO深度计算与异步FIFO设计实践指南

QuickStart准备Vivado2020.1+或Quartus…
技术分享
9天前
0
0
23
0
国产FPGA芯片在工业自动化中的最新应用案例:从选型到落地的技术指南

国产FPGA芯片在工业自动化中的最新应用案例:从选型到落地的技术指南

国产FPGA在工业自动化中的应用案例国产FPGA芯片在工业自动化领域展现…
技术分享
7天前
0
0
17
0
FPGA图像处理实战:基于Vivado HLS的实时边缘检测系统设计

FPGA图像处理实战:基于Vivado HLS的实时边缘检测系统设计

本指南旨在指导您完成一个基于VivadoHLS(高层次综合)的实时图像…
技术分享
16天前
0
0
43
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容