☰
FPGA远程升级实战:基于AXI Quad SPI与Multiboot的完整避坑指南
2026/10/5 12:52:36 网站建设 项目流程

这两天群里又有人在问FPGA远程升级的事,板卡已经发到现场了,结果业务逻辑要改,总不能还拎着JTAG线飞过去。以前遇到这种情况确实头疼,但现在Xilinx FPGA基本都支持multiboot,配合外部SPI Flash就能实现“上电自主选择加载哪个镜像”,配合ICAPE3还能在运行中主动触发跳转。整套方案的关键器件就是AXI Quad SPI这个IP核和N25Q128这颗Flash,踩过的坑不少,写篇完整的避坑流程出来,希望能帮正在搞远程升级的人少走点弯路。

这篇博文的内容,我按“原理—配置—读写—升级—排障”这条链路来拆,覆盖AXI Quad SPI IP核的参数配置、N25Q128的指令集操作、Golden/Update双镜像规划、ICAPE3触发multiboot,以及我在实测中遇到的几类典型问题。适合已经会用Vivado建工程、能看懂AXI总线基本时序的FPGA开发工程师阅读,刚入门的朋友建议先补一下SPI协议和FPGA配置流程的基础知识再来看。

1. 远程升级的底层逻辑与整体方案设计

1.1 为什么是“双镜像 + Multiboot”?

FPGA上电后,配置逻辑会从外部Flash的起始地址搬运bitstream。Xilinx在7系列及之后的器件里支持Multiboot功能,意思是FPGA启动时可以先从Flash的0地址加载一个“引导镜像”,这个引导镜像运行起来之后再通过ICAPE3发起一条IPROG命令,让FPGA主动跳转到Flash的另一个地址去加载真正的业务镜像。如果跳转加载失败,或者写入的镜像本身是坏的,FPGA还能自动回退到Golden镜像继续启动。

这套机制的好处是明显的:现场不需要任何物理连接,只要业务端能收到新的bit文件,把它写到Flash的指定区域,然后触发热复位,整机就在无人干预的情况下完成了逻辑更新。就算新镜像写坏了,设备也不会变成砖,还会回到出厂版本继续工作。坏处也有,就是Flash分区、镜像生成、回退逻辑这些必须在前期设计好,不能临时抱佛脚。

1.2 AXI Quad SPI在这个方案里扮演什么角色?

AXI Quad SPI是Xilinx提供的一个软核IP,挂在AXI4-Lite总线上,通过寄存器操作去控制QSPI控制器,最终完成对SPI Flash的擦除、写入和读取。在远程升级方案里,它的定位是“运行时的Flash访问通道”。

为什么不用普通的GPIO模拟SPI?因为QSPI控制器支持x1、x2、x4模式,可以跑到较高的时钟频率,而且自带命令解析逻辑,适配市面上主流的SPI NOR Flash指令集,同时还能配置成线性地址模式,把Flash直接映射到处理器地址空间,读起来非常方便。用GPIO模拟当然也能写,但性能差太多,写一个16MB的镜像会等到怀疑人生。

需要注意的是,FPGA上电配置阶段,Flash是由FPGA内部的配置逻辑直接控制的,AXI Quad SPI在此时还不存在。等Golden镜像加载完成,AXI Quad SPI跑起来之后,它才接管Flash的访问权。这个“接管”不是硬件切换,而是靠时序上的约定,AXI Quad SPI发出访问时,配置阶段的驱动早已释放总线。

1.3 N25Q128芯片特性速览

N25Q128是Micron(现在归入英飞凌旗下)的128Mbit SPI NOR Flash,等于16MB。这个容量放2~3个中等规模的FPGA镜像完全够用。它的页大小是256字节,扇区大小默认是64KB(部分型号支持子扇区擦除),擦写寿命大约10万次,数据保持能力官方标称20年,工业级温度范围-40~85摄氏度,非常适合做配置存储。

指令集方面,我常用的就几个:0x06写使能、0x05读状态寄存器、0x20扇区擦除、0x02页编程、0x03/0x0B读取、0x9F读ID、0x01读状态寄存器1、0x50清状态寄存器。后面操作的时候会逐个讲。

1.4 我最终采用的架构

我在实际项目里用的是XC7K325T + N25Q128,Flash挂在FPGA的专用配置引脚上,AXI Quad SPI配置成Standard+Quad模式,线性地址空间直接映射Flash的前16MB。片内逻辑挂了一个AXI-Lite Master,通过它操作SPI控制器寄存器,完成擦写和校验。

镜像布局分三块:

  • 0x000000:Golden镜像,永远保留,只允许在出厂时写入,后续升级流程不许擦除它。
  • 0x200000:Update A区,放最新版本业务镜像。
  • 0x400000:Update B区,放上一个版本镜像,形成AB双备份,避免单区更新失败后无版本可用。

实际容量如果镜像较小,也可以简化成Golden + Single Update两个区,关键是必须留一个不会动的兜底镜像。

2. AXI Quad SPI IP核配置:从参数选择到上电初始化

2.1 IP配置里最容易忽略的4个参数

AXI Quad SPI IP的配置界面乍看很简单,就几项下拉框,但真正决定成败的就是几个细节。

第一个是SPI Mode。IP提供Standard、Dual、Quad、Dual/Quad等选项。如果Flash支持x4读,想让启动速度快一点,选择“Quad”并勾选“Use the Quad Mode for Read”这类选项,注意x4模式的DO引脚会被复用,必须设为inout类型。

第二个是Data Width。默认是32位还是8位?取决于你的AXI总线设计。用AXI4-Lite的话,数据位宽最好统一,避免后续地址对齐和读写逻辑混乱。

第三个是Instruction Width。N25Q128指令通常是8位,选8就行,但如果是某些国产Flash或者老型号是16位指令,这里不对,控制器就会发出错误的命令字。

第四个是SPI Clock Divider。这个很多人都忽略了,直接用默认的32分频,结果在SPI时钟频率较高的时候Flash工作不稳定。我自己习惯先在低频下把所有功能调通,比如AHB/AXI时钟100MHz,SPI时钟先给10MHz左右,稳定后再往上提。后面有专门一节讲速度问题。

还有个容易忽略的点:IP配置里需要选择是否有FIFO,FIFO深度是多少。做大量数据搬运的时候,FIFO深一点能减少CPU轮询次数,但消耗的BRAM也更多,根据实际需要取舍。

2.2 STARTUP原语:不说清楚这里必踩坑

如果你把AXI Quad SPI的时钟直接接到普通逻辑时钟上,一开始功能看似正常,但等到真正加载大镜像时经常出现偶发错误。原因在于,FPGA配置完成后,CCLK引脚可能已经停止输出,而QSPI IP为了操作Flash,需要在运行时自己产生SPI时钟。如果这个时钟没有经过STARTUP原语引到CCLK相关路径上,硬件上可能存在时钟拓扑不匹配的问题。

正确做法是在AXI Quad SPI IP的配置界面里勾选“Include STARTUP primitive”,然后让IP内部产生的时钟通过STARTUP原语连接到配置时钟路径,或者手动例化STARTUPE2原语,把SPI时钟从原语的CLKOUT引脚引出来。这样Flash的时钟域就与配置阶段保持一致,读写时序不容易出幺蛾子。

具体到代码,用STARTUPE2的时候大概是这样的写法:

STARTUPE2 #( .PROG_USR("FALSE"), .SIM_CCLK_FREQ(100.0) ) STARTUPE2_inst ( .CFGMCLK(), // 输出 .CLK(0), .GSR(0), .GTS(0), .KEYCLEARB(0), .PACK(1), .USRCCLK(spi_clk_internal), // 把IP生成的时钟喂进来 .USRDONEO(1), .USRDONETS(1) );

这样写完之后,实测SPI时序的稳定度会高很多,尤其在高位宽模式下,误码率明显下降。

2.3 寄存器模型与读写接口

AXI Quad SPI的寄存器不多,常用的就几个。0x00是控制寄存器,0x04是状态寄存器,0x08是发送FIFO数据寄存器,0x0C是接收FIFO数据寄存器,0x10是SPI状态寄存器,0x14以上还有一些扩展控制位。

单次传输的基本流程是:先在控制寄存器里配置SPI传输模式,把要发送的指令和数据写入DTR寄存器,然后拉起控制寄存器的启动位,控制器内部会自动完成MOSI/MISO的移位操作,完成后状态寄存器会置位。接收侧从DRR寄存器读回数据即可。

需要注意的是,控制寄存器里有个“执行”位,必须在写入指令数据之后再触发,顺序反了会导致指令先发出去、数据跟不上,Flash直接解析成错误命令。另外SPI状态寄存器里有忙标志,每次传输结束后要等待它清零,不要急着发下一条命令。

2.4 IP核的“开机动作”

AXI Quad SPI上电之后,不能直接就去操作Flash。建议先做一次软复位,然后清空两个FIFO,再读取Flash的ID确认链路正常。这个动作花不了几个时钟周期,但能避免上一次复位残留的状态影响后续操作。

清FIFO的方法在不同版本IP里略有差异,一般是写控制寄存器的FIFO清空位,然后读状态寄存器确认FIFO空标志。如果FIFO里残留数据,后续读回的数据就全是偏移的,那排查起来非常蛋疼。

3. N25Q128 Flash读写实操:指令模式还是线性模式?

3.1 指令模式与线性模式的适用场景

AXI Quad SPI支持两种操作模式:指令模式和线性模式,这个概念必须分清。

指令模式下,CPU通过写寄存器的方式向Flash发出完整的命令序列,比如“发送0x06写使能”、“发送0xD8擦除扇区”、“发送0x02写入256字节”。这个模式的优点是灵活,可以执行任意指令,缺点是每次传输要CPU参与,带宽不高。

线性模式下,AXI地址直接映射到Flash的地址空间,CPU像读内存一样读Flash,控制器自动生成读指令并搬家数据。这个模式非常高效,适合大量回读校验,以及直接把配置数据从Flash加载到DDR。

我在项目里的用法是:升级阶段用指令模式擦写,校验阶段用线性模式整片回读比对。两个模式都打开,按需切换。

3.2 最关键的坑:跨页编程

N25Q128的一个page是256字节,page program指令最多只能写一个page,而且不能跨页。这句话看起来简单,实际操作中至少有一半的“写入失败”案例是跨页造成的。

比如你要从一个非对齐地址开始写300字节,0x0200地址写100字节,剩下的200字节写到0x0300,这实际上是两个page的操作,你必须拆成两次page program,第一次写0x0200~0x02FF,第二次写0x0300~0x0363,否则Flash会从当前页起始地址卷绕,把数据写错不报错,回读才发现对不上。

我的经验是封装一个“任意起始地址、任意长度写入”的函数,内部自动处理页对齐:

int flash_write(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t offset = addr & 0xFF; uint32_t first_chunk = 256 - offset; if (first_chunk > len) first_chunk = len; // 先写不满一页的首块 spi_write_page(addr & ~0xFF, data, first_chunk); addr += first_chunk; len -= first_chunk; // 剩余部分按整页写 while (len > 0) { uint32_t chunk = (len > 256) ? 256 : len; spi_write_page(addr, data + (addr - (uint32_t?) ), chunk); addr += chunk; len -= chunk; } }

注意上面的索引只是示意,实际要维护好源数据指针,别拿地址当偏移去索引数组。

3.3 读状态与等待函数

Flash写完或者擦除后,状态寄存器1的低位WIP会保持1,表示器件的内部状态机还在忙。发送0x05指令后读回的那个字节,bit0为1就继续等,为0才代表操作结束。

等待逻辑一定要设计超时机制。N25Q128整片擦除可能要几十秒,扇区擦除也要几十到几百毫秒,页编程一般在1ms以内。如果程序里只写了个死等,一旦Flash异常,CPU就hang死在那边了。建议给不同操作设置不同的超时阈值,比如页编程100ms、扇区擦除5s、读状态100us轮询一次,超时就报错返回。

int spi_flash_wait_ready(uint32_t timeout_ms) { uint32_t elapsed = 0; while (elapsed < timeout_ms) { uint8_t status; spi_flash_read_status(&status); if (!(status & 0x01)) return 0; delay_ms(1); elapsed++; } return -1; }

这个函数虽然简单,但在远程升级里是生命线。状态没等就发下一条命令,Flash会直接忽略;超时时间设太短,明明还在擦除就会报写入失败。

3.4 完整读写函数库的结构

我习惯把N25Q128的操作封装成一个独立的C文件,对外暴露几个接口:

初始化、读ID、擦除扇区、页写入、任意长度写入、线性模式读取、CRC校验。这些接口底层都走AXI Quad SPI的寄存器读写,上层业务调用时不用关心SPI细节。

页写入的底层序列一定要按顺序来:先写0x06使能,然后立即发0x02页编程命令,后面跟上3字节地址,再跟256字节数据。命令和数据之间不要有任何多余的SPI访问,N25Q128要求这一整条序列是连续的,中间被打断就会失败。

3.5 线性模式回读校验

写完镜像之后,我习惯用线性模式把整个Update区读回来算CRC,和发送前的CRC对比,完全一致才允许跳到新镜像启动。这一步虽然多花几秒钟,但能把绝大部分坏块和总线问题挡在升级之前。

线性模式的优势在于,读回操作不会污染Flash内部状态,也不需要擦除,可以反复读。如果发现CRC不一致,还能再把出错的扇区重新擦写一遍,不用全片重来。实测下来,大部分闪存错误都是单bit翻转,重写一次就能修复。

4. 远程升级与回滚实现:Multiboot、ICAPE3与WBSTAR

4.1 Golden区与Update区的镜像规划

双镜像规划的核心是:不管你Update区怎么折腾,Golden区都不能碰。所以我推荐Golden区在出厂时一次性烧好,后面所有升级程序都只操作Update区。

地址分配我给出一个实际例子:

分区起始地址大小用途
Golden0x0000002MB出厂引导镜像,永不修改
Update A0x2000002MB当前业务镜像
Update B0x4000002MB备份业务镜像

如果你的bit文件超过2MB,按实际大小调整,但注意扇区擦除是按64KB对齐的,分区边界最好对齐64KB,否则擦除一个扇区会连累邻居分区。

4.2 ICAPE3原语与32位命令序列

FPGA运行过程中,用户逻辑可以通过ICAPE3原语把命令发送给配置逻辑,从而实现重新配置(重加载)。ICAPE3是一个32位命令接口,发送固定序列即可触发multiboot跳转。

我常用的跳转命令序列如下:

localparam [31:0] SYNC_WORD = 32'hAA995566; localparam [31:0] CMD_WBSTAR = 32'h20000000; // 写入WBSTAR寄存器之前的前导命令 localparam [31:0] WBSTAR_UPDATE = 32'h00200000; // Update区起始地址,注意低4位为0 localparam [31:0] CMD_IPROG = 32'h30008001; localparam [31:0] IPROG_CMD = 32'h0000000F;

发送顺序是:先同步字0xAA995566,再发0x20000000,紧接着把目标地址0x00200000写入,再发0x30008001,最后写0x0000000F触发IPROG。其中WBSTAR的值要左移对齐,实际上WBSTAR寄存器只使用高28位,所以地址低4位必须为0。

有个细节:ICAPE3一次只能接收32位,不能用AXI直接连续写,必须通过状态机把序列一条一条喂进去。喂完之后配置逻辑会自动重新加载Flash,此时原来的逻辑停止运行,所以这个跳转是不可逆的,执行前一定要确认写好的镜像合法。

4.3 触发远程升级的三种方式

第一种最简单,收到远程命令后,先校验完整镜像,再写WBSTAR触发ICAPE3。适合有外部通信链路的场景,比如以太网、UART。

第二种是定时触发,系统每天凌晨固定检查一次是否有升级包,有就自动升级。这种适合无人值守设备,但必须加看门狗,防止升级过程中主流程卡死。

第三种是外部IO触发,比如拨码开关或者按钮按下才允许升级,适合维修调试时手动升级。我的做法是这三种都保留,用寄存器选择触发源,灵活性最高。

4.4 升级、校验与回滚的完整流程

一次真正靠谱的远程升级,我总结成六步:

第一步,通过通信接口接收升级文件,放入DDR暂存。第二步,对文件计算CRC,和发送端的CRC比对,确认文件完整。第三步,擦除Update A区。第四步,向Update A区写入新镜像。第五步,线性模式回读整区并计算CRC,再次比对。第六步,写WBSTAR指向Update A区,触发ICAPE3重加载。

这六步里最容易被忽略的是第二步和第五步。第二步不校验,网络传输出错后照样写进Flash,回读比对时才发现,但此时旧镜像可能已经被擦掉了。第五步校验不能省,万一擦写过程中掉电,Flash里是半截数据,直接重启必挂无疑。

4.5 Fallback回退是怎么生效的?

很多人以为fallback要靠自己的逻辑实现,其实Xilinx的配置逻辑自带一个回退机制。当FPGA从Update区加载bitstream时,配置逻辑会启动一个超时计数器,如果在规定时间内没有完成同步字检测,或者加载出错,就会自动重新回到Golden区加载。

这个超时时间在生成bitstream时的属性里设置,一般设5~20ms。时间设大了,启动失败后要等很久才能回退;设小了,正常加载稍微慢一点就会误触发回退。我实测7系列在50MHz SPI时钟下,2MB镜像大约几百毫秒能加载完成,所以超时时间设置在100ms左右比较稳妥,具体要看你自己的Flash时钟频率。

需要特别注意的是,回退只能跳过损坏的bitstream,如果你写进去的镜像能通过配置逻辑的校验,但业务逻辑本身跑飞了,那fallback不会触发。这种场景需要在应用层加看门狗,发现业务异常时主动通过ICAPE3跳回Golden。

4.6 生成合并镜像与烧写

Vivado里生成bitstream之后,勾选生成bin文件,然后用Write_cfgmem生成一个适合烧写的bin或者MCS格式文件。如果想把Golden和Update拼成一个文件一次性烧进Flash,可以直接用bin文件拼接,最简单的方式是:

dd if=golden.bin of=combined.bin bs=1M count=2 dd if=update_a.bin of=combined.bin bs=1M seek=2 dd if=update_b.bin of=combined.bin bs=1M seek=4

注意dd的bs和count要和实际分区大小对应。拼完之后用Vivado Hardware Manager加载到Flash,或者自己做上位机通过AXI总线写进去都行。

如果只更新Update区,更快的做法是直接通过运行中的系统把bin文件送到FPGA端,由FPGA自己把文件写入Update区,完全不需要连接JTAG。这也是“远程升级”的核心价值所在。

5. 实测中踩过的坑与排查思路

5.1 Flash ID读到0xFF或0x00

这个问题大概率不是Flash坏了,而是SPI链路压根没通。先查硬件连接,确认片选、时钟、MISO/MOSI没有接反;再查约束,如果Flash挂在专用配置引脚上,需要在XDC里加上SPI_BUS_STANDARD、SPI_BUS_READ_WRITE这些属性;最后查时钟,IP的SPI时钟频率是不是超出了Flash支持上限,N25Q128在Quad模式下最高标称108MHz,但常规读取模式建议先降到20MHz以下调试。

还有一个隐蔽原因:SPI控制器被配置成Quad模式,但Flash并没有进入Quad Enable状态,此时读ID会读到错误值。先发0x01读状态寄存器,看bit1(Quad Enable)是否为1,不是的话发0x35或写状态寄存器把它打开,不同型号Flash状态位可能不同,具体看数据手册。

5.2 升级之后设备直接卡死

设备卡死要分两种。一种是新镜像本身有问题,比如差分时钟没锁住、外设初始化卡住,这种是应用层问题,fallback救不了,需要在业务逻辑里加软狗。另一种是Flash里的镜像被写坏了,这种fallback应该能拉到Golden,但如果Golden区也空着或者本身没烧好,那就只能JTAG重新烧了。

排查思路是先用ILA抓ICAPE3的状态,确认WBSTAR有没有写进去、IPROG有没有发出来;再量一下Flash的CS脚,看加载期间有没有正常的跳变;最后看PS端的BOOT_MODE引脚,确认当前是从SPI启动,别是板卡上跳线被改了。

5.3 Fallback没生效

Fallback没生效,最常见的三个原因:一是生成bitstream时没勾选fallback属性,默认是关的;二是WBSTAR写到的地址不是一个合法镜像的起始地址,配置逻辑找不到同步字就直接挂住,不会再回退;三是超时时间设得太短,Flash时钟太慢,5ms内连同步字都没给全,提前触发了回退的“假失败”。

实操中最容易疏忽的是第三条,因为看起来像是回了Golden,实际上只是超时设太小。我调试时把超时时间直接设成500ms,保证一帧镜像能完整加载完,最后再把时间调回合适值。

无论什么原因,排查fallback问题最快的办法是拿一个已知能启动的镜像分别放到Golden和Update区,然后故意在Update区放一个空的全FF文件,触发一次升级,看能不能回退。能回退说明机制正常,不能回退再逐项查配置。

5.4 用了普通IO导致启动失败

如果Flash接的是普通IO而不是专用配置引脚,上电时FPGA还没加载配置,普通IO处于高阻态,Flash片选可能悬空,导致配置读取失败。解决办法主要有两种:一是干脆把启动模式改成从普通IO接的Flash加载,前提是这块FPGA支持非专用引脚启动,这个要查器件手册;二是加上拉电阻,让片选在上电时保持无效电平,等逻辑跑起来之后再把存储访问权接管过来。

更稳的方案是:Flash放在专用配置引脚上,同时把AXI Quad SPI的引脚约束指向同一组引脚。这样上电时配置逻辑用这些引脚加载,运行时的SPI控制器也用这些引脚访问Flash,不会冲突。

5.5 SPI速率上不去,跑高频就出错

N25Q128在标准SPI模式下最高可以跑到几十MHz,但关键是PCB走线、上拉电阻、寄生电容都会影响信号质量。我调试时发现时钟升到33MHz以上就丢数据,降到20MHz一切正常。

升速要分步走,不要一上来就调到最高。先把分频系数调到SPI时钟10MHz,验证所有读写和擦除流程没问题;再调到20MHz,重复验证;最后再往高提。同时检查XDC里有没有对QSPI引脚做时序约束,如果时序违例,高速模式下数据采样点不准是必然的。

5.6 ILA抓不到信号怎么办

远程升级逻辑跑在FPGA内部,最直接的调试手段就是ILA。抓不到信号,先确认ILA例化的数量和采样深度会不会导致综合后资源不够,ILA被优化掉了;再确认触发条件是不是太严格,最好先设一个“任意变化就触发”,看波形里有没有信号活动;最后确认时钟域,ILA的采样时钟和待测信号必须在同一个时钟域。

我遇到过一种情况:ILA能看到写寄存器的AXI事务,但SPI控制器没有任何输出,原因是IP的时钟没有使能。后来在IP配置里把SPI时钟的分频系数改成了可配置项,代码里先写分频寄存器,再打开全局使能,问题就没了。

5.7 常见问题速查表

现象可能原因解决办法
读ID全FFSPI链路不通、时钟未起查硬件连接和约束,降频重试
写入后回读不一致跨页写入封装页对齐写入函数
擦除后数据没变Flash被写保护先读状态寄存器,清BP位
升级后卡死新镜像不合法检查WBSTAR地址,改用可回退流程
Fallback不动作没勾选fallback属性重新生成bit文件,确认属性
高频读写误码信号完整性差降频、检查PCB走线、加约束
ILA无信号时钟域不对或ILA被优化确认采样时钟和触发条件

这里面Flash写保护这个坑我想单独多说一句。N25Q128状态寄存器里有BP0~BP3位,出厂时可能是0,但有些板厂烧录工具或者旧代码会不小心置1,导致后面所有写操作都静默失败。遇到写不进、擦不掉的情况,第一步永远是读状态寄存器,确认保护位状态,必要时发0x50清除状态寄存器。

结语还是算了吧,说点实在的

做了这么多次远程升级,我最大的体会是:做这套东西,七分靠设计,三分靠调试。设计阶段把分区、回退、校验、超时这些都想清楚,后面几乎不会出大问题;反过来,如果一上来就疯狂写代码,等板子现场挂了才查,那真是叫天天不应。

再分享一个小技巧:在开发环境里,我习惯用Vivado的硬件管理器把Flash内容导出来,用电脑上的十六进制工具和原始bin文件做对比,一眼就能看出来地址偏移或数据错位。这个习惯帮我抓过三次升级文件生成时的打包错误,比在板子上盲调高效得多。

调试顺序上也有一点建议,先把“读写-擦除-校验”这条链路完全跑通,再做“WBSTAR跳转”,最后才接远程通信。三层都稳定之后,再考虑并发、超时、断点续传这些业务细节。远程升级不是功能,是保命用的基础设施,值得多花几天时间打磨。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询