项目交付后的第一次功能改动往往是远程升级需求被真正提上日程的节点。我手里这套基于Artix-7的边缘采集设备当初设计时只留了JTAG口结果客户要求在不停机的前提下更新FPGA内部的滤波逻辑这就逼着我去做SPI FLASH动态烧录。选型阶段看了几条路线最后落在STARTUPE2原语方案上FPGA运行期间由用户逻辑接管配置引脚直接把新比特流写入外部SPI FLASH等设备下次重新加载配置时新逻辑自动生效。这个功能单独看只是“能写flash”而已但把数据链路、烧录状态机和回滚机制拼在一起就是一个完整可落地的FPGA远程升级方案。本文把这套方案的原理与实现过程梳理出来适合正在给FPGA设备加远程升级能力、或者被“运行中不能烧配置芯片”问题卡住的工程师。1. 远程升级的不止是“下一版比特流”两类升级需求与三条实现路线1.1 先分清改配置镜像和改逻辑运行是两回事很多朋友一说FPGA升级第一反应是把新的.bit文件下载到Flash里。这个理解没错但不完整。FPGA的配置文件在运行时的角色和MCU的固件不一样——FPGA上电时会从SPI FLASH读出配置数据写入内部配置内存CRAM然后逻辑才开始运行。SPI FLASH里的bit流只在上电或重配置时被读取运行过程中FPGA并不依赖外部Flash里的内容。这一条特性恰好被远程升级利用运行期间把Flash里的旧镜像擦掉、写入新镜像当前逻辑完全不受影响“下次配置时生效”。但要注意这属于“配置镜像升级”。还有另一类远程升级是向FPGA内部动态写入重构指令比如通过ICAPE2原语做部分重配置运行中的部分逻辑可以被替换而不断流但那是完全不同的另一套机制。本文讨论的是前者更新SPI FLASH里保存的配置镜像。这种方式的工程价值在于它几乎适用于所有外置SPI Flash配置的Xilinx 7系列设备改动面小风险可控。1.2 三条常见路线MCU代烧、内部逻辑烧录、JTAG注入抛开“派人去现场接JTAG”这种最原始方案远程升级在FPGA工程里通常有三条路线。路线是否需要额外芯片对当前运行的影响实现复杂度典型场景A外部MCU/ARM代烧Flash需要无影响低板卡本来就有管理MCUBFPGA Fabric直接控制FlashSTARTUPE2方案不需要无影响中无MCU或想保持独立CBSCANE2 JTAG链远程注入需要上位机工具链需要短暂介入中高调试维护通道不适合产品批量升级路线A最常见STM32这类主控通过普通IO连接Flash在FPGA运行期间把数据写进去。优点是代码好写MCU生态成熟缺点是增加BOM而且MCU和FPGA之间还要约定握手协议系统将来要扩展功能时MCU固件也得跟着升级。路线C用BSCANE2原语把JTAG指令接进Fabric由上位机通过JTAG电缆或者远程JTAG服务器下发命令本质上是把下载器搬到了网络上。它适合调试和产线生产但作为产品的日常升级通道协议复杂度和上位机依赖都比较重。路线B就是本文主角。FPGA内部逻辑直接操作Flash数据通道可以是UART、以太网或者PCIe完全绕开外部处理器。板子上如果本来就有FPGA这个方案的增量成本几乎为零。1.3 为什么最终选定STARTUPE2方案我当时选路线B最主要的原因是板卡结构已经定了没有多余空间塞MCU。而且这个采集设备本身有千兆网上行口数据已经能通过网络进FPGA让Fabric直接写Flash是最顺的路径。另一个考虑是集成度。FPGA内部逻辑自己管Flash意味着远程升级协议、校验、回滚策略全部可以在FPGA工程内闭环。后续换了Logic版本只要接口不变Flash烧录模块可以原封不动复用。这种“自包含”能力在工业设备维护里非常省事——不需要和MCU团队扯皮谁负责升级、谁的固件先刷。不过路线B有一个绕不开的技术前提FPGA的配置引脚CCLK、D00/D01等在用户模式下默认不受用户逻辑控制尤其是CCLK它是一条专用时钟网络。要想让用户逻辑在运行期间驱动这些引脚必须使用STARTUPE2原语。这一点是整篇文章的核心入口。2. STARTUPE2原语到底放开了什么配置引脚在用户模式下的接管与例化2.1 专用配置引脚为什么不能当普通IO用在7系列FPGA上SPI Flash配置相关的引脚有CCLK、D00/D01/D02/D03、FCS_B等。它们在配置阶段由FPGA内部的配置引擎驱动配置完成后大多数会被释放但CCLK引脚的处理比较特殊它在用户模式下保持高阻或者由配置引擎钳制普通逻辑根本没有驱动它的通路。有人会想那我用Fabric里的普通IO去接Flash的CLK不就行了物理上不行。Flash的CLK必须和FPGA的CCLK引脚相连才能走配置通路而CCLK引脚在用户模式下并不等价于一个普通IO它连接的是配置时钟网络。想让它输出用户自定义时钟官方原语就是STARTUPE2。另外D00/D01这段也有认知误区。7系列的D00/D01在配置完成后是允许作为普通IO使用的但前提是配置引擎不再占用它们而且相关电压、IO标准要匹配。如果设计里既没有MCU也没有STARTUPE2这两个引脚被配置逻辑释放后其实是“浮空可用”的状态但FPGA工具默认不会把它们当普通IO给用户所以工程上还是要专门处理。CCLK那条路则必须走STARTUPE2。2.2 STARTUPE2端口行为与最小例化STARTUPE2在7系列里的端口不算多关键就这几个端口名方向作用典型接法USRCCLKO输入用户时钟输出到CCLK引脚接自定义SPI时钟USRCCLKTS输入CCLK三态控制0使能输出1高阻平时拉1烧录时拉0USRDONEO输入用户DONE信号输出固定1USRDONETS输入DONE三态控制固定1CFGMCLK输出配置时钟输出可观察配置时钟不用可不接GSR输入全局复位输入固定0GTS输入全局三态输入固定0KEYCLEARB输入密钥清除固定1PACK输入打包控制固定1最小例化代码非常简单在模块里直接例化原语就可以wire spi_clk; // 用户逻辑生成的SPI时钟 STARTUPE2 #( .PROG_USR(FALSE), .SIM_CCLK_FREQ(0.0) ) STARTUPE2_inst ( .CFGMCLK ( ), .CLK (1b0 ), .GSR (1b0 ), .GTS (1b0 ), .KEYCLEARB (1b1 ), .PACK (1b1 ), .USRCCLKO (spi_clk ), .USRCCLKTS (1b0 ), .USRDONEO (1b1 ), .USRDONETS (1b1 ) );当USRCCLKTS拉低时spi_clk就会出现在CCLK引脚上。这里最容易被忽略的就是USRCCLKTS。如果你拉高CCLK保持高阻Flash收不到时钟后面一切读写都白搭。2.3 时钟与片选的两个关键接法第一FPGA的CCLK引脚必须和Flash的CLK引脚直连。注意不能中间加缓冲器也不能串电阻太大否则配置阶段时序会出问题。第二Flash的CS片选引脚强烈建议接普通IO而不是专用FCS_B。FCS_B虽然也能在用户模式下释放但处理起来更麻烦还要关心配置属性设置。普通IO做CS逻辑控制最直接后续调试也方便。如果硬件已经固定用FCS_B也不是不行需要在生成比特流时确保FCS_B在配置完成后被释放并且用户逻辑能够接管。但我的经验是凡是用到FCS_B的板子后期总会有人因为CS时序不对踩坑。新设计最好单独留一个普通IO。另外USRCCLKTS不要一直拉低。烧录结束后把USRCCLKTS拉回1让CCLK处于高阻减少不必要的翻转功耗。平时Flash不操作时钟一直跑也容易耦合噪声到模拟电路。这个细节在系统EMC测试时会体现出来。2.4 与Intel/其他平台实现方式的对照习惯用Altera/Intel系列的朋友可能会疑惑为什么那边很少听到STARTUPE2这种原语因为Intel的AS配置接口和用户逻辑的关系不一样。Cyclone V等器件用EPCS/配置器件时ASDO、DCLK、nCSO这些引脚在配置完成后同样会被释放但Intel提供了Remote System Upgrade IP来专门处理远程升级流程核心是配置映像的切换逻辑而不需要自己写底层Flash控制。Xilinx 7系列这边STARTUPE2负责打通引脚通路Flash时序反而要自己写。如果只是做简单的只读配置甚至不需要STARTUPE2因为配置引擎已经读过了。但要做动态烧录就必须自己落地SPI Master和烧录状态机。其实这个模式自由度更高不受厂商IP流程限制。早期Xilinx的SPI配置应用文档比如XAPP523这一系列虽然当时主要针对Spartan-6但里面的SPI时序分析、配置过程说明至今仍有参考价值。7系列具体引脚行为则以UG470为准。3. 系统级设计数据通道、Flash空间规划与升级协议3.1 数据通道按带宽选型串口、千兆网、PCIeSTARTUPE2只解决“能烧”的问题烧录数据从哪里来、以什么方式进FPGA是系统设计要决策的事。数据通道典型速率16MB镜像传输耗时适用场景UART115200bps约24分钟本地维护、调试UART921600bps约3分钟快速维护千兆以太网900Mbps秒级传输但受Flash擦除制约产品远程升级主力PCIe/DMA数Gbps瓶颈在Flash写入板内高速更新串口方案胜在实现简单很多FPGA工程本来就有串口调试模块搭一个帧协议就能用。要注意即便是460800bps4MB镜像也要约73秒加上Flash擦除时间整个升级流程可能超过2分钟。所以串口方案更适用于配置镜像较小的逻辑。千兆网是我这次项目的实际选择。数据通道用UDP分包发送FPGA侧做简单的序号校验和CRC校验。整包传输到FPGA内部RAM的速度根本不是瓶颈真正的瓶颈在Flash擦除尤其是整片或大扇区擦除那种毫秒级到秒级的等待。3.2 Flash空间规划与镜像布局不要整个Flash都用来放一套bitstream的裸数据那会给升级和安全带来很大麻烦。合理的做法是分区管理。以W25Q12816MB为例我的分区习惯是这样地址范围大小用途0x000000 - 0x3FFFFF4MBGolden镜像永不擦除0x400000 - 0x7FFFFF4MB升级区A正常加载的镜像0x800000 - 0xBFFFFF4MB升级区B新镜像暂存区0xC00000 - 0xFFFFFF4MB应用数据、日志、参数如果bitstream只有2MB可以把分区调小但保留“永不擦除的Golden区 正常加载区 暂存区”这三个角色是必要的。Golden镜像是一个出厂固化且能找到的最基础版本哪怕正常加载区被写坏设备上电后也能从Golden跑起来。Flash容量选型时建议至少装下两份镜像再加数据区。有些项目为了省钱选刚好装一份镜像的Flash后面做双镜像回滚时完全没法扩展只能换料得不偿失。3.3 升级协议设计帧格式、校验与应答远程升级协议不需要搞得多花哨稳定可靠就行。我的设计思路是三层命令层、数据层、应答层。帧格式定长头 变长数据 | 0 | 1 | 2字节帧头 0xAA55 | | 2 | 1 | 命令字0x01擦除,0x02写数据,0x03校验,0x04跳转 | | 3 | 2 | 帧序号用于重传和乱序判断 | | 5 | 4 | 目标Flash地址32bit | | 9 | 2 | 数据长度最多256字节 | | 11 | n | 有效数据 | | 11n | 4 | CRC32校验 |每一包数据被FPGA接收后FPGA会回一个应答帧包含帧序号、处理结果成功/失败/重发请求。上位机发出一个包后只有收到ACK才会发下一包超时则重发。这个简单的停等协议虽然效率不高但抗网络抖动能力强排查问题也直观。数据长度定成256字节不是随便选的市面上主流SPI Flash页编程最大就是256字节一页一帧对应一页地址对齐简单处理逻辑清晰。如果一帧超过256字节Flash侧还得拆页徒增状态机复杂度。3.4 升级流程闭环与状态上报整个升级流程的闭环是上位机下发“开始升级” → FPGA擦除目标区 → 上位机分包发送新bit流 → FPGA逐页写入并CRC校验 → 上位机下发“校验”命令 → FPGA全区域回读比对 → 上位机下发“跳转启动”命令 → FPGA触发重配置 → 设备从新镜像启动 → 新逻辑上电后上报版本号。很容易漏掉最后一步“新逻辑上报版本”。很多项目升级完了以为重启成功就万事大吉结果新镜像和新外设不匹配设备起不来又没有回退机制只能现场处理。正确的做法是在每个镜像里固定一个版本寄存器启动后通过同一套远程通道上报给上位机上位机确认版本号变更且运行状态正常才认为升级成功。4. 动态烧录状态机实现从擦除到校验的完整链路4.1 常用SPI Flash指令集与关键时序动态烧录本质就是用SPI Master去访问Flash芯片。篇幅关系我以W25Q系列为例这套指令在大多数SPI NOR Flash上通用。指令编码功能备注Read ID0x9F读厂商ID/设备ID上电自检常用Write Enable0x06写使能置WEL位写/擦前必须执行Read Status0x05读状态寄存器WIP位在bit0Sector Erase0x204KB扇区擦除耗时约50ms~1sPage Program0x02256B页编程每次最多256BRead Data0x03普通读数据校验用这里有个特别容易踩的细节写使能0x06不是发一次就行。每次擦除、每次页编程之前都必须重新发0x06因为芯片在写完或擦完之后会自动清除WEL位。如果你把Write Enable放在初始化阶段只发一次后面的页编程会静默失败状态寄存器里的WEL位根本没置起来。轮询WIP位也是一门学问。发完擦除或页编程命令后要通过0x05命令读取状态寄存器直到bit0变为0才表示Flash内部操作完成。这个轮询周期不能太狠建议每次间隔几十微秒。也不需要去精确延时SPI读命令本身就能当作轮询节奏。4.2 主状态机擦除、页编程、轮询与校验烧录状态机是我整个模块的核心。大致状态划分如下IDLE等待升级开始命令ERASE_SECTOR擦除当前扇区WAIT_ERASE_DONE轮询WIP直到擦除完成PAGE_PROGRAM执行一页写入WAIT_PROGRAM_DONE轮询WIP直到该页写完成READ_BACK按页读回Flash数据VERIFY与RAM中缓存的数据比对JUMP_BOOT写入跳转命令触发重配置简化版的Verilog状态机框架localparam IDLE 4d0; localparam WRITE_ENABLE 4d1; localparam ERASE_CMD 4d2; localparam WAIT_ERASE 4d3; localparam PROGRAM_CMD 4d4; localparam WAIT_PROGRAM 4d5; localparam READ_CMD 4d6; localparam VERIFY 4d7; localparam BOOT_TRIGGER 4d8; localparam ERROR_STATE 4d9; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin case (state) IDLE: begin if (upgrade_start) state WRITE_ENABLE; end WRITE_ENABLE: begin // 发送0x06CS拉低发送后拉高 if (tx_done) state ERASE_CMD; end ERASE_CMD: begin // 发送0x20 24bit地址 if (tx_done) state WAIT_ERASE; end WAIT_ERASE: begin // 循环发送0x05读取状态寄存器 if (flash_busy 1b0) state PROGRAM_CMD; else if (timeout) state ERROR_STATE; end // ... 后续状态类似 endcase end end状态机的关键在于每个SPI操作都要有明确的“CS拉低→发送数据→CS拉高”的完整时序任何一步CS没有拉回高电平后续操作都会乱。我见过太多人把状态机写成了“发完命令就发呆”结果CS一直低着Flash根本不知道你在干什么。4.3 SPI时钟频率选择与约束要点SPI时钟频率不是越高越好。Flash芯片标称支持104MHz那是针对连续读等高规格场景页编程时Flash内部本身有写操作时间SPI时钟在这里只是搬运数据高速搬进去后还是要等Flash慢慢写。真正限制频率的是FPGA内部逻辑到引脚的通路延迟以及CCLK引脚在动态切换过程中的时序余量。我的建议是用户模式下SPI时钟控制在25MHz以内。实测25MHz读校验和页编程都非常稳定搬到50MHz以后同样的代码偶发出现页编程校验不一致。这个问题的根源不是Flash不支持50MHz而是STARTUPE2输出的CCLK到Flash引脚的路径上时序余量和板级信号完整性并不像配置模式那样经过严格仿真。另外USRCCLKO的时钟源不要直接拿系统高频时钟分频得到的毛刺信号用。要么用BUFR/MMCM输出干净时钟要么用简单的寄存器输出二分频。保证时钟占空比稳定Flash在上升沿采数据时才不会出错。4.4 校验策略与断点续传页编程完成后最直接有效的手段是“写后即读”。每写完256字节马上用0x03读回来与缓冲区数据逐字节比较。不一致就重新写这一页最多重试三次。这个策略能挡住绝大部分硬件噪声和时序问题。有些人只在全部写完后做一次整体回读这样不是不行但一旦出错就不知道错在哪一段得整包重来。按页校验的好处是定位精确异常时只需要重写那一页对应的扇区升级中断后也能从最后成功的那一页继续传。配合上位机按页通协议断点续传能力自然就有了。断点续传的实现也不需要额外保存太多状态。FPGA侧只需要维护一个“当前成功写入到哪个Flash地址”的变量上位机升级中断后重新连上先读一次这个变量然后从该地址继续发包。省去了每次都从第一页擦、第一页写的漫长等待。5. 实测中容易翻车的五个位置与规避方法5.1 片选时序不对Flash忙位永远轮询不到这个坑我调了整整一个晚上。现象是页编程命令发完后Flash状态寄存器里的WIP位一直为1程序卡在WAIT_PROGRAM状态直到超时。起初我以为是Flash坏了换了芯片也一样最后才发现是轮询状态的CS时序错了。读状态寄存器0x05时CS拉低后要等一个短延时再发命令字命令字发完后读一个字节再把CS拉高。我的代码在CS拉低后立刻发命令板级走线长了之后第一个bit采样就不稳定命令字实际没有正确到达Flash。改法是CS拉低后加一个SPI时钟周期的延时同时确保读到的那个字节走完再拉高CS。这个经验对所有SPI外设都适用。5.2 SPI时钟频率过高导致页编程偶发失败这是另一个典型的玄学问题。现象不是每次都失败而是写大量数据后偶发某一页校验失败重试一次又好了。排查到最后发现是SPI时钟频率偏高把USRCCLKO从50MHz降到25MHz之后连续写了几百页全部通过。这里要明白SPI时钟的作用范围。配置模式下CCLK是由配置引擎精心管理的用户模式下你把时钟引到CCLK引脚这条路径经过STARTUPE2原语实际延迟会比普通IO路径复杂。频率越高翻转窗口越窄任何一点板级反射都可能让采样点落进亚稳态区域。对于烧录这种操作速度远没有稳定重要。5.3 WP#/HOLD#悬空埋下的隐形雷很多SPI Flash的WP#和HOLD#引脚内部有弱上拉悬空状态下基本能用但是在某些操作组合下会埋雷。W25Q系列在做状态寄存器写保护相关操作时如果WP#恰好被干扰拉低写使能就会被屏蔽页编程表现为“写操作没报错但数据没进去”。排查这种问题特别烧脑因为表面看指令、时序都对就是数据不对。后来我在原理图上补了10kΩ上拉电阻把WP#和HOLD#都固定到高电平问题彻底消失。新板卡设计时这两个引脚一定不要省上拉电阻。5.4 升级过程中断后设备变砖升级到一半断电或者网络中断导致烧录不完全Flash里现有镜像被擦掉一半下次上电就加载失败。如果设备只有一份镜像那就直接变砖了。这也是我为什么在前面强调Flash空间规划里必须有Golden区和双镜像。规避思路分两个层面。硬件层面Flash分区黄金镜像永不擦除。软件层面升级新镜像时先写暂存区写完整并且回读校验通过后才允许系统切换到新镜像启动。永远不要先把正在运行的工作区擦掉再慢慢写新镜像。5.5 GSR/GTS误触发导致烧录状态机异常STARTUPE2的GSR和GTS引脚默认接0就能正常工作但有些设计图省事把这两个引脚悬空。悬空引脚在复杂电磁环境下可能被干扰拉高从而触发全局复位或者全局三态Flash操作瞬间被打断。我的做法是GSR接常量0GTS接常量0KEYCLEARB接常量1PACK接常量1。四个引脚全部写死逻辑里不依赖任何外部信号。这些引脚本来就不是给普通用户玩的接死防止误触发是最稳的方式。另一个细节是DONE相关引脚。USRDONETS建议固定为1保持DONE引脚的输出三态免得用户逻辑意外驱动DONE影响配置状态机。6. 双镜像与MultiBoot给升级失败留一条后路6.1 MultiBoot的回退机制与关键寄存器7系列FPGA的MultiBoot机制天生适合做升级回退。简单说配置逻辑先加载起始地址的Golden镜像运行起来之后如果FPGA收到IPROG指令它会停止当前逻辑执行内部热启动然后从指定地址加载另一份镜像。如果加载过程中发现IDCODE错误、CRC错误或者配置超时配置逻辑会自动回退到WBSTAR寄存器里设定的地址重新加载。这个WBSTARWarm Boot Start Address寄存器非常关键它决定了失败后回退到哪个地址。通常我们把WBSTAR设成Golden镜像所在地址这样一旦升级区镜像启动失败设备会自动回到Golden版本至少保证设备还能联网、还能被远程再次升级。IPROG指令可以通过ICAPE2原语从用户逻辑发出也可以在生成比特流时通过属性配置。实际做远程升级时上位机下发的“跳转启动”命令就会触发FPGA内部执行IPROG从而实现重配置。6.2 黄金镜像区与升级区地址规划实战回退机制要想可靠地址规划不能拍脑袋。我这里给一个实际可用的例子假设bitstream文件约4MB分区地址作用Golden镜像区0x000000 - 0x3FFFFF出厂基础固件永不被擦除正式工作区0x400000 - 0x7FFFFF当前正常运行的增强版本暂存工作区0x800000 - 0xBFFFFF远程升级时先写入新镜像数据区0xC00000 - 0xFFFFFF运行参数、日志、版本号升级时上位机先把新bitstream发到暂存工作区。FPGA完成写入并回读校验后再把暂存区的镜像整体复制到正式工作区或者通过配置跳转直接从暂存区启动。两种方式各有优劣。我的习惯是正式工作区保持一份可用镜像暂存区只放“待验证的新版本”。新版本在暂存区跑起来后上位机验证运行正常再在下一次升级时把正式工作区覆盖。这样最稳。6.3 稳定升级的推荐操作顺序综合前面所有内容我给出一套经过实测的推荐操作顺序你可以直接套用上位机下发升级指令FPGA读取当前版本号上报。上位机下发擦除暂存区指令FPGA按扇区擦除暂存区。上位机分包发送新bitstreamFPGA逐页写入暂存区每页回读校验。全部写入并校验通过后上位机下发校验命令FPGA完整回读暂存区并计算CRC。上位机下发跳转启动命令FPGA执行IPROG设备从暂存区启动。新镜像启动后主动上报版本号和运行状态。上位机确认新镜像正常后再下发指令把暂存区镜像同步到正式工作区。如果新镜像启动失败MultiBoot自动回退Golden设备保持可维护状态上位机收到异常状态后重新发起升级。这套顺序每一环都有校验任何一步失败都能明确知道问题出在数据传输、Flash擦写还是镜像本身。虽然过程比“直接写Flash然后重启”复杂但产品运维阶段这种复杂度换来的是可控性和安全感。我这里最后再补一句个人体会项目从小批量到量产远程升级代码被反复回看的概率远高于普通逻辑。把状态机写清晰、把异常分支写完整、把Flash分区打上注释未来不管是自己维护还是同事接手都会省下大把时间。我见过太多“能用就行”的远程升级代码一到现场设备需要回退时整个团队对着时序图痛苦排查。与其那样不如一开始就把STARTUPE2、Flash分区和回滚机制当成系统的一部分来设计。