项目交付后的第一次功能改动,往往是远程升级需求被真正提上日程的节点。我手里这套基于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 | 需要 | 无影响 | 低 | 板卡本来就有管理MCU |
| B:FPGA Fabric直接控制Flash(STARTUPE2方案) | 不需要 | 无影响 | 中 | 无MCU或想保持独立 |
| C:BSCANE2 + 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,烧录时拉0 |
| USRDONEO | 输入 | 用户DONE信号输出 | 固定1 |
| USRDONETS | 输入 | DONE三态控制 | 固定1 |
| CFGMCLK | 输出 | 配置时钟输出,可观察配置时钟 | 不用可不接 |
| GSR | 输入 | 全局复位输入 | 固定0 |
| GTS | 输入 | 全局三态输入 | 固定0 |
| KEYCLEARB | 输入 | 密钥清除 | 固定1 |
| PACK | 输入 | 打包控制 | 固定1 |
最小例化代码非常简单,在模块里直接例化原语就可以:
wire spi_clk; // 用户逻辑生成的SPI时钟 STARTUPE2 #( .PROG_USR("FALSE"), .SIM_CCLK_FREQ(0.0) ) STARTUPE2_inst ( .CFGMCLK ( ), .CLK (1'b0 ), .GSR (1'b0 ), .GTS (1'b0 ), .KEYCLEARB (1'b1 ), .PACK (1'b1 ), .USRCCLKO (spi_clk ), .USRCCLKTS (1'b0 ), .USRDONEO (1'b1 ), .USRDONETS (1'b1 ) );当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 数据通道按带宽选型:串口、千兆网、PCIe
STARTUPE2只解决“能烧”的问题,烧录数据从哪里来、以什么方式进FPGA,是系统设计要决策的事。
| 数据通道 | 典型速率 | 16MB镜像传输耗时 | 适用场景 |
|---|---|---|---|
| UART | 115200bps | 约24分钟 | 本地维护、调试 |
| UART | 921600bps | 约3分钟 | 快速维护 |
| 千兆以太网 | 900Mbps+ | 秒级传输但受Flash擦除制约 | 产品远程升级主力 |
| PCIe/DMA | 数Gbps | 瓶颈在Flash写入 | 板内高速更新 |
串口方案胜在实现简单,很多FPGA工程本来就有串口调试模块,搭一个帧协议就能用。要注意,即便是460800bps,4MB镜像也要约73秒,加上Flash擦除时间,整个升级流程可能超过2分钟。所以串口方案更适用于配置镜像较小的逻辑。
千兆网是我这次项目的实际选择。数据通道用UDP分包发送,FPGA侧做简单的序号校验和CRC校验。整包传输到FPGA内部RAM的速度根本不是瓶颈,真正的瓶颈在Flash擦除,尤其是整片或大扇区擦除那种毫秒级到秒级的等待。
3.2 Flash空间规划与镜像布局
不要整个Flash都用来放一套bitstream的裸数据,那会给升级和安全带来很大麻烦。合理的做法是分区管理。
以W25Q128(16MB)为例,我的分区习惯是这样:
| 地址范围 | 大小 | 用途 |
|---|---|---|
| 0x000000 - 0x3FFFFF | 4MB | Golden镜像,永不擦除 |
| 0x400000 - 0x7FFFFF | 4MB | 升级区A,正常加载的镜像 |
| 0x800000 - 0xBFFFFF | 4MB | 升级区B,新镜像暂存区 |
| 0xC00000 - 0xFFFFFF | 4MB | 应用数据、日志、参数 |
如果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 | 有效数据 | | 11+n | 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 ID | 0x9F | 读厂商ID/设备ID | 上电自检常用 |
| Write Enable | 0x06 | 写使能,置WEL位 | 写/擦前必须执行 |
| Read Status | 0x05 | 读状态寄存器 | WIP位在bit0 |
| Sector Erase | 0x20 | 4KB扇区擦除 | 耗时约50ms~1s |
| Page Program | 0x02 | 256B页编程 | 每次最多256B |
| Read Data | 0x03 | 普通读数据 | 校验用 |
这里有个特别容易踩的细节:写使能(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 = 4'd0; localparam WRITE_ENABLE = 4'd1; localparam ERASE_CMD = 4'd2; localparam WAIT_ERASE = 4'd3; localparam PROGRAM_CMD = 4'd4; localparam WAIT_PROGRAM = 4'd5; localparam READ_CMD = 4'd6; localparam VERIFY = 4'd7; localparam BOOT_TRIGGER = 4'd8; localparam ERROR_STATE = 4'd9; 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 // 发送0x06,CS拉低,发送后拉高 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 == 1'b0) 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接常量0,GTS接常量0,KEYCLEARB接常量1,PACK接常量1。四个引脚全部写死,逻辑里不依赖任何外部信号。这些引脚本来就不是给普通用户玩的,接死防止误触发是最稳的方式。
另一个细节是DONE相关引脚。USRDONETS建议固定为1,保持DONE引脚的输出三态,免得用户逻辑意外驱动DONE影响配置状态机。
6. 双镜像与MultiBoot:给升级失败留一条后路
6.1 MultiBoot的回退机制与关键寄存器
7系列FPGA的MultiBoot机制天生适合做升级回退。简单说,配置逻辑先加载起始地址的Golden镜像,运行起来之后,如果FPGA收到IPROG指令,它会停止当前逻辑,执行内部热启动,然后从指定地址加载另一份镜像。如果加载过程中发现IDCODE错误、CRC错误或者配置超时,配置逻辑会自动回退到WBSTAR寄存器里设定的地址重新加载。
这个WBSTAR(Warm 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按扇区擦除暂存区。
- 上位机分包发送新bitstream,FPGA逐页写入暂存区,每页回读校验。
- 全部写入并校验通过后,上位机下发校验命令,FPGA完整回读暂存区并计算CRC。
- 上位机下发跳转启动命令,FPGA执行IPROG,设备从暂存区启动。
- 新镜像启动后,主动上报版本号和运行状态。
- 上位机确认新镜像正常后,再下发指令把暂存区镜像同步到正式工作区。
- 如果新镜像启动失败,MultiBoot自动回退Golden,设备保持可维护状态,上位机收到异常状态后重新发起升级。
这套顺序每一环都有校验,任何一步失败都能明确知道问题出在数据传输、Flash擦写还是镜像本身。虽然过程比“直接写Flash然后重启”复杂,但产品运维阶段,这种复杂度换来的是可控性和安全感。
我这里最后再补一句个人体会:项目从小批量到量产,远程升级代码被反复回看的概率远高于普通逻辑。把状态机写清晰、把异常分支写完整、把Flash分区打上注释,未来不管是自己维护还是同事接手,都会省下大把时间。我见过太多“能用就行”的远程升级代码,一到现场设备需要回退时,整个团队对着时序图痛苦排查。与其那样,不如一开始就把STARTUPE2、Flash分区和回滚机制当成系统的一部分来设计。