
做FPGA开发的人十有八九都有过这种经历仿真里跑得好好的设计一上板子就翻车。翻车多了你就会明白软件仿真验证的终归只是“模型的模型”而真实芯片的行为远比你建的模型复杂得多。硬件在环验证HIL要解决的就是这个“真实”的问题——把真实芯片放进测试环路里直接测它本身而不是测你写的那个仿真模型。这篇文章我不会跟你讲太多晦涩的理论就结合我自己做过的项目把FPGA硬件在环验证这套东西从头到尾拆开聊一遍。内容涵盖为什么仿真已经够多了还要上HIL、HIL的几种玩法、一套能复用的验证环境怎么搭、以及我在实际测试中踩过的那些坑。不管你是刚接触FPGA的在校生还是已经在做芯片验证的工程师这篇文章应该都能给你一些参考。1. 为什么必须“测真实芯片”纯仿真解决不了的三个问题不管你是做FPGA原型验证还是用FPGA去测一颗外部的真实芯片比如ADC、DAC、SDRAM、Flash、PHY芯片只要你还停留在“写testbench 跑仿真”的阶段就一定会撞上三个绕不开的墙。1.1 仿真模型与真实芯片的差异远比你想象的大先问一个问题你的仿真模型是谁写的如果是你自己根据芯片手册写的那恭喜你这个模型的准确度基本取决于你的理解程度和理解漏洞。手册上不会写的那些东西——上电瞬间的行为、温漂、制造工艺带来的个体差异、引脚之间的串扰——你的模型里一概没有。我曾经踩过一个特别典型的坑。项目里要用一颗SPI接口的ADC我根据手册写了个行为模型仿真里数据读出完全正确时序余量也看着很舒服。结果板子回来一测ADC读出来全是乱码。排查了半天发现是芯片的DOUT引脚在上电后默认是高阻态要等它内部上电校准完成之后才会输出有效数据。手册附录里有一行小字写了这件事但我建模型的时候根本没注意到。仿真模型里我把DOUT默认拉低了所以从仿真结果上看一切正常。这个例子想说明什么仿真模型再精细它也只是你对芯片行为的“主观理解”的具象化。你理解错了仿真结果就会跟着错。而硬件在环验证不同它把真实芯片直接接入环路你测的是什么芯片就是什么不存在理解和建模的偏差。这就是HIL最核心的价值所在。1.2 速度差距带来的验证深度完全不同第二个问题很现实仿真太慢了。假设你有一个FPGA内部的图像处理模块处理一帧1024x768的图像需要一个完整的流水线周期在仿真里跑完这一帧可能需要几分钟甚至更久。这种速度下你根本不可能做长时间的压力测试、随机注入测试、或者跑完一个复杂的DMA传输场景。真实芯片一秒钟能跑多少个周期以100MHz为例一秒就是一亿个时钟周期。仿真一秒钟大概只能跑到几百到几千个周期。三个数量级以上的差距意味着什么意味着仿真只能覆盖到“典型路径”而HIL可以快速跑完大量的“边界路径”和“随机路径”。我自己的经验是同样的一个DDR读写控制器纯仿真跑到95%的代码覆盖率要整整一周而放到HIL平台上跑大半天就能跑到97%以上而且很多仿真里根本覆盖不到的时序竞争问题在HIL上跑几分钟就直接暴露出来了。1.3 电气层面的事情仿真根本插不了手仿真软件里没有电流、没有电压、没有信号完整性、没有地弹。但如果你的设计要跟真实的外部芯片连接这些电气特性就是决定项目成败的生死线。我曾经调过一块FPGA直连一个SDRAM芯片的板子仿真里读写全对因为仿真模型里SDRAM的时序参数是理想化的。上板之后发现当SDRAM工作在100MHz以上时读数据偶尔会出现单bit错误。用示波器一抓发现是FPGA IO输出的信号沿太陡在SDRAM的数据线上引起了振铃超过了输入高电平的阈值范围。这种问题在仿真里是永远不可能发现的因为它根植于PCB的走线阻抗和芯片引脚的寄生参数。HIL天然就是在这种真实电气环境下运行IO翻转的实际波形、PCB走线上的延迟、甚至电源纹波对芯片的影响全都会被如实地反映在测试结果里。这也是为什么在一些对时序和电气要求苛刻的领域比如汽车电子、航空航天、通信基站的芯片验证中HIL几乎是一个标配环节。2. 硬件在环验证的整体设计思路弄明白你到底在测谁聊完“为什么”我们再聊聊“是什么”。我发现很多新人分不清“仿真、原型验证、硬件在环验证”这三者的边界容易一锅粥。这里我给你一个相对清楚的分层方式。2.1 三种验证形态的边界与各自的适用场景仿真Simulation是你和你的模型在玩DUT是模型周围的环境也是模型。好处是可控性强、可观测性好、不用等板子坏处是“模型永远不等于真实”。原型验证Prototyping通常是指把FPGA当作一颗待流片的ASIC来用你验证的是FPGA本身能不能跑通你的逻辑侧重点是“逻辑功能正确性”。硬件在环验证HIL则更进一步被测对象是真实的芯片或者真实的接口FPGA在这里扮演的是“测试平台”的角色负责给真实的DUT注入激励、读取响应、并做出判断。所以HIL严格来说有两种取向一种是“FPGA作为被测对象”把FPGA放到一个真实的外部环境中去验证另一种是“FPGA作为测试仪器”用FPGA去驱动和检查一颗外部的真实芯片。这篇文章主要讨论后者因为“测真实芯片而不是仿它的模型”这个标题指向的就是这个方向。2.2 一个标准的HIL环境由哪几块组成一套完整的FPGA硬件在环验证环境无论被测的是什么芯片抽象出来无非就是三块激励源、被测设备DUT、响应采集与判决。激励源负责产生DUT需要的所有输入信号。对于SPI/I2C/UART这类低速接口直接用FPGA内部的状态机就能生成对于PCIe、DDR、LVDS这类高速接口就需要用到FPGA内部的硬核或者高速收发器SerDes。被测设备就是那颗真实芯片它通过PCB走线和FPGA连接。响应采集负责把DUT的输出信号读回来在FPGA内部做比对和分析。比对可以有两种方式一种是把回读数据通过串口/网口发回PC用上位机脚本比对另一种是FPGA内部就直接做实时比对一旦发现错误立刻拉高一个错误标志、停止测试或者打印日志。用一句话总结HIL本质上是用FPGA搭了一个“真实环境模拟器”让DUT以为自己工作在最终系统里而实际上所有交互都被FPGA监测和记录。2.3 什么时候必须上HIL什么时候没必要HIL不是灵丹妙药它也有成本——板卡、硬件调试时间、环境搭建难度都不低。所以你需要对它有一个合理的预期判断。我个人的经验是满足以下任一条件就可以考虑上HIL一是DUT是真实的外部芯片且仿真模型不完善或者根本不存在比如一颗新出的ADC或者一颗行为逻辑保密的芯片二是有大量数据吞吐或者长时稳定性验证需求纯仿真跑不完三是接口协议涉及复杂的时序协商过程比如DDR Training、PCIe Link Training这类仿真模型很难完整模拟。相反如果只是验证一段纯粹的内部逻辑功能跟外部芯片交互很少那老老实实跑仿真就行没必要杀鸡用牛刀。3. 从零搭建一套FPGA硬件在环验证环境一个具体可落地的流程理论聊够了接下来这部分是文章的重头戏。我以一个非常典型的场景为例用FPGA去验证一颗真实的SPI接口ADC芯片把它的数据正确读回来、存储、并和预设的模拟输入信号做对比。这套流程能跑通你稍加改动就能复用到其他接口的芯片上。3.1 硬件平台的选型与准备首先你需要一块FPGA开发板以及一块带有DUT芯片的接口板或者直接把DUT焊在FPGA同板载的扩展口上。FPGA选型上我这里给一个参考如果被测接口是SPI/I2C/UART这类低速接口市面上带Artix-7或者Cyclone 10 LP级别的FPGA开发板就完全够用了性价比很高上手也快。如果被测接口是DDR3/DDR4、MIPI、PCIe、千兆以太网这种高速接口就需要选带相应硬核或高速收发器的芯片比如Kintex-7、Zynq UltraScale或者Intel的Cyclone 10 GX / Arria 10系列。我这次用的是Xilinx Artix-7开发板加一块自制的ADC子卡ADC型号是AD760616位、8通道、并行/串行接口都支持的同步采样ADC。接口板上有模拟信号输入端我用一个信号发生器给它输入一个1kHz的正弦波然后用FPGA把ADC采样到的数据读回来在PC端用Python做FFT分析看看采样出来的波形和原始信号有多吻合。3.2 时钟和复位的处理HIL环境里最容易忽视的基石在搭HIL环境时时钟和复位的设计比纯仿真要更讲究。纯仿真里你经常随手写一个initial块或者直接加一个时钟源就完事了但在HIL环境里时钟的来源、相位关系、抖动特性都会直接影响真实芯片的行为。以AD7606这类带并行和串行接口的ADC为例你需要给它提供一个主时钟通常是MCLK同时SPI接口的SCLK又由FPGA侧产生。这时候FPGA内部就要有一个统一的时钟管理模块把所有时钟的相位关系对齐避免出现采样时刻处于信号不稳定区间的情况。我一般会在FPGA里专门分出一个时钟模块用MMCM/PLL产生各路的时钟并加上一个复位同步模块。有一点要特别提醒真实芯片的复位信号通常要求足够长因为芯片内部可能有上电自检、寄存器初始化等过程。如果你复位时间太短芯片初始化不完全后面数据就是错的。AD7606的数据手册上有写复位信号需要至少25ns的低电平时间但这只是理论最小值实际项目中我习惯于给到1us以上免得因为PCB走线延迟导致复位时序不够。3.3 激励注入与数据回读SPI时序的FPGA实现这一节放一段能跑通的核心代码。下面是我用Verilog写的一个AD7606 SPI串行读取控制器在HIL验证环境里用来产生SPI时序并完成数据回读。这段代码经过实际板级调试可以直接参考。// AD7606 SPI read controller with 16-bit data capture // Supports 1MHz SCLK, CPOL0, CPHA0 module ad7606_spi_reader #( parameter CLK_FREQ_HZ 100_000_000 )( input wire clk, // FPGA system clock 100MHz input wire rst_n, // active-low reset input wire convst_done, // signal from state machine that conversion is done output reg sclk, // SPI clock to ADC output reg cs_n, // chip select, active low input wire sdo, // MISO from ADC output reg sample_valid, // pulse high when a 16-bit sample is ready output reg [15:0] sample_data // captured data ); localparam IDLE 3d0; localparam ASSERT_CS 3d1; localparam BIT_LO 3d2; localparam BIT_HI 3d3; localparam DONE 3d4; reg [2:0] state; reg [3:0] bit_cnt; reg [15:0] shift_reg; reg [6:0] sclk_div; reg sclk_rise; // pulse for each rising edge of sclk always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk_div 7d0; sclk_rise 1b0; end else begin sclk_div sclk_div 1b1; sclk_rise 1b0; if (sclk_div 7d49) begin // 100MHz / 50 2MHz base, then toggle makes 1MHz SCLK sclk_div 7d0; sclk_rise 1b1; end end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cs_n 1b1; sclk 1b0; bit_cnt 4d0; shift_reg 16d0; sample_data 16d0; sample_valid 1b0; end else begin sample_valid 1b0; case (state) IDLE: begin cs_n 1b1; sclk 1b0; if (convst_done) state ASSERT_CS; end ASSERT_CS: begin cs_n 1b0; // pull CS low, but SCLK stays low bit_cnt 4d0; state BIT_LO; end BIT_LO: begin if (sclk_rise) begin sclk 1b1; // rising edge shift_reg {shift_reg[14:0], sdo}; // sample data at rising edge bit_cnt bit_cnt 1b1; state BIT_HI; end end BIT_HI: begin if (sclk_rise) begin sclk 1b0; // falling edge if (bit_cnt 4d16) begin cs_n 1b1; sample_data shift_reg; sample_valid 1b1; state IDLE; end else begin state BIT_LO; end end end default: state IDLE; endcase end end endmodule这段代码的要点在于SCLK的极性是CPOL0即空闲时为低电平数据在SCLK上升沿被FPGA采样所以我们要保证在上升沿到来的那一刻SDO线上的数据已经稳定。AD7606的SDO在SCLK下降沿更新数据所以FPGA这边在上升沿采样是符合时序要求的。另外我把SCLK分频到1MHz对于AD7606这种最高能跑到几十兆赫兹的器件来说余量足够。编完代码后不要急着综合上板先在仿真里把时序波形过一遍确认SPI读写状态机的状态跳变有没有竞争。做HIL和做纯验证的不同之处在于你仿真的对象是“FPGA内部逻辑对真实芯片接口的驱动行为”行为模型可以不精确但接口时序的宏观帧格式必须正确。3.4 数据比对与自动化用Python把回读数据变成测试报告FPGA把ADC采样数据读回来之后怎么判断它对不对两种思路一是把数据通过串口或者USB发到PC上在PC端做离线分析。这是最简单的思路适合验证初期。我一般是把UART波特率设为921600这样16位数据加一个校验头每秒大概能传两万多个采样点。虽然带宽一般但用来做功能验证足够了。二是FPGA内部直接做比对。比如你给ADC输入一个已知电压FPGA内部存一个期望值范围采样结果一旦超出范围就拉高error信号、记录错误数量并把错误样本存入内部RAM测试结束之后通过调试接口一次性读出来。这种方式更接近生产级的芯片测试适合批量验证。我这次选择的是第一种。Python脚本的核心逻辑很简单从串口读取一帧数据校验帧头然后拼接成16位数据保存到列表里。等抓完足够多的样本之后做FFT看频谱上有没有异常的谐波分量。这样的好处是可以顺带评估ADC的动态性能而不只是验证通信是否正确。import serial import numpy as np import matplotlib.pyplot as plt PORT COM7 BAUD 921600 ser serial.Serial(PORT, BAUD, timeout1) samples [] expected_header 0xAA55 while len(samples) 4096: data ser.read(2) if len(data) 2: continue val (data[0] 8) | data[1] if val expected_header: # read actual sample word raw ser.read(2) if len(raw) 2: sample ((raw[0] 8) | raw[1]) 0xFFFF samples.append(sample) else: continue samples np.array(samples) - 32768.0 # convert to signed range N len(samples) fft_vals np.fft.rfft(samples * np.hanning(N)) fft_amp np.abs(fft_vals) / N freq np.linspace(0, 0.5, len(fft_amp)) # normalized frequency plt.figure(figsize(10, 5)) plt.plot(freq, 20 * np.log10(fft_amp 1e-12)) plt.xlabel(Normalized Frequency) plt.ylabel(Amplitude (dB)) plt.title(ADC Sampled Signal FFT) plt.grid(True) plt.savefig(adc_fft.png)这脚本看起来简单但里面埋了一个我自己踩过的坑串口读数据必须考虑帧同步问题。因为串口是字节流你不知道当前读到的第一个字节到底是帧头还是数据的一部分。解决方案就是用两个字节的固定帧头0xAA55做对齐读到了0xAA再读下一个字节如果确实是0x55就认为这是一帧的起始然后把后面的两个字节当作真正的ADC数据。如果帧头不匹配就把第二个字节当作可能的帧头重新开始对齐。这个处理如果你漏掉了后面所有的数据错位分析都会让你怀疑人生。3.5 自动化测试用例设计覆盖“正常、边界、异常”三类场景HIL的价值很大程度体现在你可以很轻松地把测试用例跑上一千遍一万遍。所以在搭环境时我会在FPGA里做一个简单的状态机让它按照预定的序列自动完成“发启动转换命令 - 等待转换完成 - 读取数据”这一整套流程然后循环执行。同时在PC端做一个更上层的用例管理把所有用例分成三类正常用例给ADC输入已知频率和幅值的正弦波验证采样数据的幅度、频率、失真度是否在规格范围内。边界用例把输入信号调到大信号的满量程附近或者小信号贴近最低有效位LSB附近看ADC是否还能正常采样无丢码同时把SPI时钟往上调找到时序的极限工作频率。异常用例比如在转换过程中频繁地复位ADC芯片看FPGA状态机能否自动恢复或者把SPI的CS信号长时间拉高看总线悬空时是否有漏电流导致的误触发。这三类用例在仿真里都很难做到“真实”因为边界和异常往往涉及芯片内部的模拟特性。在HIL平台上这些就是几行脚本的事。4. 实操中的常见问题与排查技巧实录HIL环境跑起来不难难的是出问题时你能快速定位。这一章我把自己这些年折腾HIL平台时遇到过的高频问题整理成一个速查表外加几个我认为最有价值的排查思路。4.1 信号不同步与毛刺如何用“三段式”快速定位如果你的HIL平台出现数据偶发错误但又不是持续错误排查起来特别难受。我通常会用三步走第一步先用示波器同时抓FPGA输出的SCLK、CS和DUT输出的数据线。看SCLK的上升沿到底是落在数据稳定的区间内还是落在了数据翻转的边沿上。100MHz的逻辑分析仪可能不够用最少要上200MHz以上的示波器最好是500MHz级别的。大部分此类问题这一步就能看出端倪。第二步检查是不是FPGA IO的驱动强度设置不对。在Xilinx Vivado里IO Standard和Drive Strength是可以配置的。如果驱动强度过大会导致信号过冲如果过小在长走线上信号上升时间太长会让采样点的时序余量变得不足。我一般会把FPGA侧输出引脚的Slew Rate设为SLOW虽然会牺牲一点翻转速度但信号完整性会好很多。第三步如果前两步都没问题那就要怀疑是不是DUT本身对电源噪声敏感。给DUT单独供电排查一下模拟电源和数字电源之间的隔离确认没有通过地平面耦合进噪声。这个在HIL验证里经常被忽视但实际上影响非常大。4.2 常见问题速查表现象可能原因排查方向数据全是0或者全FF片选或时钟极性反了用示波器对比时序图和手册数据偶尔错一两个bit采样沿选错或时序余量不足把采样沿从上升沿换到下降沿或降低接口时钟通信完全不通复位时间不够或者DUT未初始化拉长复位时间确认上电时序采样值正确但幅度偏小模拟输入驱动能力不足检查信号源的输出阻抗加缓冲器长时间运行后崩溃状态机没有异常恢复机制在状态机里加超时跳转增加看门狗逻辑上板后和仿真结果不一致仿真模型遗漏了DUT的电气行为用逻辑分析仪抓真实时序回头修正模型4.3 一个我在实际项目中踩过的复现弯路最后讲一个比较特殊的案例。有一块HIL板卡测试一颗I2C接口的传感器芯片。FPGA作为主控制器发读请求传感器返回数据。仿真里一切正常用逻辑分析仪抓总线波形也完全正确但就是读回来的数值会漂越漂越离谱。排查了整整两天最后发现原因在I2C的上拉电阻上。I2C总线需要外部上拉电阻我们用的两颗4.7kΩ上拉到3.3V。逻辑分析仪接上去之后等效输入电容变大导致总线上升时间变慢传感器芯片在读取时处于亚稳态返回了错误的数据。把逻辑分析仪去掉、加大上拉电阻驱动能力之后问题就消失了。这个案例给我们的教训是什么做HIL验证时你用的每一根探针、每一个测试工装本身都会改变被测电路的行为。所以当出现完全不符合逻辑的怪问题时先把所有外接测量设备断开看看问题是否依旧存在。如果断开后问题消失那大概率就是测试工装影响了信号质量。5. 硬件在环验证能走多远从单芯片到系统级的前景写到这里我想把视角再拉高一点。硬件在环验证并不是芯片验证的专利它在汽车电子、工业控制、新能源等领域用得非常多。比如汽车上常见的域控制器工程师在实验室就要搭一套“虚拟车辆 真实控制器”的HIL环境把真实ECU接上去用上位机模拟发动机、轮速、刹车等各种传感器信号来看ECU的算法能不能正确响应。这种思路跟用FPGA测一颗ADC芯片本质上是完全一样的——被测对象真实存在环绕它的是可以精确控制的激励环境。所以如果你现在把“用FPGA测真实芯片”这套流程玩熟了后面往系统级验证方向转型会非常顺畅。你可以从单颗芯片的SPI/I2C接口测试延展到整个板卡的多芯片协同测试再往大一点可以把通信总线、电源管理、传感器融合模块全都并进来在实验室里复现一个接近真实运行的整机环境。我个人在这条路上的下一步计划是把之前做的那套单ADC测试环境扩展成一个多通道同步采集验证平台用来验证一颗8通道ADC芯片在不同采样率、不同输入信号下的串扰和同步特性。如果你的项目也需要类似的验证建议从一开始就把数据回读、用例管理、报告生成的框架搭好别到后面再回头补。6. 写在最后的几条实操心得文章到这里该展开的技术细节都聊得差不多了。最后分享几个我从多次HIL项目里总结出来的个人经验算是给新入坑的同学一点叮嘱。第一做HIL时永远要把时序图先看懂再动手写代码。芯片手册里的时序参数表才是最高优先级FPGA里再漂亮的代码也拗不过真实的建立保持时间要求。第二不要一上来就追求全自动化。先把“手动读取一帧数据”这个最小流程跑通确认接口通信无误再逐步加入错误注入、自动比对、批量回归这些高级功能。一次性把框架搭太大出问题的时候你会完全不知道从哪排除。第三也是最重要的一点对外部工具不要迷信。逻辑分析仪、示波器、仿真模型都不是神圣不可侵犯的它们本身也会干扰和扭曲被测对象的行为。遇到不可思议的Bug时反过来思考是不是工具本身在说谎。硬件在环验证这条路越往里走越有意思。它不像纯仿真那样在一个可控的沙盘里推演而是要你真刀真枪地面对物理世界的千奇百怪。但恰恰是这些不确定性才让每一次“真实芯片跑通了”的时刻格外有成就感。希望这篇文章能帮你少踩几个坑早日跑通自己的第一个硬件在环验证平台。