简介:本资源是一套面向FPGA工程师与嵌入式系统开发者的在线更新(In-System Programming)实践例程,聚焦于Slave SelectMAP模式下主FPGA对从FPGA的配置加载与动态重配置场景,适用于多FPGA协同系统、远程固件升级及硬件功能动态重构等实际应用。压缩包共125个文件,涵盖17个Verilog源码(.v)、4个VHDL文件(.vhd/.vhdl)、6个Xilinx IP核封装(.xci)、5个文本说明(.txt)、3个批处理脚本(.bat)及仿真相关文件(.log/.xml/.wdf等),完整支撑从工程创建、综合、仿真到波形调试全流程,包体大小为6.84MB。已有406人学习下载,资源包含可直接运行的XSIM仿真环境配置(如xsim.ini.bak、simulate.bat)、BD系统级设计(design_1.bd)、调试信息文件(xsim.dbg)及内存初始化文件(.mem),结构清晰、模块分工明确,便于理解SelectMAP协议时序、地址映射机制与双FPGA通信握手逻辑。 搞FPGA在线更新的工程师,大概率都跟slave selectMAP打过照面。板子上电时由CPU或CPLD通过selectMAP把bit流灌进FPGA,这是很常见的配置方式;但如果你需要在系统运行过程中远程更新固件,事情就没这么简单了——片选、读写、忙信号、重配置触发,哪一环对不上都可能让板子直接“变砖”。这次我整理了一套围绕slave selectMAP在线更新场景的HDL例程仿真代码,把从设备接口模型、更新控制状态机、testbench激励一次讲清楚,适合正在做远程升级方案、或者第一次接触selectMAP时序的FPGA工程师参考。
这套例程不依赖特定厂商的硬核,用Verilog/SystemVerilog描述,可以在Vivado XSim、ModelSim、Questa里直接跑。你拿到手之后,既可以拿它验证自己写的更新逻辑,也可以改一改接口位宽,把外部主机从ARM换成CPLD。仿真通过后再上板,能省掉非常多拿示波器抓引脚、反复重新烧录的麻烦。
我会先聊聊在线更新方案选型时为什么绕不开slave selectMAP,再拆解接口时序里最容易写错的几个点,然后给出仿真工程的完整组织方式和关键代码,最后分享我在调试过程中踩过的一些坑。整个思路是从“为什么要这么设计”讲到“代码具体怎么写”,你看完可以直接照着搭一个自己的版本。
1. 在线更新方案选型:为什么绕不开slave selectMAP
1.1 常见在线更新路径的对比
在线更新FPGA固件,业界主流思路大概有三种:MultiBoot(通过SPI Flash双镜像启动)、ICAP内部重配置、以及外部主机通过selectMAP接口直接配置。严格来说,这三种方案解决的目标不太一样。
MultiBoot适合断电重启后自动加载新版本的场景,核心是golden image加update image,上电后由比特流里的WBSTAR寄存器控制从哪个地址启动。好处是逻辑简单,纯靠配置过程完成;缺点是不够灵活,如果新镜像CRC校验失败,只能回退到golden,没法做细粒度的升级控制。
ICAP则是FPGA内部访问配置逻辑的接口,适合动态部分重配置,比如在通信设备里只更新某个DSP模块,不中断其他业务。但ICAP的时序和配置帧结构比较绕,调试门槛高,而且它本质上是“自己配置自己”,无法直接接收外部处理器送来的大批量数据。
slave selectMAP的优势就在于它把主动权交给外部主机。CPU、ARM、CPLD都可以通过一组并行总线,像写SRAM一样把配置数据送进FPGA,速度比JTAG和SPI快一个量级。对在线更新来说,这意味着你可以在系统运行中,由主控处理器按需加载新固件,甚至可以配合外部Flash做断点续传、校验回滚。缺点则是引脚数多、时序约束严格,而且HDL侧需要一套可靠的控制状态机来配合。
用一个不严谨但好记的类比:MultiBoot是“开机时自动选择启动盘”,ICAP是“在系统里自己替换正在运行的软件”,selectMAP则是“外部管理员直接插U盘重新装系统”。
1.2 在板卡上的典型角色
在真实板卡上,selectMAP在线更新链路通常是这样的:主控处理器(比如ARM或Zynq的PS侧)先从以太网或串口接收新固件,存到DDR;然后通过selectMAP接口把配置数据写入FPGA,FPGA侧预先烧录的更新代理逻辑收到完整数据后,再把它写到外部SPI Flash的upgrade分区;最后拉低PROG_B或者执行IPROG指令,让FPGA重新加载新固件。这套流程里,FPGA侧需要处理的不仅仅是数据接收,还要有帧协议解析、CRC校验、Flash控制器调度、重配置触发等逻辑。
这套HDL例程,就是围绕上述流程中FPGA侧的逻辑来设计的。DUT(也就是更新控制器)通过selectMAP接口模型与外部主机通信,收到的数据按自定义帧格式解析,校验通过后写入模拟Flash,全部结束后触发重配置信号。testbench则扮演外部主机的角色,用BFM封装的写时序把激励数据灌进去。
1.3 为什么先跑仿真再上板
我见过不少工程师直接拿真实板子调在线更新,结果是配置时序问题、CWB死锁、重配置没触发这些问题混在一起,一个晚上都排查不完。其实selectMAP这类并行时序接口特别适合仿真验证,因为它的信号关系是确定的,不涉及复杂的模拟特性。
仿真能提前发现的主要有几类问题:状态机跳转条件写错、CWB电平判断逻辑反了、CS_B和RDWR_B的时序窗口不满足、数据总线上出现三态冲突。这些问题在波形上一眼就能看出来,但如果直接上板,你往往要抓十几个信号去对比,费时费力。所以我的建议是,任何涉及selectMAP在线更新的逻辑改动,都必须先过一遍仿真,再进板级验证。
2. selectMAP接口关键时序:决定仿真能不能写对
2.1 引脚定义与位宽选择
写selectMAP相关的HDL之前,先把接口上的每个信号搞清楚。以Xilinx 7系列为例,典型引脚包括:
| 信号名 | 方向 | 功能说明 |
|---|---|---|
| D[7:0]或D[15:0] | 输入/输出 | 配置数据总线,x8或x16模式由配置选项决定 |
| CCLK | 输入 | 配置时钟,由外部主机驱动 |
| CS_B | 输入 | 片选,低有效 |
| RDWR_B | 输入 | 读写控制,低为写,高为读 |
| CWB/BUSY | 输出 | 忙信号,高表示FPGA正在忙,不能写入新数据 |
| DONE | 输出 | 配置完成指示 |
| INIT_B | 双向 | 初始化状态,拉低表示配置内存初始化或CRC失败 |
| PROG_B | 输入 | 重配置触发,拉低复位配置逻辑 |
x8和x16的选择会影响数据位宽和最高CCLK频率。工程里如果只是给MCU做配置通道,x8足够了,引脚占用少;如果追求吞吐量,x16可以用更少的时钟周期传完同样数据。仿真代码里最好把位宽做成参数,比如parameter DATA_WIDTH = 8,切换的时候只改一个地方,避免后续维护时两头改漏。
2.2 写时序的核心逻辑
selectMAP的写时序可以拆成三个步骤:拉低片选、检查忙信号、在CCLK上升沿送数据。具体顺序是:外部主机先把CS_B拉低、RDWR_B置为写模式,然后等CWB为低;CWB变低后,把数据放到D总线上,给出一个CCLK上升沿,数据就被采样进去了。如果CWB为高,主机必须保持当前数据和片选状态,直到CWB被释放,才能继续下一个数据。
这里最容易踩的坑有两个。第一个是把CWB的判断放在CCLK边沿之后,导致总线上已经出现了新数据,但FPGA还没准备好接收,数据被丢掉。第二个是忽略CS_B的时序窗口,某些场景下CS_B需要保持到CWB低电平稳定之后才能释放,否则数据写入提前终止,配置流程会报错。
用生活化一点的说法,CWB相当于生产线上的“暂停”按钮。生产线在跑的时候主机可以按节奏上料,但只要FPGA内部在处理上一批数据(比如CRC计算),它就会拉高CWB提示“先等等”。理解了这一点,仿真里的握手逻辑就很容易写了。
2.3 在线更新时的重配置触发
在线更新和普通上电配置还有一个重要区别:写完全部数据后,需要主动触发FPGA重新加载新固件。常见的做法有两种:一种是由外部主机拉低PROG_B,并保持至少几百纳秒,再释放,让FPGA重新走一遍完整的配置流程;另一种是使用IPROG命令,通过配置接口往WBSTAR寄存器写入目标地址并触发内部重配置。
仿真里通常会做一个简化,但简化也要保留关键握手:收到完整固件且CRC校验通过后,DUT拉出一个reboot_req信号,testbench可以检查这个信号是否在正确时机出现,并且和DONE、INIT_B的状态变化做关联断言。这样上板之后,只要物理连接没问题,重配置流程基本不会出大岔子。
3. 仿真工程搭建与HDL例程实现
3.1 工程文件怎么组织
一套干净的仿真工程,文件划分比代码本身更重要。我习惯把接口模型、DUT、仿真激励分开,避免所有东西堆在一个文件里。
tb_top.sv // 测试顶层,负责例化DUT和BFM selectmap_bfm.sv // selectMAP接口模型,封装write/read任务 update_controller.v // DUT:在线更新控制状态机 flash_model.sv // 简化Flash模型,用于验证数据落盘 fw_image.hex // 由bit文件转换来的十六进制激励数据这样划分的好处是,以后换一个项目,DUT和BFM都不用重写,只需要改testbench里的激励文件和断言。selectmap_bfm可以当成一个公共IP沉淀下来。
3.2 用BFM封装selectMAP写时序
BFM的价值在于把波形的时序细节封装成任务调用,testbench里不需要关心每一拍CS_B、RDWR_B怎么拉,只需要调用selectmap_write_byte(data)。下面是x8模式下的一个基础写时序任务:
task automatic selectmap_write_byte(input [7:0] data); begin // 拉低片选,进入写模式 @(posedge cclk); cs_b = 1'b0; rdwr_b = 1'b0; // 等CWB低,确认FPGA可接收 while (cwb === 1'b1) begin @(posedge cclk); end // 数据放上总线,在下一个上升沿被采样 d_bus = data; @(posedge cclk); // 释放片选,回到空闲态 cs_b = 1'b1; rdwr_b = 1'b1; d_bus = 8'hzz; end endtask有一点要特别注意:上面这个任务是基本的单字节写,实际传输中不一定每一拍都释放片选。连续burst传输时,CS_B和RDWR_B会一直保持有效,只在全部数据写完后释放。所以BFM里最好再提供一个burst版本,避免testbench里频繁调用上面的任务导致时序碎片化。
还需要注意高阻态。写方向的总线在空闲时要释放成高阻,否则和FPGA侧输出的读数据造成总线冲突。这也是仿真里最容易报X态的地方。
3.3 DUT更新控制状态机的设计思路
DUT这边,我用了一个适合在线更新的状态机,状态不要太多,每个状态职责明确:
localparam S_IDLE = 3'd0; localparam S_HEADER = 3'd1; localparam S_PAYLOAD = 3'd2; localparam S_CRC = 3'd3; localparam S_FLASH = 3'd4; localparam S_REBOOT = 3'd5; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; end else begin case (state) S_IDLE: begin if (frame_valid) state <= S_HEADER; end S_HEADER: begin if (header_done) state <= S_PAYLOAD; end S_PAYLOAD: begin if (payload_done) state <= S_CRC; end S_CRC: begin if (crc_ok) state <= S_FLASH; else state <= S_IDLE; end S_FLASH: begin if (flash_done) state <= S_REBOOT; end S_REBOOT: begin state <= S_IDLE; end endcase end end状态机的核心是每一拍都检查对应的握手标志,不要把多个判断条件堆在一起。S_FLASH到S_REBOOT之间,最好加一个延时计数器,确保Flash写操作的完成信号已经稳定,再触发重配置。实际项目中Flash型号不同,擦除时间差异很大,这个计数器的时间要留足余量。
3.4 用真实bit文件做激励
验证配置类逻辑,不能用一串随机数当激励,最好用真实的bit流。Xilinx的bit文件头部有同步字,后面是配置数据,依次转换成十六进制存到fw_image.hex里。testbench里用$readmemh加载:
reg [7:0] fw_mem [0:FW_SIZE-1]; initial begin $readmemh("fw_image.hex", fw_mem); for (int i = 0; i < FW_SIZE; i = i + 1) begin selectmap_write_byte(fw_mem[i]); end end有了真实bit流,还能顺便在testbench里加一个CRC校验的断言,确保DUT从selectMAP收到的数据和自己从Flash读回的数据完全一致。这个操作能覆盖掉数据通路里一大批隐蔽的位宽错位问题,尤其是x8和x16混用时。
4. 仿真常见问题与排查实战
4.1 总线高阻态与X态冲突
仿真中最常见的就是数据总线出现X态,导致DUT收到的数据全是X。原因通常是BFM在写完后把d_bus赋成了8'hzz,但DUT侧有个信号在驱动d_bus,两边同时驱动就冲突了。排查方法也很直接:在testbench里对d_bus加一个$assert检查,出现X态就立刻停止仿真并打印时间戳。
用三态总线时要注意,d_bus在DUT里要声明成wire,不能在多个always块里用reg去驱动;BFM里也尽量用一个task集中控制总线赋值,不要在testbench的多个initial块里分别操作。
4.2 CWB忙信号导致死锁
另一个高频问题是CWB一直为高,BFM里的while循环永远等不到释放,仿真卡死。这个问题的根源往往在DUT侧:CWB被某个逻辑错误地拉高后没有释放路径,或者DUT没有实现完整的CWB输出逻辑,导致它永远返回忙状态。
我自己的排查套路是:先看波形里CS_B有没有正常拉低,RDWR_B是不是写模式,再追踪CWB是由哪段逻辑拉高的。如果是简化版BFM,还需要检查从设备模型里“忙”状态持续的时间和释放条件是否和主机侧预期一致。仿真卡死不可怕,可怕的是没有任何超时保护;建议在BFM里加一个等待计数器,超过一定周期就报$fatal,这样死锁问题一眼可见。
4.3 重配置触发时机不对
在线更新最容易翻车的地方其实是“最后一步”:数据全部写完,但重配置信号没有触发,或者触发过早导致配置数据还没完全落盘。这个问题在仿真里主要靠DONE和reboot_req的关联断言来抓。DUT在触发reboot之前,必须确保Flash写操作完成、且数据已经回读校验过。
我在例程中专门加了一段覆盖组,记录S_FLASH -> S_REBOOT跳转前后的数据状态,避免状态机是“蒙混过关”跳过去的。这样就算仿真通过,也能确认是经过完整握手后才触发的重配置,而不是某个标志被意外置位。
4.4 仿真通过但上板失败的差异点
最后说一个比较玄学但真实存在的问题:仿真仿真得好好的,一上板就失败。常见差异点有这么几个:一是仿真里没有加时钟偏斜和引脚延时,上板后CCLK和数据线在物理走线上难免有偏差;二是CWB在真实器件上可能因为内部逻辑繁忙而随机拉高,仿真模型往往是比较规律的;三是上电瞬间PROG_B和INIT_B的时序,模拟器和真实器件行为有差异。
所以我会在仿真通过的版本基础上,额外做一次带SDF标注的时序仿真,或者至少在testbench里对关键信号加随机抖动,模拟真实环境。上板调试时,优先用逻辑分析仪抓CS_B、RDWR_B、CWB三根信号,不要一上来就抓全部数据线,这样定位会快很多。
我个人的习惯是,selectMAP在线更新这类逻辑,仿真环境一定要做到可复用。接口模型写成BFM,配置数据用真实bit流,断言覆盖写时序和握手流程,这本账花得特别值。等你在多个项目里用过这套仿真框架后,会发现真正需要改的地方很少,大部分工作都花在新增帧协议和Flash控制逻辑上。
最后再分享一个小技巧:如果你在调试中怀疑CWB相关时序有问题,先把仿真时钟频率降到器件的十分之一,跑一个简单burst,用波形量一下CS_B拉低到第一个CCLK上升沿之间的时间,再对比手册。很多时候问题不是逻辑写错,而是你对“忙窗口”的理解和真实器件行为存在偏差,多留点余量保平安。这套例程的完整代码我在项目里持续维护着,后续如果再碰到比较典型的在线更新问题,我会继续整理出来。
本文还有配套的精品资源,点击获取