YAOTU INSIGHTS

从Verilog到SystemVerilog:芯片验证工程师的进阶之路

从Verilog到SystemVerilog:芯片验证工程师的进阶之路
我在面试芯片验证岗位的那段时间把招聘网站翻了无数遍发现一个扎心的现象凡是和数字IC验证沾边的职位要求栏里几乎清一色写着熟悉SystemVerilog了解UVM。那时候我还是个写了两年Verilog的FPGA工程师看到这个要求的第一反应是Verilog不也能写测试平台吗为什么非要学SystemVerilog等到真正转过去、把一个模块的验证环境从零搭起来之后我才明白岗位要求背后的原因。这篇博文就是我的个人笔记方向是SystemVerilog这个主题围绕chips相关的验证工作记录我从Verilog背景转到SV验证过程中那些花了很多时间才想明白、串起来的知识和方法。文章不打算写成标准手册式的语法罗列而是按我实际学习和使用的顺序把最关键的几个知识点、最容易踩的坑、以及我自己验证过好用的路径整理成文。如果你也正在学SystemVerilog或者正准备从设计岗转验证岗这篇笔记应该能帮你省掉不少弯路。1. 为什么芯片验证岗位都在问SystemVerilog1.1 验证在芯片流程中的比重进芯片行业之前我一直以为设计是项目里最核心的环节验证只是写几个测试用例跑跑仿真。真正参与项目之后才发现这个认知错得离谱。一个中等规模的SoC项目设计和验证的人员配比通常是1:2甚至1:3也就是说验证工程师的数量远多于设计工程师。为什么会这样因为每一次流片都要花钱而流片回来发现功能bug定位问题、改版、重新验证、再流片整个周期成本和资金损失都是指数级上升的。所以业界普遍的做法是在流片前把验证做到极致用尽量多的仿真、断言、覆盖率去证明设计行为符合预期。在这样的大背景下验证本身不再只是搭个testbench喂几个激励看看波形对不对而是一个需要高抽象、高复用、可维护的工程。模块级验证、子系统验证、芯片级验证每一层都需要大量测试用例和回归回归分析。如果你只是用Verilog来干这件事很快就会撞上墙Verilog本身是面向硬件建模的语言它的测试平台是用initial块、task、forever循环一点点拼出来的模块性和抽象层次都太弱。一个小模块还能应付一旦模块之间交互多起来代码量爆炸、用例难以维护、随机化能力基本为零整个验证环境就变成一坨牵一发动全身的意大利面。1.2 从Verilog到SystemVerilog一次必要的能力升级SystemVerilog简称SV之所以能成为验证主流根源在于它是Verilog的超集没有抛弃原本的硬件描述能力而是在验证方向上做了极其丰富的扩展。用一句话概括SV保留了Verilog的设计能力同时增加了面向对象、随机约束、断言、覆盖率、接口抽象这些专门为验证服务的语言特性。它不是一个新语言而是能写设计的验证语言。这里有个常被混淆的点很多人以为SV只是验证语言其实SV的设计能力同样完整甚至比Verilog更好用。2009年以后IEEE 1800标准把SystemVerilog和Verilog合并成了一个标准也就是说现在的SV标准本身就包含设计建模的语法硬件描述和验证描述共用同一套语言。只是在实际工业界设计岗位依然有很大比例在用Verilog或混合写法而验证岗位则全面转向SV。这种分工导致了一个有趣的现象SV的设计能力反而经常被忽略验证能力被无限强调。对我来说学SV最强烈的体感是它把软件工程的思维带进了硬件验证。面向对象、类的继承、多态、随机化这些概念以前只在我写C的时候出现过现在居然在硬件描述语言的测试平台里全部能用上。这个变化的意义非常重大——验证环境本身是一个复杂的软件系统只不过它运行在仿真器里、和硬件设计打交道。用软件工程的方法来构建和复用验证环境这正是SystemVerilog最大的价值。2. 从Verilog到SystemVerilog先改变写代码的思维习惯2.1 先忘掉reg和wirelogic与二值逻辑类型从Verilog转过来的工程师最先遇到的冲击就是这个SV一个logic可以代替wire和reg。在Verilog里wire是线网类型reg是变量类型新手经常搞不清楚什么时候该用哪个一旦把reg连接到输出端口还会报错。SV里的logic是四值类型既能被连续赋值驱动也能在过程块里赋值对大多数信号都不再需要区分线网还是变量。但这里有个工程上的陷阱logic只能有一个驱动源。如果你在编译时给某个logic既做了连续赋值又在always块里赋值编译器会直接报错如果是多个模块同时驱动一个信号比如双向总线场景还必须用wire类型。我刚转SV时图省事到处用logic结果在写双向总线接口模型时吃了亏仿真信号一直是X态排查了半天才发现是多驱动问题。所以经验法则是单驱动信号用logic多驱动或总线信号用wire。再说二值逻辑。SV里bit、byte、int、longint这些是二值类型只有0和1两个状态仿真速度快、内存占用少但遇到X和Z就无能为力。硬件信号本身有四值状态0、1、X、Z所以描述DUT信号时用logic这种四值类型验证环境里的计算变量、参考模型里的数据对象用bit或int就够了。这样既能保证验证环境性能又不会丢失硬件信号的关键状态信息。如果定义了一个二值类型变量然后给它赋X值仿真结果会把它当作0处理这个行为在比对时容易埋雷一定要记住。2.2 四类数组队列、动态数组和关联数组各管一摊SV的数组体系比Verilog丰富得多也正好是验证环境里用得最多的数据结构之一。初次接触时最容易混的是固定数组、动态数组、队列、关联数组之间的区别。我简单整理过一张表后来讲给别人听基本一遍就能懂数组类型大小确定时机内存分配典型场景固定数组编译时确定静态分配固定深度的FIFO模型、配置寄存器数组动态数组仿真运行时运行时分配收集不定数量的数据包、暂存一组结果队列运行时动态增减自动管理FIFO、待处理事务列表、消息缓冲关联数组按需分配按索引稀疏存储寄存器地址索引、事务ID匹配、查找表动态数组和队列看起来很像区别在于队列支持头部和尾部的插入、删除操作效率很高适合当作FIFO来用动态数组更适合需要按下标随机访问的场景。关联数组是稀疏存储的典型我最常用到的地方是scoreboard里事务进来时用它的事务ID做索引把预期结果存进关联数组等DUT输出返回时再通过同一个ID取出比对。这种用法在Verilog时代几乎无法优雅实现在SV里几行代码就搞定了。还有一个不算数据类型、但极其常用的东西是枚举类型。Verilog里定义状态机通常用localparam加define写起来繁琐而且仿真时波形里看到的只有数字调试起来全靠记忆。SV里的enum可以给状态起名字编译器能自动检查非法赋值波形也能显示有意义的标签。我现在的状态机代码基本都会写typedef enum logic [1:0] {IDLE, RUN, DONE, ERROR} state_t; state_t state, next_state;这个写法的好处是代码自文档化仿真和综合工具能识别枚举类型赋一个不在枚举范围内的值会直接报错状态机安全性提升一个台阶。2.3 interface和clocking block连线的尽头是封装写FPGA的时候模块例化最烦的一步就是对着端口列表一个一个连线。信号少还好信号一多比如AXI总线的几十个信号端口列表写到怀疑人生一旦少连一根线或者连错位置编译报错都报得不直观。SV里的interface就是来解决这个问题的它把一组相关的信号封装成一个独立的对象模块只需要声明一个interface端口例化时把对应的interface对象传进来即可。举个例子假设要封装一组简单的请求应答信号interface handshake_if(input logic clk); logic req; logic ack; logic [7:0] data; clocking cb (posedge clk); default input #1step output #0; output req, data; input ack; endclocking modport dut_side(input req, output ack, input data); modport tb_side(clocking cb); endinterface这个例子里有两样东西值得注意。第一是clocking block它定义了测试平台相对于时钟沿的采样和驱动时机。默认写法input #1step output #0表示采样发生在时钟沿之前的那个时间步驱动则无延迟。这样设计的原因是避免仿真中的竞争情况——如果测试平台和DUT都刚好在时钟沿驱动信号不控制好采样和驱动时机读到的值就可能是未定义。clocking block把时序关系显式化验证环境就能稳定地采到正确数据。第二是modport它定义了不同端口DUT侧、TB侧看到的信号方向。同一个interface里DUT侧的input到了testbench侧就是outputmodport把这些关系声明清楚。模块例化时DUT连接dut_side测试平台连接tb_side不用担心方向写反。连线量从几十根降到一根可读性和可维护性完全是两个层级。3. 断言和随机约束验证质量的两块基石3.1 断言把应该永远成立写进代码初学SV时我对断言的认知停留在类似C语言的assert宏层面认为就是在代码里多写几个检查点。真正用上手才发现SV的断言体系远比这个强大尤其是并发断言它能用非常简洁的语法描述跨多个时钟周期的时序行为。先看两种断言的差异。立即断言immediate assertion在遇到该语句时立刻求值通常用在always块或函数里做状态检查比如检查状态机是否进入了非法状态。并发断言concurrent assertion则基于时钟沿采样可以描述复杂的时序关系是验证时序协议的主力工具。最常用的语法是assert property加|-蕴含操作符。比如我想验证请求信号拉高后1到3拍内应答信号必须拉高可以写property p_req_ack; (posedge clk) req |- ##[1:3] ack; endproperty assert property (p_req_ack) else $error(req拉高后1~3拍内没有收到ack);这就是把协议里的一句话翻译成代码。这条断言一旦挂上去仿真器会在每个时钟沿检查如果req为真那么后续1到3拍内必须看到ack为真否则报错。听起来很简单但这条断言把什么时候查、查什么都交给仿真器自动处理了而if语句需要你自己在正确的时机写检查逻辑。时序跨度越大断言的简洁优势越明显。写断言的时候有两点体验比较深刻。第一并发断言默认采样的是当前时间步之前的值也就是上一个delta cycle的值这一度让我困惑了很久后来才意识到这是为了保证不出现时序竞争。第二断言不仅能用于检查错误还能用于收集信息可以用cover property来统计某条时序路径是否被触发过这对功能覆盖率是非常有用的补充。3.2 随机约束与功能覆盖率让激励广撒网且不撒偏芯片验证的终极目标是找到设计中的bug而bug往往藏在边界条件和异常交互中。如果靠手工写定向测试用例典型路径能覆盖但边界条件、组合爆炸场景靠手工枚举根本不现实。SV的随机约束就是为了解决这个问题存在的让验证环境自动生成合法且有意义的随机激励。约束类的写法大概是这样的class transaction; rand bit [7:0] addr; rand bit [7:0] data; rand byte burst_len; constraint c_addr_range { addr inside {[0:16], [240:255]}; } constraint c_burst_valid { burst_len 0 burst_len 16; } endclass生产激励时只需要new一个对象然后调用randomize()仿真器里的约束求解器constraint solver就会在满足所有约束的范围内产生一组随机值。这里有一个常被误解的点随机不是乱随机而是在约束空间内随机。约束写得越精确生成的激励越符合协议要求验证效率越高。我第一次搭建带随机约束的验证环境时犯过一个很典型的错误给地址只写了一条宽泛的约束addr inside {[0:255]}结果大量仿真跑下来地址基本集中在0到200的区间200到255的边界区域一直没被触发覆盖率死活上不去。后来我把地址空间拆成几个区间用dist分别设置权重才让随机激励均匀撒开。这个经验说明约束并不仅仅是限制范围更是在指导随机分布的方向功能覆盖率和随机约束是强配套关系。说到覆盖率SV里用covergroup、coverpoint和cross来收集功能覆盖率。coverpoint可以统计某个变量落在不同分段的次数cross则能统计两个或多个变量组合的覆盖情况。功能覆盖率最大的意义是告诉你哪些场景测过了、哪些还没测到避免验证人员拍脑袋说测了很多啊却拿不出量化数据。我在实际项目里覆盖率做完了通常还会结合代码覆盖率行覆盖、条件覆盖、状态机覆盖一起看两者互补才能比较有把握地说验证做充分了。4. SystemVerilog不是全部和UVM的关系与定位4.1 UVM是方法学SV是语言学到SystemVerilog基础语法之后紧接着就会遇到一个大山UVM。很多初学者在这里会陷入一个误区以为UVM和SV是并列的两门技术或者觉得UVM是SV的升级版。其实两者的关系完全不同SV是一门硬件描述与验证语言UVM是基于SV构建的一套验证方法学和类库。打个比方SV就像一门编程语言UVM则像基于这门语言开发出来的一个框架。语言本身提供了类、继承、多态、随机约束这些工具但具体怎么组织一个验证环境——环境里放哪些组件、组件之间怎么通信、测试用例怎么启动和管理——SV并没有规定。UVM把这些最佳实践固化成了一个个标准类和机制uvm_env代表整个验证环境uvm_agent代表一组接口驱动和监视组件uvm_sequence和uvm_sequencer负责管理激励的产生和发送uvm_scoreboard负责数据比对uvm_reg层处理寄存器模型。验证工程师在写代码时不再需要从零搭建一切而是通过继承UVM的类、调用它提供的run_phase、connect_phase等回调机制把自己的逻辑插入到框架中去。理解这层关系很重要。我见过一些人学了几天UVM就开始看官方库源码结果被factory机制、config_db这些概念绕晕然后被迫回去补SV基础。这就是顺序搞反了。UVM的高级机制是建立在SV类体系的顶层之上的SV本身的类、继承、虚方法都没搞明白看UVM源码无异于天书。4.2 实际工程里SV和UVM怎么分工那实际的验证项目中SV和UVM各自的边界在哪里以我自己参与过的模块验证环境为例通常是这样的DUT还是用Verilog或SV设计代码编写testbench的顶层里面会例化DUT和interfaceinterface上面接的验证环境主体是UVM里面包括driver把事务转成interface上的信号时序、monitor反过来采集信号转换成事务、scoreboard比对参考模型和DUT的响应、sequence生成各种测试激励、env把上面这些组件装配起来。在这个结构里SV的接口语法负责和DUT对接断言负责实时检查协议随机约束负责生成激励UVM负责把这些内容按统一的框架组织起来并提供phase控制环境启动、结束的顺序提供factory机制让测试用例可以被灵活的配置覆盖或替代。可以说SV提供的是能力UVM提供的是组织方式。对企业来说UVM最大的价值是标准化和复用性。不同工程师写的验证环境只要都基于UVM其他人接手成本就很低看到uvm_env就知道是环境顶层看到sequence就知道是激励生成器看到scoreboard就知道是数据比对点。这一点在团队协作中的价值怎么强调都不过分。4.3 学习顺序和心态上的取舍所以我对学习路线的建议非常明确先扎实掌握SV的核心——数据类型、类与面向对象、随机约束、断言、接口这五件事然后在这几个能力都上手之后再去学UVM。UVM的三大核心机制factory对象创建的集中管理、config_db配置信息的全局分发、phase组件的生命周期管理本质上都是把SV本身的机制包装成更通用的工程模式有了SV基础理解起来就很自然没有基础则处处碰壁。这个阶段的学习心态也很重要不要指望一次性把所有UVM源码都看懂先会用再深入最后才谈得上修改框架。我在搭建第一个完整UVM环境时很多factory和config_db的细节其实是照葫芦画瓢等环境跑通、用例能跑出覆盖率之后再回头研究机制原理理解速度反而快了很多。5. 写给准备入坑的人学习路径和避坑经验5.1 是否需要先把Verilog学透这个问题有好几个人问过我我的回答要看方向来定。如果目标岗位是芯片验证那Verilog不需要学得面面俱到但有两个能力必须具备。第一是读RTL的能力验证工程师要能看懂设计的代码才能判断什么行为是预期内的第二是时序概念时钟沿、复位、建立保持时间这些基本概念必须清楚否则很难理解SV里的时钟块和并发断言。我个人的经验是我做了两年FPGA设计再转验证这段经历对理解DUT行为、和设计工程师沟通非常有帮助但并不是必要条件。如果是从零开始直接学SV只要愿意补一点数字电路基础照样能入门验证。关键不是先学Verilog还是直接学SV而是能不能理解硬件是并行执行的这个本质。SV再像软件语言它描述的验证环境最终依然是在和并行硬件打交道并行思维跟不上代码写出来只是披着SV外衣的C语言。5.2 工具链和仿真环境怎么选型学SV绕不开仿真工具。工业界主流是三大家Synopsys的VCS、Siemens EDA的Questa、Cadence的Xcelium其中VCS在大厂验证流程中占有率最高生态也最完善。这些商业工具对SV和UVM的支持都很完整。开源工具里Verilator是速度快的编译型仿真器但它对SystemVerilog的支持是子集尤其是UVM支持不完整适合用来跑基础语法和部分设计代码不太适合完整跑UVM验证环境。如果是自学我的建议是分两步走。前期快速验证语法正确性可以用开源的轻量环境把代码跑起来但很快你就会发现局限——很多SV的面向对象特性和UVM框架在开源工具里跑不通。如果想认认真真进入工业级验证流程还是得靠商业工具或者学校、公司提供的EDA环境。仿真环境搭建本身也是一项重要的实操能力包括编译脚本、波形导出、覆盖率收集、回归测试的维护这些和语言本身一样重要但经常被自学的人忽略。5.3 我踩过的几个坑写下来供参考第一个坑是reg和wire的思维惯性。刚用SV时总是下意识声明wire和reg后来改成logic但遇到多驱动场景又翻车。这个在前面提到过这里再强调一次单驱动用logic总线或多驱动用wire这个选择的依据是设计结构本身不是个人偏好。第二个坑是并发断言的采样位置。我写过一条断言在波形上明明看到ack在req后两拍拉高了断言却报错排查了很久才发现问题出在采样时机上。并发断言采样的是上一时间步的值想准确描述时序关系要么在clocking block里明确定义采样点要么对时序边界特别敏感的信号用$sampled()函数。这一块如果不亲自踩一次光看文档很难有深刻感受。第三个坑是随机约束的冲突。约束多了以后约束求解器会同时求解所有约束经常出现一条新约束和旧约束冲突导致某个变量始终无法随机化或者随机化失败。早期我遇到这种情况就傻眼后来学会看求解器的报错信息、用soft约束降低优先级、用solve...before控制求解顺序问题就少多了。约束和覆盖率一样设计的好的验证环境一定是经过反复调优的不是写上去就万事大吉。第四个坑是仿真编译时间越来越长。搭UVM环境时追求结构完整、组件齐全结果编译一次要花很长时间后来发现是环境里做了大量重复的配置查表和无效初始化。用config_db合理传递配置、减少顶层成员变量的滥用、把独立功能拆到独立的sequence里编译和仿真时间都降了下来。这个优化经验告诉我一个道理SV和UVM虽然功能强大但代码质量和工程化水平依然是验证环境可维护性的决定因素。如果让我给一条最朴素的自学建议那就是从一个小模块的验证环境开始比如给一个简单的ALU搭一套带interface、断言和随机约束的验证环境先把这套东西跑通再逐步加入UVM的组件。不要一上来就试图搭一个完整的大SoC验证平台。SV是一门需要在项目里反复打磨的语言语法书只是起点真正学会它靠的是把一个模块验证到流片标准的那段过程。