ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IC验证实战指南:从DUT到覆盖率收敛的完整流程

IC验证实战指南:从DUT到覆盖率收敛的完整流程 芯片验证这活儿外面听着挺玄乎干过的人都知道说白了就是跟bug死磕。一颗芯片从架构定义到流片复杂一点的SoC动辄几千万门设计工程师写RTL的时候脑子里跑的是理想模型可真实电路里有竞争、有冒险、有异步跨时钟域还有一堆边界情况光靠代码review根本盯不住。这时候IC验证就得顶上在芯片还没变成硅片之前用一套仿真环境把设计翻来覆去地测把能想到的场景都想到能抓的bug都在计算机上抓出来。流一次片贵的上百万美元便宜的也要几十万美元要是bug上了硅轻则改版重来重则项目直接延期损失没法估量。所以我一直跟团队里的人讲验证不是设计完成之后的“打杂”它是整个项目里最不该省成本的一道工序。这篇东西写给刚入行或者准备转行IC验证的同学也写给那些整天跟验证团队打交道、却始终搞不明白“你们到底在忙什么”的设计和项目经理。我会按一个真实项目的推进顺序从拿到DUT开始到验证环境搭建、功能点拆解、覆盖率收敛再到回归跑法和最终交付把整套流程完整过一遍。你可以把它当成一张地图先看清全貌再往具体方向深钻。1. IC验证到底在干什么从DUT说起1.1 DUT是什么验证对象不只是“一堆代码”DUT的全称是Design Under Test被测试的设计。这个叫法很直白但很多人对它的理解太窄以为DUT就是那份RTL代码。实际上DUT的范围取决于验证的层级它可能是一个UART通信模块可能是一个DMA控制器也可能是一个完整的CPU核心或SoC子系统。你在不同阶段验证的DUT不同验证策略也跟着变。拿我做过的一个UART项目举例DUT本身只有几千行Verilog功能无非是并转串、串转并、波特率控制、FIFO管理。但就是这个看似简单的模块想要验证充分我写了上万行的SystemVerilog验证环境拆出来几十个功能点最终回归跑了上千个测试用例。很多刚入行的师弟师妹不理解觉得“这么个小破模块随便写几个testbench不就完事了吗”等他们真正开始做的时候才发现UART的波特率误差边界、FIFO满空状态切换、奇偶校验错帧恢复、中断清除时序每一个都是坑都可能在真实场景里让芯片“失灵”。还有一点容易被忽略DUT不仅是功能代码它通常还包含内部的状态定义、接口时序约束、寄存器配置等。验证工程师拿到DUT之后第一件事不是急着写环境而是把DUT的spec资料、RTL代码、接口文档全部读一遍画出模块框图和数据流。这一步做好了后面的环境搭建才有方向。1.2 验证的终点不是“没有bug”而是“风险可控”项目评审会上被人问得最多的一个问题就是“验证做完了吗”——这个问题本身就是外行问法因为它隐含了一个假设验证的尽头是“零bug”。事实是对于任何一颗有实用价值的芯片验证的输入空间都是天文数字。拿一个32位的加法器来说两个操作数的组合就有2的64次方种哪怕每种组合仿真1纳秒也要跑几百年。真实芯片的复杂度远超过这种简单运算模块所以穷举验证在工程上不存在。验证的终极目标是在资源和时间约束内把设计出bug的概率压低到可以接受的范围。换句话说验证产出的是“置信度”和“风险清单”不是“绝对正确”的保证书。这个认知特别重要因为它决定了验证工程师的工作方式不是漫无目的地测而是先拆解风险把最高风险的路径优先覆盖掉用覆盖率数据说话最后让项目管理层在一个明确的剩余风险水平上做流片决策。设计工程师和验证工程师的配合关系也基于这个逻辑。设计关注的是“功能怎么实现”验证关注的是“实现是否符合规格”。所以我一直强调验证工程师一定要带怀疑眼光看RTL别太相信设计说的话spec、代码、注释三者对不上那就是bug的温床。2. 验证环境怎么搭从空目录到第一条波形2.1 验证环境的分层设计一场戏需要的不只是演员验证环境的核心任务可以概括成三件事给DUT施加正确的激励、监测DUT的输出、判定DUT的行为对不对。为了把这三件事做干净业界在UVM方法学里把环境拆成了清晰的分层结构。用一场舞台剧来类比DUT是台上的演员他的表演由剧本测试用例驱动driver是提词员按剧本把对白一句句递给演员monitor是场边记录员把演员的每一句台词和动作都记下来reference model是导演手里的“标准剧本”他知道正确的表演应该是什么样scoreboard是裁判把monitor的记录和导演的标准剧本做对比不一致就当场判定失败。每一层各司其职好处是环境特别容易维护。DUT接口变了只改interface层测试场景变了只写新的sequence比对逻辑换了只动scoreboard。我曾经接手过一个遗留项目整个验证环境只有两个文件几千行代码全揉在一起加一个小功能等于重写一遍那种痛苦经历让我对分层设计有种近乎执念的坚持UVM为什么能成为业界主流核心就是它把这套分层标准固化了团队协作的效率完全不一样。当然UVM不是必须的。小模块、工期紧、一个人全包的时候用轻量级的SystemVerilog testbench也完全可行。但一旦DUT规模上来、多人协作、要跑回归没有UVM这套规范约束环境很快就会变成一锅粥。2.2 第一版环境只需要做对三件事很多新手拿到DUT之后的第一反应是“我要写一个完美的环境”结果写了一个月还没跑出第一条波形。我的建议永远是先把环境跑通再谈完善。第一版环境只要做到三件事就够了。第一编译。DUT的RTL文件加上验证环境文件用仿真器编译通过不报语法错误。以Synopsys VCS为例一个最基础的编译命令长这样vcs -sverilog -debug_accessall \ -timescale1ns/1ps \ -f filelist.f \ -o simv-sverilog用来打开SystemVerilog支持-debug_accessall是为了后面好拉波形、好debug-timescale1ns/1ps是时间单位和精度这个不写后面仿真时序会乱套。第二例化。在tb_top里把DUT例化起来接上interface。这一步的核心是把DUT的端口信号跟验证环境里的interface正确连接。UART这种模块有几十个端口一个信号接错后面所有测试都白跑。我干活的时候有个习惯例化完之后先跑一个空测试不做任何激励只检查DUT是否正常复位、时钟是否翻转用波形确认连接正确再往下走。第三出波形。在初始块里加上$fsdbDumpfile(test.fsdb)和$fsdbDumpvars(0, tb_top)跑一小段仿真打开Verdi看波形。这一步的意义是确认整个仿真链路是通的有一种“通电亮灯”的踏实感。环境跑通之后再逐步加入UVM的agent、env、scoreboard这些组件。先跑通再丰满这个节奏能帮你省掉大量自闭时间。2.3 文件路径与版本管理别让工程变成垃圾堆验证项目跑到后期文件数量会膨胀得很夸张。一个中等规模的IP验证rtl代码、验证代码、脚本、日志、波形、覆盖率数据库叠加起来几百个文件很正常。如果文件组织混乱别说交接连自己过两周都找不到东西在哪。我常用的目录组织方式是这样的rtl/放DUT源码只读所有修改走版本管理流程tb/testbench顶层、interface、package定义agents/按协议拆分的agent组件env/环境级组件如reference model、scoreboard、coverage collectortests/所有测试用例每个用例一个文件sim/仿真运行目录脚本、日志、波形、覆盖率数据库都放这里这个目录一般不进版本库regression/回归脚本和结果汇总页面目录一旦定下来尽量别动。项目里最怕那种“热心同事”今天加一层目录明天改一次命名规范版本管理追起来欲哭无泪。Git或者Perforce在IC验证里都是标配代码提交信息一定要写清楚“改了什么、为什么改”否则半年后你看着一个commit消息写着“fix bug”死活想不起来修的是哪个bug。3. 测试用例和覆盖率验证的质量怎么算3.1 用例设计首先要拆功能点测试用例不是凭感觉写的它的源头是功能点分解。规范的做法是从规格文档里逐条提取可验证的功能点再针对每个功能点设计一个或一组测试用例来覆盖。以我前面说的UART模块为例功能点大致可以拆成这样功能点验证重点对应测试用例数据发送数据是否按顺序逐bit输出起始位、停止位是否正常uart_send_normal数据接收采样点是否正确接收后RX_FIFO数据是否完整uart_receive_normal波特率边界时钟分频在不同配置下是否准确接收端允许的误差范围uart_baud_oversample、uart_baud_error_boundaryFIFO满空满信号和空信号是否及时拉高/拉低写入溢出时是否丢弃uart_fifo_full、uart_fifo_overflow奇偶校验使能偶校验、奇校验、无校验错帧能否被检测到uart_parity_even、uart_parity_odd、uart_parity_error中断与状态发送完成、接收有效等中断触发条件是否正确uart_interrupt_tx_done、uart_interrupt_rx_valid拆功能点的时候有个经验别只看spec里的“正常情况”一定要列出边界、异常、错误注入这三类场景。正常路径只能证明设计“能用”边界和异常才是bug藏身最多的地方。UART接收端波特率误差超过3%会怎样FIFO写满之后继续写数据会怎样这些场景设计文档里常常一笔带过但芯片在实际系统里真的会遇到。3.2 代码覆盖率与功能覆盖率两张成绩单都不能缺覆盖率是验证“量”的度量业内主要分两类代码覆盖率和功能覆盖率。代码覆盖率由仿真工具自动统计包括行覆盖、条件覆盖、分支覆盖、状态机覆盖、翻转覆盖等。它回答的问题是“RTL代码有没有被执行到”。我见过不少团队盯着行覆盖率看90%以上就觉得验证够了这是个大坑。代码覆盖率90%只说明代码“跑到了”不能说明“测对了”。比如一条if语句两个分支都执行过分支覆盖率达到了100%但如果判断条件本身就有逻辑错误代码覆盖率完全反映不出来。功能覆盖率是验证工程师手动写的covergroup和coverpoint它回答的问题是“规格里的功能场景有没有被验证到”。比如UART验证中我可以定义一个coverpoint来监控“波特率分频值”把它分成低速、中速、高速几个bin仿真过程中实时统计哪些区间的分频值被配置过。功能覆盖率设计得越贴近规格这个指标的可信度越高。代码覆盖率和功能覆盖率是一对互补指标。打个比方代码覆盖率告诉你把一座城市的哪些街道路过了功能覆盖率告诉你这些街上你真正进过哪些店铺。光是道路全走一遍不代表该买的都买了。所以我的验收标准通常是两个一起打代码覆盖率尽量冲高功能覆盖率必须100%覆盖设计好的功能点。3.3 约束随机与seed管理让随机“可控”是一门手艺现代验证几乎都采用约束随机验证CRV核心思想是用随机化生成大量激励再用约束把激励限制在合法空间内。没有约束的纯随机没有意义因为你可能花半天时间生成了几万个激励结果全是非法操作DUT的正常功能根本测不到。举个具体例子配置UART波特率分频值的寄存器是16位合法范围是1到65535。如果做纯随机大概率会生成很多中间值可是真正容易出bug的分频值边界是1、2、65534、65535这几个数。这时候就要在sequence里加约束用SystemVerilog的rand配合constraint把生成值引导到边界附近同时保证中间值也有一定比例既有重点又不失随机性。随机化的引入带来一个工程问题同一个测试用例在不同seed下结果可能不同今天绿明天红遇到失败用例想复现又复现不出来。行业里的标准做法是每次仿真打印固定seed并且用ntb_random_seedxxx这样的参数把seed固化下来。回归失败时拿着日志里的seed重跑就能复现问题。我在项目里专门写了脚本自动解析回归日志、提取每个用例的seed一旦有挂掉的用例一条命令就能用原seed重新拉起仿真debug效率翻倍。4. 回归与交付验证结束靠的不是感觉4.1 回归怎么跑频率、范围、失败处理功能用例全部写好之后验证工作进入“回归”阶段。回归的意义在于RTL代码每周甚至每天都在变每次改动都可能引入新的bug或者破坏已有功能必须用一整套用例集反复回归确保新代码没有让老功能倒退。回归策略一般分两层。第一层是持续集成回归代码一提交就触发只跑冒烟测试和关键路径用例控制在半个小时到一个小时内跑完目的是快速发现致命问题。第二层是每日全量回归把整个用例集全部跑一遍几百上千个用例可能需要跑几个小时甚至十几个小时通常在每天晚上自动启动第二天早上看结果。回归跑挂之后的“triage”环节是验证工程师日常工作量的大头。拿到一份回归报告先别急着改代码先分类挂掉的用例是环境问题、DUT bug、用例本身的问题还是比对逻辑错了我的经验是按顺序排查第一看日志有没有仿真超时、有没有断言直接失败第二看是不是环境初始化问题比如某个公共资源在多用例并发时被互相干扰了第三才怀疑DUT功能问题拉波形、比对期望值和实际值的差异点。日常回归里环境不稳定造成的假失败占比相当高如果每次都当DUT bug去查人会被拖垮。4.2 收敛标准不仅能跑绿还要能背书回归全绿只是最低要求真正意义上的“验证收敛”还要满足一套更严格的标准。我项目里常用的收尾检查清单大概长这样检查项标准说明功能覆盖率100%命中所有已定义的coverpoint如果有个别做不到100%必须有书面理由和审批代码覆盖率行覆盖率、分支覆盖率、FSM覆盖率达标不同项目要求不同一般行覆盖95%以上分支覆盖85%以上回归结果连续N天全绿无新增fail我通常要求至少连续5天稳定防止偶发性失败没暴露失败用例所有fail项均已triage完毕不允许有“挂名未查”的case断言检查所有SVA和UVM内建断言无违反断言是抓时序问题最敏感的手段已知问题所有已知bug已登记、分类、明确修复或豁免waiver要有审批记录不能口头豁免文档验证计划、覆盖率报告、回归报告整理归档交付物齐全别人能接手这里我想提醒一点覆盖率数字是可以“做”出来的。比如功能覆盖率达不到100%最简单的作弊办法是把对应的bin删掉。这个做法在流程上很坑它让你的验证报告失去了公信力。正确做法是诚实标注“该场景未覆盖”给出原因让项目经理和架构师决定是否接受风险。验证工程师的底线就是数据的真实性数据一旦造假整个验证团队的存在价值都没有了。4.3 交付物清单要让别人能接手验证收尾阶段交付的不只是一份“验证完成报告”而是一整套可以让后续团队无缝接手的资产。我按重要程度排个序第一是验证环境代码必须是干净的、可复现的。别人拿到你的环境按README一跑就能出结果不需要你本人做现场讲解。第二是验证计划这里面记录了你拆了哪些功能点、每个功能点用了哪些用例去覆盖这是整个验证工作逻辑的源头。第三是覆盖率报告包括功能覆盖率和代码覆盖率除了数据还要有覆盖率未达标项的说明。第四是回归报告至少要能看到最近几轮回归的趋势是越来越稳还是时好时坏。第五是已知缺陷和waiver清单哪些问题发现了但不影响交付在什么条件下可以接受必须白纸黑字写清楚。最后还有一样东西容易被忽略记录一个“验证遗留风险列表”。比如“UART接收端波特率偏差12%时偶发采样错误经评估系统应用场景不会触发暂不修复”。这类风险如果不写下来芯片出了问题再回来翻排查成本极高。交付物做得完整是一个验证工程师职业素养最直接的体现。5. 实操中逃不开的坑我的踩坑记录5.1 回归时绿时红随机种子和环境不干净的坑做验证头两年我被“偶发失败”折磨得不轻。有一个项目某测试用例的失败频率大概十分之一日志里头显示的失败位置每次都不一样RTL代码看起来又没问题。整整折腾了一周最后定位到原因两个测试用例在仿真环境里共享同一个全局变量其中一个用例修改了它但没有恢复导致另一个用例的行为被污染。这类树典型的环境不干净问题用单线程单用例跑没事一上回归并发就跑挂。从那以后我定了一个规矩每个测试用例必须在用例初始化阶段显式设置所有配置不允许依赖前一个用例残留的状态环境代码里所有全局变量必须清零。另外所有用例在写进回归集之前先用两三个不同的seed各跑一遍能大大降低“偶发失败”的概率。5.2 仿真跑不动性能与覆盖率的工程权衡验证环境的性能问题项目中期会集中爆发。我见过一个DMA控制器的验证环境跑一个中等复杂度的测试用例要几个小时全量回归根本跑不完覆盖率也提升不上去。我的做法是分三步优化。第一步砍掉不必要的波形dump全量dump波形会拖慢仿真好几倍日常回归只保留pass/fail信息失败用例卡住时再重跑并打开波形。第二步检查环境里有没有性能瓶颈常见的问题是reference model写得不够高效一个循环能解决的问题非要用两层循环还有的是scoreboard里用了过多的字符串操作这在仿真是出了名的慢。第三步合理开并行把回归任务拆到集群的多台机器上并行跑同时限制并发数量防止互相争抢IO导致整体变慢。还有个技巧很多人不知道如果DUT里有一段计算密集的乘法器网络而且它不是当前验证重点可以在不影响功能正确性的前提下把仿真精度从timescale1ns/1ps放松到1ns/10ps性能能提升不少。性能优化的本质是成本权衡牺牲一点点精确度换回更多的回归吞吐量是划算的买卖。5.3 DUT一改版验证就崩版本兼容与可复用性芯片项目里RTL改版的频率远超大多数人的预期。设计工程师今天加了一个功能寄存器明天换了一个接口信号名后天又调整了时序要求。验证环境如果不跟上节奏就会出现“DUT一改环境崩一片”的窘境。我吃过亏之后建立了一套应对机制。第一拿到新版RTL时先跑环境自检例程单独检查reset、时钟、基本读写等基础通路是否正常确保是大环境问题还是小改动影响。第二interface层要花心思设计DUT端口变化尽量只改interface一个文件不要让driver和monitor跟着一起动。第三关键行为断言一定要写在环境里而不是写在测试用例里环境层面的断言能守住DUT改版导致的隐性行为变化。最实用的建议就一条别写“一次性环境”。看到环境里有人为了凑一个用例快速通过而写死一堆信号值我都会让他停下来重写。环境是会随项目长大的你今天图省事埋的技术债三个月后要用十倍的加班来还。最后分享一点我个人的体会。我做了这么多年验证最大的心得是验证工作的产出不是那一堆绿色通过的回归报告而是“风险已经充分暴露并可评估”的信心。真正优秀的验证工程师不是在项目收尾时把一切做得完美无瑕而是在项目每个阶段都能准确说出“现在还剩什么风险、为什么接受这个风险”。那些能一版成功的芯片项目背后往往不是一个超级天才而是一套有纪律、可量化、经得起追问的验证流程。刚入行的朋友不用急先从看懂DUT、跑通一个用例开始一步步沿着“搭建环境、拆功能点、覆盖率驱动、回归收敛”这条路走下去慢慢就会建立起属于自己的工程直觉。
返回列表