1. 在线升级FPGA也能“热插拔”大家好我是老张在FPGA和嵌入式这块摸爬滚打十几年了。今天想和大家聊聊一个非常实用但很多朋友觉得有点“玄乎”的话题Xilinx FPGA的在线升级。说白了就是让你的FPGA硬件在不掉电、不插JTAG的情况下通过网络或者其他通信方式自己给自己“换脑子”加载一个新的功能程序。这功能听起来是不是很酷其实它的核心思想和咱们玩单片机在线升级IAP是一模一样的就是通信 Flash操作 程序跳转这三板斧。只不过FPGA的“脑子”结构更复杂对应的操作细节和坑点也更多一些。我见过不少项目前期功能都调通了最后卡在升级这个“最后一公里”上要么升级失败变砖要么升级后跑飞非常头疼。所以今天我就结合自己踩过的坑把Xilinx FPGA以7系列为例在线升级的完整链路从最底层的通信驱动、Flash芯片操作到最核心的ICAP原语跳转再到容易混淆的MCS、BIT文件处理给大家掰开揉碎了讲清楚。我的目标就一个让你看完就能动手在自己的板子上把这条路跑通。咱们不搞纯理论就讲实战中怎么操作参数怎么设代码怎么写。放心我会尽量用大白话和生活中的例子来类比保证你即使刚接触FPGA也能跟上节奏。2. 通信驱动数据怎么“送”进来在线升级的第一步就是你得有个通道把存放在服务器或者电脑里的新程序文件我们叫它“升级镜像”传输到FPGA板子上。这个通道就是通信驱动。2.1 通信协议的选择为什么我偏爱UDP原始文章里提到了UDP这也是我最常用的方式。可能有人会问TCP不是更可靠吗为啥不用TCP这里就得结合FPGA升级的场景来分析了。TCP确实可靠能保证数据包按顺序、不丢失地到达。但它也复杂需要维护连接状态、处理重传、流量控制等。在FPGA里用纯逻辑实现一个完整的TCP/IP栈资源消耗大而且对于升级这种“一次性”的大数据量传输它的握手、确认机制反而可能成为瓶颈速度上不去。UDP则简单粗暴它只管发不管对方收没收到、顺序对不对。听起来不靠谱对吧但正是这种“不靠谱”给了我们极大的灵活性。在局域网这种网络环境比较好的情况下丢包率极低。我们可以在应用层自己实现简单的可靠传输机制比如给每个数据包加个序号接收方按序号校验和重组丢包了就发个NACK让发送方重传这一个包。这样我们既享受了UDP的高效又补上了可靠性的短板而且逻辑完全自己掌控非常轻量。我实测下来在千兆以太网环境下用UDP加自定义重传协议传输一个几兆的镜像文件速度能跑满线速而且稳得很。代码结构也清晰一个发送状态机一个接收状态机再加一个简单的包管理逻辑就搞定了。2.2 驱动设计要点与代码骨架设计UDP驱动时有几点需要特别注意MTU最大传输单元以太网帧通常有1500字节的限制去掉IP头和UDP头我们能用的数据载荷大概在1472字节左右。为了保险起见我一般把每个UDP数据包的有效数据定为1400字节。这样我们的升级镜像文件需要被切成很多个1400字节的“块”。数据包结构光传数据不行我们得让FPGA知道这个包是干嘛的。我设计的数据包头部很简单包头标识4字节比如 0xAA55AA55用于帧起始同步。包序列号4字节从0开始递增用于检测丢包和乱序。数据长度2字节指示本包中实际有效数据的长度最后一个包可能不满1400字节。命令字2字节用来区分不同类型的包例如0x0001代表“镜像数据”0x0002代表“升级开始命令”0x0003代表“升级结束并校验命令”。校验和2字节可选对头部或整个数据包进行CRC16校验多一层保险。下面是一个简化的Verilog接收端状态机思路不是完整代码但展示了核心流程module udp_upgrade_rx ( input wire clk, input wire rst_n, // 假设从MAC层收到解包后的数据 input wire [10:0] data_len_i, input wire [7:0] data_i [0:1400], input wire data_valid_i, output reg upgrade_data_valid, output reg [31:0] upgrade_data_addr, // 写入Flash的地址 output reg [7:0] upgrade_data [0:255], // 按页输出一次256字节 output reg upgrade_page_wr // 页写脉冲 ); localparam CMD_DATA 16h0001; reg [3:0] state; reg [31:0] total_bytes_received; reg [7:0] packet_buffer [0:1399]; reg [10:0] write_page_counter; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; upgrade_page_wr 1b0; end else begin case(state) IDLE: begin if (data_valid_i 解析出命令字CMD_DATA) begin // 将数据存入packet_buffer state PROCESS_PACKET; end end PROCESS_PACKET: begin // 将packet_buffer中的数据按照Flash页编程的要求256字节一页 // 拆分并赋值给 upgrade_data同时生成 upgrade_data_addr // 每凑齐256字节就产生一个 upgrade_page_wr 脉冲高电平一个周期 // 这个脉冲会触发后续的Flash页编程模块 if (write_page_counter 255) begin upgrade_page_wr 1b1; write_page_counter 0; // 更新地址... end else begin write_page_counter write_page_counter 1; end // 处理完一个包后回到IDLE state IDLE; end endcase end end endmodule这个模块的作用就是把网络来的数据流整理成Flash喜欢的“一页一页”的格式。实际项目中你还需要处理丢包重传、握手协议等这里只是展示了最核心的数据转换流程。3. Flash芯片驱动给新程序找个“家”数据传进来了得有个地方存起来。这个地方就是板载的SPI Flash比如常用的M25P80、W25Q128等。FPGA上电后默认会从Flash的起始地址读取配置比特流来启动。我们的升级就是要安全地擦除旧程序写入新程序并且不能影响当前正在运行的FPGA逻辑即Bootloader。3.1 芯片选型与分区规划首先一定要仔细阅读芯片数据手册这是硬件工程师的圣经。以经典的**M25P808Mbit1MB**为例我们看看关键参数参数值说明总容量8 Mbit (1 MB)够存一个中等规模的FPGA镜像扇区大小512 Kbit (64 KB)擦除的最小单位必须整扇区擦扇区数量16个地址从0x000000到0xFFFFFF页大小256 Byte编程写入的最小单位可以写一页中的部分字节SPI模式支持Mode 0和Mode 3我常用CPOL1, CPHA1Mode 3分区是重中之重。你不能把新程序随便往Flash里写万一覆盖了正在运行的程序板子立马“砖化”。标准的做法是做一个双备份A/B分区或者Bootloader App分区。Bootloader分区固定在Flash起始地址如0x000000。它非常小只负责最基本的通信、Flash操作和跳转逻辑。一旦烧录永不更新或者通过非常特殊的方式更新。App分区A和B放在Bootloader之后。假设我们有两个1MB的镜像可以这样规划分区A地址 0x10000 ~ 0x1FFFF分区B地址 0x20000 ~ 0x2FFFF中间留点空隙防止意外越界。Bootloader里会有一个“标志位”比如存在Flash的某个固定页记录当前运行的是A分区还是B分区。升级时我们就擦除非当前运行的分区将新镜像写入那个空闲分区最后更新标志位并跳转。这样即使升级中途断电也至少有一个分区是完好的下次上电还能用另一个分区启动实现了“变砖免疫”。3.2 扇区擦除与页编程实战操作Flash最核心的两个命令就是扇区擦除Sector Erase和页编程Page Program。擦除是把比特变成1编程是把需要的比特从1变成0。注意只能从1变0不能从0变1所以写之前必须先擦除擦成全1。扇区擦除流程以M25P80为例拉低片选CS#。发送写使能指令0x061个字节。这个指令必须发不然芯片拒绝写入。拉高片选等待至少t_WEL时间手册里查通常很短几微秒。再次拉低片选。发送扇区擦除指令0xD81个字节。发送24位的扇区起始地址3个字节。关键点你只需要发送这个扇区内的任意一个地址芯片就会擦除整个扇区64KB。比如发地址0x10000就会擦除以0x10000开始的64KB区域。拉高片选。此时芯片开始内部擦除操作耗时t_SE典型值0.4秒~1秒。这段时间内你可以通过读状态寄存器0x05的BUSY位来等待或者简单延时。页编程流程拉低片选。发送写使能指令0x06。拉高片选等待t_WEL。再次拉低片选。发送页编程指令0x021个字节。发送24位的页起始地址3个字节。这次地址必须精确到页的开始即地址的低8位为0。连续发送最多256字节的数据。如果数据少于256字节发送完即可。拉高片选。芯片开始内部编程耗时t_PP典型值1~3毫秒。这里有个大坑手册里明确写着如果你要写入的数据跨页了比如从地址250开始写10个字节芯片不会自动写到下一页的0-5地址而是从本页的起始地址0开始覆盖也就是说你250-255地址的数据写了第256个字节会写到地址0第257个字节写到地址1……这会导致数据错乱。所以你的驱动必须处理好页边界一旦本次写入要跨页必须分成两次页编程操作。我的做法是在写Flash的模块里维护一个写指针。每次收到256字节的数据来自前面的UDP模块就发起一次页编程。如果收到的数据不是整页比如最后一个包就补0xFF擦除后就是0xFF凑成一页或者记录有效长度下次合并。核心就是要保证每次调用页编程函数地址都是页对齐的数据长度不超过256。4. ICAP原语跳转让FPGA“换脑”的魔法前面两步通信和存储都是在为新的程序镜像做准备。最后也是最关键的一步就是告诉FPGA“别跑现在的程序了去Flash的那个新地址重新加载运行” 这个操作就是通过Xilinx独有的ICAPInternal Configuration Access Port原语完成的。你可以把ICAP想象成FPGA内部的一个“后门”。平时FPGA通过外部的配置引脚如SPI、SelectMAP从Flash加载程序。而ICAP则允许FPGA内部正在运行的程序去主动重新配置自己这就为实现动态重配置、在线升级提供了硬件基础。4.1 ICAP模块使用详解在Vivado或ISE里ICAP是一个现成的原语Primitive直接例化就能用。它的端口很简单但每个都至关重要// 7 Series FPGA的ICAP原语示例 ICAPE2 #( .DEVICE_ID(32h3651093), // 器件ID一般用默认值 .ICAP_WIDTH(X32), // 数据宽度可选X32或X16 .SIM_CFG_FILE_NAME(NONE) // 仿真文件 ) ICAPE2_inst ( .O(O), // 输出数据读配置寄存器时用 .CLK(CLK), // 时钟必须≤100MHz7系列注意周期限制 .CSIB(CSIB), // 片选低有效。我们一直拉低就行。 .I(I), // **输入数据我们要发送的配置命令流** .RDWRB(RDWRB) // 读写控制**低电平写高电平读**。我们升级只写所以拉低。 );CLK时钟脚。特别注意ICAP有最小周期限制。对于7系列是50ns即时钟不能快于20MHz。我一般就用一个20MHz或更低的时钟去驱动它求稳。用系统主频如100MHz直接驱动是常见错误会导致配置失败。I16位或32位输入由参数选择。这是我们向ICAP“喂”命令和数据的地方。在线升级的核心就是构造一段正确的比特流数据通过这个端口发送出去。RDWRB拉低表示写操作。我们全程拉低。CSIB片选拉低使能。在整个配置过程中保持低电平。O和BUSY在升级跳转这个场景下我们只写不读所以这两个信号可以不用关心。4.2 构造ICAP配置命令流这是整个在线升级技术中最“硬核”的部分。我们需要通过ICAP接口发送一系列标准的配置命令让FPGA执行一次“回读Readback- 擦除Erase- 编程Program- 启动Startup”的流程只不过这次编程的数据源不是外部Flash而是我们通过命令指定的新地址。命令流是基于Xilinx配置帧的格式。你需要查阅对应器件系列的配置手册Configuration User Guide比如UG4707 Series。里面会详细列出所有命令码Opcode。一个最简化的、用于从Flash新地址启动的命令流序列如下概念性描述非直接代码同步字SYNC发送0xAA995566用于和配置逻辑同步。发送NULL命令发送0x20000000空操作用于填充。发送RCRC命令发送0x30008001清除CRC寄存器。发送AGHIGH命令发送0x30008001这个命令很关键它告诉配置控制器“我接下来要发的地址是相对于MultiBoot起始地址即Golden Image地址的偏移量”。这是我们实现跳转到任意地址的基础。发送WBSTAR命令发送0x30020000 [新镜像的起始地址]。这是最核心的一步WBSTARWarm Boot Start Address Register寄存器里存放的就是下次配置加载的起始地址。比如新程序在Flash的0x20000这里就发送0x30020000 0x00020000注意地址对齐和位宽具体看手册。发送IPROG命令发送0x0000000F。这个命令就是“执行重配置”的触发器。一旦FPGA收到这个命令它会立即停止当前逻辑根据WBSTAR寄存器里的地址去Flash那里读取新的配置比特流然后重新配置自己完成“换脑”。这些命令码都是32位的。如果你的ICAP设置为16位模式ICAP_WIDTH“X16”那么你需要把每个32位命令拆成两个16位数据分两次从I端口输入先低16位后高16位。在实际的Verilog代码中你需要一个状态机一个ROM或者FIFO里面按顺序存储好这一系列命令字然后在ICAP的时钟驱动下依次送到I端口。同时要确保命令之间的间隔满足ICAP的时序要求。4.3 生成支持MultiBoot的烧录文件你可能会问我直接生成一个.bit文件烧进Flash不就行了吗为什么还要专门提MultiBoot文件这里又是一个关键点。普通的.bit文件其头部包含了引导信息默认是从Flash的0x0地址开始加载。如果我们直接把新镜像的.bit文件写入Flash的0x20000地址然后通过ICAP跳转到0x20000FPGA会找不到正确的配置头而失败。因此我们需要使用Xilinx工具bootgen生成一种特殊的文件——MCS文件并且要在生成时指定MultiBoot属性。这个过程中工具会为我们的镜像生成一个特殊的头部里面包含了镜像的长度、CRC校验以及相对于Golden Image的偏移量等信息。在Vivado中你可以通过TCL命令或GUI生成.bin文件然后用bootgen工具。一个简单的bootgen.bif文件示例如下// 架构、器件、文件名 the_ROM_image: { [bootloader] fsbl.elf // 第一阶段的Bootloader可选 [offset 0x10000] app1.bit // 应用镜像1放在0x10000 [offset 0x20000] app2.bit // 应用镜像2放在0x20000 }然后运行命令bootgen -image bootgen.bif -arch zynq -o i BOOT.bin -w on。生成的BOOT.bin或者对应的MCS文件就包含了正确的偏移信息。FPGA的配置控制器在读取非0地址的镜像时会识别这个头部从而正确加载。5. MCS与BIT文件剥开洋葱看内核很多朋友对MCS文件和BIT文件的关系感到困惑。原始文章提到了它们的区别这里我再深入讲一下这对我们理解升级数据的本质很有帮助。BIT文件是Vivado/ISE生成的最直接的配置文件它包含了FPGA配置所需的全部比特流信息。但是它前面带了一个文件头。这个头里包含了诸如设计日期、器件型号、CRC校验等元信息。如果你用十六进制编辑器打开一个.bit文件前面很长一段都不是真正的配置数据。MCS文件是一种ASCII码格式的文本文件它是Intel定义的一种用于烧录存储器的标准格式。它的每一行都像一条记录包含了地址、数据长度、数据类型和校验和。Xilinx工具生成的MCS文件其数据部分就是去掉了文件头的、纯的配置比特流。它们的关系可以这样理解BIT文件 文件头约109字节不同工具版本可能不同 纯配置数据MCS文件的数据区 纯配置数据所以我们通过网络传输给FPGA的应该是那个“纯配置数据”。有两个方法获取它使用MCS文件用编程器软件如Vivado Hardware Manager将MCS文件烧录到Flash时软件会自动解析MCS格式只把数据部分写进去。我们在PC端做升级服务器时可以直接读取MCS文件跳过前面的冒号:等格式字符提取出数据部分进行传输。处理BIT文件如果你只有.bit文件可以用一个小工具比如Xilinx SDK里的bootgen或者自己写脚本把前109个字节具体长度需确认的文件头去掉剩下的部分就是和MCS文件数据区一样的内容。在Linux下用dd命令就能轻松实现dd ifdesign.bit ofdesign.bin bs1 skip109。在实际的升级流程中我推荐在服务器端预处理准备好纯二进制.bin的镜像文件。FPGA的Bootloader接收到的也应该是这个纯数据流。这样概念最清晰处理起来也最简单。6. 实战全流程串联与避坑指南好了现在我们把所有模块串起来看看一次完整的在线升级是如何发生的上电启动FPGA从Flash 0x0地址加载Bootloader程序并运行。等待命令Bootloader初始化UDP通信、SPI Flash驱动和ICAP模块然后等待网络命令。接收镜像PC端升级工具发送“开始升级”命令并开始将新镜像的纯二进制数据.bin格式分片通过UDP发送。Bootloader按页256字节接收并写入Flash的备用分区假设当前运行在A区则写入B区。校验与确认数据发送完毕后PC端发送“传输完成”命令。Bootloader可选地对写入的Flash区域进行读取校验确保数据正确。然后向PC端回复“升级成功”或“失败”。更新标志与跳转如果成功Bootloader将Flash中一个特定的“标志位”页或扇区更新指明下次应从B区启动。紧接着Bootloader内部的ICAP控制状态机启动依次发送那一系列神秘的32位命令字其中最关键的是设置WBSTAR为新镜像地址B区地址最后发送IPROG命令。重启生效FPGA配置控制器收到IPROG后立即复位并根据WBSTAR的值从Flash的B区地址开始读取新的配置流完成自我重配置。几毫秒后全新的FPGA逻辑开始运行升级完成。我踩过的坑和给你的建议时钟是魔鬼给ICAP的时钟一定不能快严格按照手册来7系列≤20MHz。最好用MMCM/PLL专门分频出一个稳定的低频时钟。地址要对齐Flash的页地址、MultiBoot的偏移地址都要注意字节对齐问题。WBSTAR寄存器设置的地址通常是字节地址但具体格式要看手册有时是高位地址。电源要干净在线升级尤其是Flash写入和ICAP重配置瞬间对电源完整性要求很高。电源纹波过大可能导致配置失败。确保你的板子电源设计有足够的余量和滤波。超时与看门狗Bootloader里一定要加软件看门狗。万一升级过程卡死在某个状态如等待网络包看门狗能复位整个系统恢复到一个安全状态。先仿真后上板用Verilog仿真工具如VCS、ModelSim先完整地跑一遍Bootloader的流程包括模拟UDP数据包输入、Flash模型的行为、ICAP命令发送。这能帮你排除掉大部分逻辑错误。保留调试接口在Bootloader里实现一个简单的UDP回显或者状态查询命令。这样当升级出问题时你至少能通过网络ping通Bootloader查询错误码而不是完全“黑盒”。最后我想说FPGA在线升级确实比单片机复杂因为它涉及到底层硬件配置。但一旦你打通了这个流程你会发现它带来的好处是巨大的产品可以远程修复bug、迭代功能再也不用派人去现场插JTAG了。希望我分享的这些实战细节能帮你少走弯路顺利搞定这个“硬核”功能。如果过程中遇到问题不妨回头再看看Flash的手册和Xilinx的配置指南很多时候答案就在那里。