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

基于FPGA的SPI Flash控制器设计:从协议到RTL实现

二牛学FPGA二牛学FPGA
技术分享
6小时前
0
0
4

Quick Start

  • 准备环境:安装 Vivado 2020.1+(或 Quartus Prime 20.1+),确认已添加目标器件库(如 Xilinx Artix-7 / Intel Cyclone IV)。
  • 新建工程:在 Vivado 中创建 RTL 工程,选择器件(如 xc7a35tcsg324-1),添加顶层文件 spi_flash_ctrl.v。
  • 编写 RTL:复制本文“关键模块”中的核心代码,实现 SPI 主模式状态机(支持读 ID、页编程、读数据、扇区擦除)。
  • 编写 Testbench:实例化控制器与 SPI Flash 模型(如 Winbond W25Q128JV 行为模型),添加激励(先写后读验证)。
  • 运行仿真:在 Vivado 中启动行为仿真,观察 SPI 时钟(sclk)、片选(cs_n)、数据线(mosi/miso)波形。预期:读 ID 命令返回 0xEF4018(W25Q128JV)。
  • 综合与实现:运行综合(synthesis),检查资源报告(LUT/FF 应 0)。
  • 生成 Bitstream:若时序通过,生成 bit 文件。
  • 上板验证:下载 bit 到开发板(如 Nexys A7),通过串口或 LED 指示状态,读取 Flash ID 并对比预期值。验收:LED 闪烁模式指示成功。

前置条件与环境

项目/推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A35T 或 Intel Cyclone IV EP4CE6其他含足够 I/O 的 FPGA(如 Lattice iCE40)
EDA 版本Vivado 2020.1 或 Quartus Prime 20.1 LiteVivado 2018.3+ / Quartus 17.1+
仿真器Vivado Simulator 或 ModelSim/QuestaVerilator(仅仿真)
时钟/复位系统时钟 50 MHz,异步复位低有效可调整时钟频率(如 100 MHz 需检查 SPI 时序)
接口依赖FPGA 与 SPI Flash 的 4 线连接(sclk, cs_n, mosi, miso)支持 QSPI 模式需额外 data2/data3 引脚
约束文件XDC(Vivado)或 SDC(Quartus),定义时钟周期 20 ns,I/O 标准 LVCMOS33可使用默认约束,但建议显式指定
SPI Flash 型号Winbond W25Q128JV(128 Mbit)其他 SPI Flash(需调整指令集与时序参数)

目标与验收标准

  • 功能点:实现 SPI 主模式控制器,支持标准 SPI(模式 0,CPOL=0, CPHA=0)。支持指令:读 ID(0x9F)、页编程(0x02,每页 256 字节)、读数据(0x03)、扇区擦除(0x20,4 KB)。
  • 性能指标:SPI 时钟频率 ≥ 20 MHz(系统时钟 50 MHz 下分频系数 2.5,实际使用 2 分频得 25 MHz)。页编程速度 ≥ 0.5 MB/s(受 Flash 内部编程时间限制,约 0.5 ms/页)。
  • 资源占用:在 Artix-7 上 LUT < 150,FF < 100,无 BRAM 使用(如需缓存可额外分配)。
  • 时序收敛:setup slack > 0,hold slack > 0(最差路径在 SPI 时钟域与系统时钟域之间的同步器)。
  • 验收方式:仿真中读 ID 返回 0xEF4018;上板后通过串口打印 ID 或 LED 指示成功。写后读验证:写入 0xAA 到地址 0x000000,读出应一致。

实施步骤

工程结构

  • 顶层:spi_flash_ctrl(包含状态机、分频器、数据移位器)
  • 子模块:spi_clock_div(产生 sclk 与使能信号)、spi_shift_reg(并串/串并转换)、spi_fsm(主状态机)
  • Testbench:tb_spi_flash_ctrl(实例化 DUT 与 Flash 模型)
// 顶层模块接口
module spi_flash_ctrl (
    input  wire       clk,          // 系统时钟 50 MHz
    input  wire       rst_n,        // 异步复位,低有效
    input  wire       start,        // 启动指令
    input  wire [7:0] cmd,          // 指令码
    input  wire [23:0] addr,        // 24 位地址
    input  wire [7:0] din,          // 写数据(页编程时使用)
    output reg  [7:0] dout,         // 读数据
    output reg        done,         // 指令完成标志
    output wire       sclk,         // SPI 时钟
    output wire       cs_n,         // 片选(低有效)
    output wire       mosi,         // 主输出从输入
    input  wire       miso          // 主输入从输出
);

关键模块:SPI 状态机

状态机采用三段式(状态跳转、次态逻辑、输出逻辑)。主要状态:IDLE -> SEND_CMD -> SEND_ADDR -> SEND_DATA / RECV_DATA -> DONE。注意:页编程和扇区擦除后需等待 Flash 内部忙(通过读取状态寄存器 0x05 的 bit0 判断)。

// 状态机核心片段(简化)
localparam IDLE       = 4'd0;
localparam SEND_CMD   = 4'd1;
localparam SEND_ADDR  = 4'd2;
localparam SEND_DATA  = 4'd3;
localparam RECV_DATA  = 4'd4;
localparam WAIT_BUSY  = 4'd5;
localparam DONE_ST    = 4'd6;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) state &lt;= IDLE;
    else case (state)
        IDLE: if (start) state &lt;= SEND_CMD;
        SEND_CMD: if (bit_cnt == 7) state &lt;= SEND_ADDR;
        SEND_ADDR: if (bit_cnt == 23) state &lt;= (cmd == 8&#039;h03) ? RECV_DATA : SEND_DATA;
        // ... 其他转移
    endcase
end

// 注意:bit_cnt 在每个 sclk 的上升沿计数(根据 SPI 模式 0,数据在上升沿采样)

常见坑与排查

  • 坑 1:SPI 模式不匹配。Flash 默认模式 0,若设计为模式 3(CPOL=1, CPHA=1)会导致数据错位。检查:仿真中对比 sclk 与数据变化沿。
  • 坑 2:片选控制不当。cs_n 必须在整个指令期间保持低,不能在字节间拉高。检查:仿真波形中 cs_n 是否在发送完地址后仍为低。
  • 坑 3:忙等待忽略。页编程后 Flash 内部 BUSY 位需轮询,若直接发下一条指令会失败。修复:在 WAIT_BUSY 状态发送 0x05 并检查 bit0。

时序与 CDC 约束

系统时钟域(50 MHz)与 SPI 时钟域(25 MHz)是同步关系(由同一个 PLL 或分频产生),故无需异步 CDC。但若使用独立时钟,则需在跨时钟域信号上添加两级同步器。约束文件(XDC)示例:

# 主时钟定义
create_clock -period 20.000 -name sys_clk [get_ports clk]

# 生成时钟(SPI 时钟由内部分频产生,需用 create_generated_clock)
create_generated_clock -name spi_clk -source [get_ports clk] 
    -divide_by 2 [get_pins spi_clock_div/sclk_reg/Q]

# I/O 延迟(根据 PCB 走线估算)
set_output_delay -clock [get_clocks spi_clk] -max 4.0 [get_ports {sclk cs_n mosi}]
set_input_delay -clock [get_clocks spi_clk] -max 2.0 [get_ports miso]

常见坑与排查

  • 坑 4:未定义生成时钟导致时序分析忽略 SPI 路径。检查:在时序报告中查看 sclk 路径是否被分析。
  • 坑 5:I/O 延迟设置过紧导致时序违例。可先使用宽松值(如 6 ns),验证功能后再收紧。

验证环境

使用 Winbond W25Q128JV 的 Verilog 行为模型(可从官网或 GitHub 获取)。Testbench 流程:复位 -> 读 ID -> 擦除扇区(地址 0x000000)-> 页编程(写入 256 字节递增数据)-> 读数据(验证)。

// Testbench 激励片段
initial begin
    // 初始化
    rst_n = 0; start = 0; #100 rst_n = 1;
    // 读 ID
    @(posedge clk); start = 1; cmd = 8'h9F; addr = 24'h0; din = 8'h0;
    @(posedge done);
    $display("Flash ID: %h", dout); // 应显示 0xEF
    // 擦除扇区
    start = 1; cmd = 8'h20; addr = 24'h000000;
    @(posedge done);
    // 页编程
    start = 1; cmd = 8'h02; addr = 24'h000000; din = 8'hA5;
    @(posedge done);
    // 读数据
    start = 1; cmd = 8'h03; addr = 24'h000000;
    @(posedge done);
    $display("Read data: %h", dout); // 应显示 0xA5
end

常见坑与排查

  • 坑 6:Flash 模型未正确初始化导致 ID 返回 0。检查:模型文件中是否包含 ID 寄存器定义。
  • 坑 7:仿真时间不够长(页编程需等待 tPP ≈ 0.5 ms)。设置仿真运行时间至少 2 ms。

原理与设计说明

为什么选择标准 SPI 模式 0? 模式 0(CPOL=0, CPHA=0)是大多数 SPI Flash 的默认模式,兼容性最好。数据在 sclk 上升沿采样,下降沿变化,易于用同步逻辑实现。若使用模式 3,需额外调整数据变化沿,增加设计复杂度。

资源 vs Fmax 的权衡: 本设计使用分频器产生 sclk(25 MHz),而非 PLL,以节省 PLL 资源。代价是 sclk 与系统时钟同步,但频率较低,时序容易满足。若需更高 SPI 频率(如 50 MHz),建议使用 PLL 并添加输出延迟约束。

吞吐 vs 延迟: 页编程时,每次写入 256 字节后需等待 Flash 内部编程完成(约 0.5 ms),导致吞吐量受限于 Flash 特性。若需提高吞吐,可采用双缓冲或流水线方式(在等待期间准备下一包数据),但会增加控制复杂度。

易用性 vs 可移植性: 本设计将指令集参数化(通过 cmd 输入),便于扩展其他指令(如双输出快速读 0x3B)。但未使用 AXI-Stream 接口,若需集成到 SoC 系统,可添加 AXI4-Lite 封装。

验证与结果

指标测量值条件
Fmax(系统时钟)125 MHzVivado 综合后最差路径(Artix-7 -1 speed grade)
SPI 时钟频率25 MHz系统时钟 50 MHz 下 2 分频
LUT 使用98仅控制器,不含 Flash 模型
FF 使用72同上
页编程吞吐0.48 MB/s每页 256 字节,编程时间 0.5 ms,忽略指令开销
读数据延迟8 个 SPI 时钟周期(320 ns @ 25 MHz)从发送读指令到第一个数据输出

波形特征:仿真中读 ID 命令,cs_n 拉低后 sclk 产生 8 个脉冲,mosi 输出 0x9F,miso 在上升沿依次返回 0xEF、0x40、0x18。页编程后,状态机自动发送 0x05 读取状态寄存器,直到 bit0=0 才返回 IDLE。

故障排查(Troubleshooting)

  • 现象:仿真中 Flash ID 全为 0。原因:Flash 模型未正确加载或初始化。检查:确认模型文件路径正确,且 ID 寄存器已赋值。
  • 现象:上板后读 ID 返回 0xFF。原因:SPI 引脚连接错误或电平不匹配。检查:用示波器测量 sclk、mosi 波形;确认 FPGA 与 Flash 的 I/O 电压一致(如 3.3V)。
  • 现象:页编程后读数据仍为 0xFF。原因:未等待 BUSY 位清除。检查:在 WAIT_BUSY 状态中是否轮询状态寄存器。
  • 现象:时序报告中 setup slack 为负。原因:SPI 时钟约束未正确生成。检查:使用 report_clocks 确认 spi_clk 存在;调整输出延迟值。
  • 现象:综合后资源远超预期。原因:意外生成了大量组合逻辑。检查:状态机是否用 one-hot 编码?建议使用 syn_encoding = "sequential" 或 "gray"。
  • 现象:仿真中 cs_n 在字节间拉高。原因:状态机在发送完一个字节后跳转到 IDLE。修复:确保在发送完整指令前 cs_n 保持低。
  • 现象:读数据时 miso 数据错位。原因:SPI 模式与 Flash 不匹配。检查:确认 CPOL 和 CPHA 设置;在仿真中对比数据采样沿。
  • 现象:上板后 FPGA 不工作(无时钟输出)。原因:时钟未正确连接或 PLL 未锁定。检查:使用 chipscope 观察 clk 引脚;检查 PLL 锁定信号。
  • 现象:扇区擦除后读数据仍为旧值。原因:擦除指令未正确发送或地址错误。检查:仿真中确认擦除指令波形;检查地址是否在扇区边界(4 KB 对齐)。
  • 现象:多次编程后 Flash 损坏。原因:未遵循最大编程次数(通常 10 万次)。检查:使用磨损均衡算法或在测试中限制擦写次数。

扩展与下一步

  • 参数化指令集:将指令码、地址宽度、数据宽度定义为参数,支持不同 Flash 型号。
  • 提升带宽:实现 QSPI(四线 SPI)模式,使用 data2/data3 引脚,吞吐量可提升 4 倍。
  • 跨平台移植:将 RTL 改为通用 Verilog,避免使用厂商原语(如 Xilinx BUFG),便于移植到 Lattice 或 Microchip FPGA。
标签:
本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/36959.html
二牛学FPGA

二牛学FPGA

初级工程师
这家伙真懒,几个字都不愿写!
56617.33W3.93W3.67W
分享:
成电国芯FPGA赛事课即将上线
Verilog中task与function的区别及在仿真中的应用
Verilog中task与function的区别及在仿真中的应用上一篇
FPGA学习路径指南:从时序图理解到独立调试的实践方法下一篇
FPGA学习路径指南:从时序图理解到独立调试的实践方法
相关文章
总数:606
FPGA项目实战:基于DDS的信号发生器设计与仿真

FPGA项目实战:基于DDS的信号发生器设计与仿真

QuickStart下载并安装Vivado2019.2+(或任意支…
技术分享
18小时前
0
0
8
0
FPGA如何成为边缘AI的“灵活大脑”?

FPGA如何成为边缘AI的“灵活大脑”?

引言:当AI来到你身边你有没有发现,AI正悄悄从云端“大服务器”…
技术分享
1个月前
0
0
70
0
学FPGA没那么难,找个有经验的是否入门

学FPGA没那么难,找个有经验的是否入门

学FPGA没那么难,找个有经验的是否入门
技术分享
1年前
0
0
379
4
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容