FPGA远程升级实战:AXI Quad SPI驱动N25Q128与MultiBoot双区设计

发布时间:2026/10/5 6:20:01
FPGA远程升级实战:AXI Quad SPI驱动N25Q128与MultiBoot双区设计 这两年做设备远程维护FPGA升级始终是个绕不开的话题。我手上一块板卡用的是 Artix-7 N25Q128 SPI Flash 组合后端业务希望设备现场不动维护人员通过网络把新版本 bitstream 推给设备设备自己完成 FPGA 镜像切换。听起来不复杂实际做起来却发现光是把 AXI Quad SPI IP 和 N25Q128 调通就够喝一壶的。这篇文章把整个流程、关键决策、踩过的坑一起写出来给同样做 Xilinx 平台远程升级的朋友一个可以直接参考的路线图。文章会覆盖五个层面为什么远程升级离不开 SPI Flash、MultiBoot 双区设计、AXI Quad SPI IP 的配置细节、N25Q128 的驱动实现、以及完整升级动作中的校验与回退。全程按实际项目节奏走相关代码和参数都是实测下来能用的版本。1. 整体方案设计远程升级为什么绕不开 SPI Flash 与双镜像分区很多人第一次接触远程升级时第一反应是“能不能像单片机一样通过以太网把固件写入 Flash”。这个思路本身没错但 FPGA 的启动架构跟 MCU 不一样方案设计必须先搞清楚“FPGA 上电后到底从哪里取代码”。1.1 FPGA 启动架构SRAM 工艺带来的天然“失忆”问题FPGA 内部基于 SRAM 存储配置数据掉电后配置全部丢失所以每次上电都要从外部非易失存储介质加载 bitstream。常见的加载接口包括 SPI Flash、QSPI Flash、BPI 并行 Flash、SD 卡、JTAG 等其中 SPI Flash 因为引脚少、容量合适、成本低成了板级设计的首选。加载过程是这样的FPGA 上电后配置逻辑根据模式引脚 M[2:0] 的拨码状态决定从哪个接口启动。如果是 SPI 模式配置逻辑会自动产生 SCK 和 CS把挂在 SPI 总线上的 Flash 内容读进来完成初始化。这个过程不需要用户逻辑参与属于芯片内部硬件行为。理解了这一点“远程升级”的本质就清楚了。FPGA 正在运行旧版本逻辑时用户逻辑比如 MicroBlaze 软核或状态机先通过网络或总线拿到新镜像数据然后把数据写入 SPI Flash 的另一个区域。全部写完并通过校验后再通过特殊指令触发 FPGA 重新配置让配置逻辑从新区域加载。所以远程升级不是简单“烧 Flash”而是“换启动源”。1.2 MultiBoot 双区架构Golden 镜像与 Update 镜像怎么分Xilinx 7 系列 FPGA 原生支持 MultiBoot 功能。芯片内部有一段固化逻辑能从用户指定的 Flash 地址读取 bitstream 头部如果发现头部无效、配置 CRC 错误或者配置看门狗超时就会自动回退到地址 0 处的备份镜像。这个机制对远程升级来说非常宝贵等于芯片硬件层面就给了我们“免砖”保险。因此分区设计上一定要把 Flash 分成至少两个镜像区Golden 镜像区放在 Flash 起始地址比如 0x0000000。这个区域的镜像出厂时烧录是最后一个保底版本永远不通过远程升级覆盖。除非专门做“引导区升级”的重维护流程否则普通升级只读不动。Update 镜像区放在 Flash 高地址比如 0x1000000。每次远程升级都写入这个区域全部校验通过后才切换启动指针。实际操作中还要考虑一个问题Update 镜像本身如果升级到一半断电写入一半的数据可能让 MultiBoot 启动失败。但因为启动指针还没有切换系统仍然从 Golden 启动这是双区架构最大的价值。后面升级流程设计里这个理念贯穿始终。1.3 Flash 分区规划地址对齐、版本记录、CRC 存储以 N25Q128 举例容量 128Mbit换算下来 16MB。一个 Artix-7 的 bitstream 通常在 2MB 到 7MB 之间具体取决于逻辑规模和配置模式。我一般分区这样安排起始地址大小用途0x00000001MBGolden 镜像0x10000014MBUpdate 镜像区0xF000000.5MB升级状态与版本标记区0xF800000.5MB保留区存放设备信息、校准数据等需要注意Flash 扇区擦除最小单位一般是 4KB所以分区起始地址必须按 4KB 对齐。Update 镜像的起始地址还要注意 WBSTARWarm Boot Start Address Register的地址构造要求建议也按 4KB 对齐省去很多麻烦。另外版本记录不要放在镜像头部附近尽量单独划分扇区。因为镜像头部是 FPGA 配置逻辑直接读取的任何意外破坏都可能导致启动失败。版本区单独放可以在用户逻辑启动后再读取安全性更高。2. AXI Quad SPI IP 的配置与连线几个决定成败的细节IP 配置看起来就是 Vivado 里点几下鼠标但几个参数选错后面读写 Flash 会非常被动。这里详细说明每个关键选项的取舍。2.1 为什么用 AXI Quad SPI IP而不是自己写 SPI 控制器可能有人会问FPGA 内部逻辑灵活用 GPIO 模拟 SPI 时序不就行了吗确实可以而且对速度要求在 1MHz 以下时GPIO 模拟完全够用。但远程升级往往涉及整片 Flash 擦写数据量几十 MB模拟 SPI 的 CPU 占用非常高还要处理各种边沿时序可靠性很难保证。AXI Quad SPI IP 是 Xilinx 官方提供的 SPI 控制器内部带收发 FIFO、状态寄存器、中断支持还能直接通过 AXI4-Lite 接口被 MicroBlaze 或 Zynq ARM 核访问。实际测试下来SPI 时钟跑到 40MHz 到 50MHz 很稳定读写大块数据比 GPIO 模拟快一个数量级。更重要的一点这个 IP 支持 Standard SPI、Dual SPI、Quad SPI 三种模式。N25Q128 本身支持 Quad Output Fast Read用四根数据线同时读读回校验速度会快很多。这在量产升级场景中很有意义能明显缩短设备离线窗口。2.2 Vivado 里参数怎么选模式、FIFO、时钟分频在 Vivado IP Catalog 中搜索 AXI Quad SPI双击配置。我建议重点关注以下参数参数名推荐取值说明ModeQuad SPI直接用四线模式兼容标准模式FIFO Size32 或 64读写大块数据时减少 AXI 交互次数Transaction Width8 bit最通用Flash 指令处理方便SPI Clock Ratio8 或 16根据 AXI 时钟频率换算Master Mode勾选IP 作为 SPI 主机CPOL / CPHAMode 0 或 Mode 3N25Q128 两种都支持推荐 Mode 0SPI Clock Ratio 的含义是 AXI 总线时钟与 SCK 的分频比。如果 AXI 时钟是 100MHzRatio 填 8SCK 就是 12.5MHz。这个频率对绝大多数 SPI Flash 都足够属于“稳定优先”的配置。补一个容易忽略的点FIFO 深度太浅时连续写入大块数据会频繁触发 FIFO Full影响吞吐。但 FIFO 太深又会占更多 BRAM所以常规项目用 32 就够调试阶段可以先 64后面再优化。2.3 引脚约束和 PCB 上的高频信号注意点N25Q128 的引脚里CS#、SCK、DQ0、DQ1 是标准 SPI 必需引脚DQ2 对应 WP#写保护DQ3 对应 HOLD#保持。如果只用标准 SPI 模式DQ2 和 DQ3 可以不上拉也不连 FPGA。但既然用了 Quad 模式这两个引脚必须接到 FPGA 的普通 IO 上因为 Quad 读数据时它们要作为 DQ2/DQ3 数据线参与传输。PCB 设计时SCK 走线要短DQ0-DQ3 尽量等长避免高速模式下信号偏移。Flash 电源引脚旁边至少放一组 0.1uF 和 10uF 去耦电容大面积擦写瞬间电流很大电压跌落会导致编程失败。还有一个 1.8V/3.3V 的坑。N25Q128 有 1.8V 版本和 3.3V 版本采购时必须确认后缀。如果 FPGA 对应 bank 电压是 3.3V却贴了 1.8V 的 Flash上电后读 ID 通常会有问题轻则读不出来重则长期工作不稳定。这类问题现象隐蔽排查起来非常熬人。3. 驱动实现与读写 N25Q128 的关键流程IP 配置完成后真正跟 Flash 打交道的是驱动代码。这一节把 AXI Quad SPI IP 的寄存器结构、常用 SPI 指令、擦写读流程完整梳理一遍。3.1 寄存器层级先摸清AXI Quad SPI 的寄存器视图AXI Quad SPI IP 通过 AXI4-Lite 接口暴露一组寄存器常见关键寄存器有SPICR0x00控制寄存器包含 SPI 使能、手动片选、FIFO 复位等位域。SPISR0x04状态寄存器包含发送 FIFO 空/满、接收 FIFO 空/满等位域。SPIDTR0x08数据发送寄存器向 FIFO 写入要发送的字节。SPIDRR0x10数据接收寄存器从 FIFO 读出接收到的字节。底层一次 SPI 传输的套路是固定的先查发送 FIFO 是否满不满就往 SPIDTR 写一个字节接着查接收 FIFO 是否为空不为空就从 SPIDRR 读一个字节。注意寄存器是 32 位宽度但发送和接收时有效数据在低 8 位。还有一个细节AXI Quad SPI IP 的 FIFO 在初始化后可能残留旧数据建议先通过 SPICR 里的 FIFO Reset 位做一次复位再开始正常收发。否则第一次读数据可能读到上一次会话的残留内容。3.2 N25Q128 常用指令集和底层收发函数N25Q128 属于标准 SPI NOR Flash指令集跟市面上大多数 SPI Flash 高度一致。我整理了一张常用指令表指令指令码说明WREN0x06写使能任何写操作前必须先发RDSR10x05读状态寄存器 1READ0x03普通读无 Dummy 周期Fast Read0x0B高速读带 8 个 Dummy 周期Quad Output Fast Read0x6B四线输出读带 8 个 Dummy 周期Page Program0x02页编程最多 256 字节Sector Erase0x20扇区擦除4KBBlock Erase0xD8块擦除64KBRead ID0x9F读 JEDEC IDN25Q128 的 JEDEC ID 是 0x20 0xBB 0x180x20 表示制造厂商0xBB 是设备类型0x18 表示容量。驱动初始化时先读 ID可以有效防止因为 Flash 贴错、虚焊、IO 连接错误导致的后续一系列诡异问题。底层的spi_transfer函数是所有 Flash 操作的基础核心逻辑如下int spi_transfer(uint8_t *txbuf, uint8_t *rxbuf, uint32_t len) { for (uint32_t i 0; i len; i) { // 等待发送 FIFO 非满 while (read32(BASE SPISR) TX_FIFO_FULL_MASK); write32(BASE SPIDTR, txbuf[i]); // 等待接收 FIFO 非空 while (read32(BASE SPISR) RX_FIFO_EMPTY_MASK); rxbuf[i] read32(BASE SPIDRR); } return 0; }注意发送和接收是同时发生的SPI 协议本身就是全双工。即使只需要接收数据也要先发送对应长度的“空字节”或指令字节来推动时钟。3.3 擦除、写页、读回校验完整动作序列写 Flash 之前必须先擦除因为 NOR Flash 只能把 1 写成 0不能把 0 写成 1。扇区擦除指令会把整个 4KB 区域全部变成 0xFF。下面是擦除一个扇区的标准流程发送 WREN0x06使能写操作。发送 Sector Erase0x20 加 3 字节地址。轮询状态寄存器 1等待 WIPbit0从 1 变成 0表示擦除完成。可选读取该扇区内容确认全部为 0xFF。页编程流程也很固定发送 WREN。发送 Page Program0x02 加 3 字节地址再加最多 256 字节数据。轮询 WIP等待编程完成。读回这 256 字节与原始数据比对。这里有三个实测经验值得写出来。第一写使能不是发了就行。写完 WREN 后最好读一次状态寄存器确认 WELbit1被置 1。如果 Flash 处于写保护状态或指令没被正确接收WEL 不会置位后续擦写会静默失败。第二页编程地址和数据长度都有限制。单次 Page Program 最多只能写 256 字节而且一页以内的地址偏移不能跨页。如果要从地址 0x1F00 写 256 字节前 256 字节没问题但从 0x2000 开始就属于下一页了必须拆分两次发送。第三擦除和编程后必须轮询 WIP不能一口气发几十个擦除命令再统一等。每个 Flash 内部操作都需要时间4KB 擦除大概几十毫秒64KB 擦除可能要几百毫秒。轮询时要设置超时避免 Flash 异常时系统陷入死循环。4. 远程升级的完整动作从生成镜像文件到安全回退驱动调通只是第一步。真正做远程升级时还要把“镜像怎么生成”“启动源怎么切换”“失败怎么回退”这几个环节串起来。4.1 别拿原始 .bit 直接写 Flash先转换成 .bin很多人第一次做升级直接通过网络把 Vivado 生成的 .bit 文件写到 Flash 里结果重启后起不来。原因是 .bit 文件本身是给 JTAG 和调试器用的除了配置数据外还包含设备 ID、时间戳等头部信息不一定适配 SPI 启动的加载格式。正确做法是用 Vivado 的 write_cfgmem 命令把 .bit 转换成 .bin 文件再通过上位机下发到设备。例如write_cfgmem -format bin -interface SPIx4 -size 128 \ -loadbit {up 0x01000000 D:/build/upgrade.bit} \ -file D:/build/upgrade.bin -force这个命令生成的 bin 文件数据内容在 Flash 中的偏移就是 0x01000000。升级程序收到文件后直接把数据写到 Flash 的 0x01000000 地址即可。这里有个细节interface 填 SPIx4 表示按 Quad SPI 的位序输出如果你的板子决定只用标准 SPI 启动interface 要填 SPIx1否则生成的 bit 顺序可能和实际启动模式不匹配。4.2 触发重启切换ICAP、WBSTAR 和 IPROG镜像写完且校验通过后下一步是把 FPGA 的启动地址切到 Update 镜像区。常规做法是通过 ICAPE2 原语向芯片内部发送配置指令设置 WBSTAR 并触发 IPROG。7 系列 FPGA 里WBSTAR 用来保存下一次热启动的起始地址。写入这个寄存器后再发送 IPROG内部程序控制指令FPGA 就会立刻重新加载指定地址的 bitstream。典型 Verilog 代码如下ICAPE2 #( .ICAP_WIDTH(WORD), .SIM_CFG_FILE_NAME(NONE) ) u_icape2 ( .O(icap_o), .I(icap_i), .CSIB(icap_cs), .RDWRB(icap_rw) ); // 写 WBSTAR地址指向 0x01000000 // 然后发送 IPROG 触发重配置具体字节序列需要根据 UG470 里 WBSTAR 和 IPROG 的指令格式构造。不同 FPGA 型号、不同 ICAP 位宽序列会有差异。核心思路是先让 ICAP 进入写状态再发送同步字然后写入 WBSTAR 值最后发送 IPROG 命令。如果工程里有 MicroBlaze 或 Zynq ARM也可以通过 PS 侧寄存器触发 PL 重配置原理一样只是入口不同。4.3 回退机制升级失败不变成砖的底线MultiBoot 的回退逻辑是这样的如果 WBSTAR 指向的 Update 镜像加载失败比如头部同步字不对、IDCODE 不匹配、CRC 错误配置引擎会等待配置看门狗超时然后自动回到地址 0 加载 Golden 镜像。这个机制能兜住大部分升级失败场景但前提是 Golden 镜像必须完整且可启动。所以我前面反复强调普通远程升级绝不覆盖 Golden 区。如果哪次升级真的把设备搞挂了现场至少还能通过重启回到 Golden重新进入升级流程。另一个容易被忽略的问题配置看门狗超时时间。Vivado 里有相关参数可以配置默认值可能过长或过短。过短会导致 Golden 镜像还没加载完就误判失败过长则会让损坏镜像的加载等待时间很长设备离线窗口变大。建议在测试板上实际测量正常镜像从 IPROG 到工作的时间然后留够余量。5. 联调实测中的高频坑和排查清单驱动写完、流程打通并不代表万事大吉。联调阶段最容易出问题很多坑是文档里不写、不踩不知道的。5.1 Flash 型号混淆N25Q128 和 W25Q128 的细微差异N25Q128 和 W25Q128 都是 128Mbit SPI NOR Flash容量一样指令也高度兼容但至少有两个地方容易出问题。一个是状态寄存器的细节。虽然 WIP 和 WEL 的位置一致但状态寄存器里的某些保护位、功能位的默认值可能不同如果驱动里做了寄存器比特级操作必须按数据手册核对。另一个是读 ID 的返回值。代码里如果用固定 ID 校验换 Flash 型号后就会失败。我在多块板子上使用过两款 Flash发现最好的方式是初始化时把读到的 ID 打印或上报ID 匹配表做成可配置的不要写死单一型号。还有一个电压等级问题。N25Q128 有 1.8V、2.5V、3.3V 多种版本市面上最常见的是 3.3V 版本。如果板卡 bank 电压是 1.8V却误用 3.3V Flash长时间工作会不断积累损坏风险。5.2 数据错位、丢字节先调 SPI 时钟再说别的现象很典型读 ID 正常小数据读写正常大块连续读时中间出现一簇 0xFF 或错位数据。排了一圈发现最后往往是 SPI 时钟频率跑太高或者 Quad Read 指令的 Dummy 周期配置不对。N25Q128 的 Quad Output Fast Read0x6B默认需要 8 个 Dummy 周期而某些 Flash 的 Quad Read0xEB需要 4 或 6 个。驱动里如果写死了一种 Dummy 数换芯片后读数据就会整体错位。排查方法是降频试。把 SPI Clock Ratio 改成 16 甚至 32如果现象消失再逐步提升频率直到找到稳定边界。这个边界跟 PCB 走线长度、接插件质量、FPGA IO 驱动强度都有关不要盲目追求最高频率。5.3 ILA 调试时怎么抓对信号怎么不被优化掉用 Vivado 的 ILA 在线调试时建议抓 AXI4-Lite 总线的 AW、W、AR、R 通道信号而不是抓 SPI 引脚。SPI 引脚信号频率高且变化快ILA 深度不够时很难看到完整时序AXI 总线上一个读写操作就是一次明确的事务方便定位问题。比较烦的是 ILA 被综合优化掉。如果信号在综合后被优化了ILA 是看不到波形的。解决办法是在 RTL 代码里对目标信号加(* mark_debug true *)属性或者直接在 Block Design 里把 ILA 接入对应接口让工具保留这些信号。调试完成后要记得把 ILA 从工程里移除或禁用。ILA 会占用大量 BRAM 和布线资源带 ILA 的版本跑在电性能差的板子上可能引入额外时序问题这是很多“测着好好的正式版出问题”的根源。5.4 升级没写完就断电用版本标记兜底双区架构只能保证“启动源没切换时安全”但如果升级程序已经写完 WBSTAR 并触发重启而 Update 镜像偏偏是坏的那就只能靠 Golden 回退。回退本身没问题可设备会一直停留在旧版本升级流程需要一个机制来识别“当前镜像是否有效”。实际项目中我们采用了一个版本标记扇区。该扇区固定位置存放结构体包含magic 值比如 0xA5A5A5A5用来判断扇区是否已被初始化。当前启动状态比如 BootNormal、Upgrading、UpgradeCompleted。版本号、镜像 CRC、写入时间等。升级流程按状态机执行进入升级前读取版本标记。如果发现 Upgrade 状态异常回退到 BootNormal。远程镜像写入 Update 区之前把标记写成 Upgrading。镜像写完并读回校验通过后把标记写成 UpgradeCompleted。设置 WBSTAR 指向 Update 区触发 IPROG。重启后从 Update 启动用户逻辑运行后把标记改回 BootNormal。这个状态机配合 MultiBoot 回退能保证绝大多数断电、断网、镜像损坏场景下设备最终还能回到一个可用版本。6. 一段可以直接复用的 C 风格驱动框架最后给一段通用的驱动框架按 AXI4-Lite 地址映射实现方便读者移植到自己的工程。6.1 基本宏定义与初始化#define QSPI_BASE 0x44A00000 #define SPICR_OFFSET 0x00 #define SPISR_OFFSET 0x04 #define SPIDTR_OFFSET 0x08 #define SPIDRR_OFFSET 0x10 #define SPICR_SPE 0x00000200 #define SPICR_FRES 0x00000100 #define SPISR_RX_EMPTY 0x00000040 #define SPISR_TX_FULL 0x00000020 void qspi_init(void) { uint32_t cr; cr read32(QSPI_BASE SPICR_OFFSET); cr | SPICR_FRES; write32(QSPI_BASE SPICR_OFFSET, cr); cr read32(QSPI_BASE SPICR_OFFSET); cr ~SPICR_FRES; cr | SPICR_SPE; write32(QSPI_BASE SPICR_OFFSET, cr); }6.2 底层传输与状态等待int qspi_transfer(uint8_t *tx, uint8_t *rx, uint32_t len) { for (uint32_t i 0; i len; i) { while (read32(QSPI_BASE SPISR_OFFSET) SPISR_TX_FULL); write32(QSPI_BASE SPIDTR_OFFSET, tx[i]); while (read32(QSPI_BASE SPISR_OFFSET) SPISR_RX_EMPTY); rx[i] read32(QSPI_BASE SPIDRR_OFFSET); } return 0; } int flash_wait_busy(uint32_t timeout_ms) { uint32_t start get_tick_ms(); uint8_t tx[2] {0x05, 0x00}; uint8_t rx[2] {0, 0}; while (get_tick_ms() - start timeout_ms) { qspi_transfer(tx, rx, 2); if (!(rx[1] 0x01)) return 0; } return -1; }6.3 擦除与页编程int flash_sector_erase(uint32_t addr) { uint8_t tx[4]; flash_write_enable(); tx[0] 0x20; tx[1] (addr 16) 0xFF; tx[2] (addr 8) 0xFF; tx[3] addr 0xFF; qspi_transfer(tx, NULL, 4); return flash_wait_busy(1000); } int flash_page_program(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t remain len; uint8_t tx[4 256]; uint16_t offset 0; while (remain 0) { uint16_t chunk (remain 256) ? 256 : remain; flash_write_enable(); tx[0] 0x02; tx[1] ((addr offset) 16) 0xFF; tx[2] ((addr offset) 8) 0xFF; tx[3] (addr offset) 0xFF; memcpy(tx[4], data offset, chunk); qspi_transfer(tx, NULL, 4 chunk); if (flash_wait_busy(100) ! 0) return -1; remain - chunk; offset chunk; } return 0; }这里把页编程做了拆页处理保证跨页数据只出现在单个 Page 内。实际项目中我还会在每 4KB 扇区写完后读回比对一旦发现错误立即定位到具体扇区只需要重写该扇区不必整片推倒重来。这套框架在多个基于 Artix-7 的板卡上跑过配合 25MHz 到 40MHz 的 SPI 时钟升级 8MB 镜像大概需要两到三分钟大部分时间花在擦除和编程轮询上。如果对时间有更高要求可以考虑在擦除和编程阶段使用 IP 的中断机制避免 CPU 空转等待但逻辑复杂度也会相应上升。我个人在实际项目中的体会是远程升级方案里成功路径永远不是核心难点失败路径才是。写驱动前先想清楚断电、断网、镜像损坏、Flash 异常这几类场景下设备怎么兜底双区架构、版本标记、回退机制都设计好了再开始写代码。最后再分享一个小技巧升级流程上线前拿到测试板上反复做断电实验不固定断点随机在擦除、页编程、校验、切换启动源各个阶段断电重启连续跑几十轮设备能稳定回到 Golden 镜像这个方案才敢交付到现场。