ARTICLE DETAIL

资讯详情

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

UVM验证环境中AHB接口与事务的设计要点解析

UVM验证环境中AHB接口与事务的设计要点解析 IC验证里的活儿有一多半是花在“把协议落到代码”这一步上的。上一篇笔记我搭好了AHB-RAM验证环境的大体框架今天专门把接口interface和事务transaction这块掰开揉碎了讲。你要是也在写AHB总线相关的UVM环境这篇应该能帮你少踩几个坑。先说清楚为什么把这两块放在一起。接口和事务一个是验证环境和DUT之间的物理通道一个是数据流动的载体。两者一个负责“怎么把信号正确打出去”一个负责“这次传输到底想干什么”。在AHB-RAM这种相对简单的项目里接口定义得干不干净事务约束写得稳不稳直接决定后面driver、monitor、scoreboard能不能顺利串起来。我见过不少同学一上来就闷头写driver结果回头发现interface里信号方向都放反了整个环境推倒重来那滋味是真不好受。我自己写UVM组件有个习惯顺序先定接口再定事务然后才是driver/monitor最后sequence和scoreboard。原因很简单接口决定了你能用什么样的方式驱动信号事务决定了你能产生什么样的激励。这两个地基不打牢上面的房子早晚会歪。1. 接口与事务代码在整个验证环境中的位置1.1 为什么先写接口和事务——两个基础的先后顺序很多UVM初学者容易犯一个错误以为验证环境的核心是driver、是scoreboard所以一上来就狂写组件。但等写到driver的run_phase时发现自己不知道该用什么方式去握手信号什么时候驱动、什么时候采样全都靠猜。这就是因为没先把interface想清楚。AHB总线是典型的非连续式握手协议地址周期和数据周期在同一个时钟周期内交错完成。如果interface里没有定义好时钟块clocking block或者至少明确约定驱动与采样的时序边界那么driver采到的是不是稳定信号monitor采到的是不是有效数据全靠运气。而在验证环境里“靠运气”是最忌讳的。事务类的设计也一样。AHB传输有读有写有单次传输有突发传输burst突发又分INCR和WRAP两种。这些属性如果不预先在transaction里定义清楚并且约束好组合关系后面写sequence的时候要么随机出来的激励根本不符合协议规范要么就是约束之间互相冲突编译过不了。所以我把interface和transaction当成一对孪生兄弟来写先定它们再谈别的。1.2 AHB-RAM 验证环境的模块拆解与读写路径AHB-RAM这个DUT本质上就是一个挂在AHB总线上的从设备slave内部是一块可读写的存储器。验证环境里主要验证的是master发起的读写操作能不能正确访问到RAM的地址空间burst传输时地址能不能按规则自增或回卷以及访问非法地址或者不支持的对齐方式时能不能正确返回ERROR响应。所以接口侧需要有完整的AHB信号集合包括全局信号HCLK、HRESETn地址与控制信号HADDR、HTRANS、HWRITE、HSIZE、HBURST数据信号HWDATA写数据、HRDATA读数据响应信号HREADY、HRESP事务侧则需要把一次完整的AHB访问抽象成对象。比如随机一个地址、选择传输类型、指定数据宽度、设定突发模式再填入数据内容。这样driver拿到transaction之后照着字段把信号依次驱动出去monitor从接口上抓到一次传输也能反过来填出一个transaction交给scoreboard比对。![AHB-RAM验证环境模块关系图]注作为文字笔记这里不贴图。模块间的数据流关系是sequencer产生transaction → driver通过interface驱动DUTDUT输出响应 → monitor通过interface采样并重组transaction → scoreboard比对。2. AHB 接口的代码设计要点2.1 信号集合与 modport 划分接口代码最基础的部分是把AHB需要的信号都声明出来。这里有个经验信号的宽度一定要和AHB-RAM的实现匹配。比如地址总线宽度是32位还是更宽数据总线是32位还是64位这些在一开始就要和RTL设计对齐否则后面到处是位宽截断的问题。SystemVerilog的interface里我习惯把信号声明和modport分开写。信号声明只负责“存在”modport负责“方向”。interface ahb_if(input logic hclk, input logic hrstn); logic [31:0] haddr; logic [1:0] htrans; logic hwrite; logic [2:0] hsize; logic [2:0] hburst; logic [31:0] hwdata; logic [31:0] hrdata; logic hready; logic [1:0] hresp; modport master_mp( output haddr, htrans, hwrite, hsize, hburst, hwdata, input hrdata, hready, hresp ); modport slave_mp( input haddr, htrans, hwrite, hsize, hburst, hwdata, output hrdata, hready, hresp ); endinterfacemodport的作用是让后续的driver、monitor在连接时明确自己能看到哪些信号以及这些信号的方向。比如driver连接的是master_mp那它就把地址、控制信号打出去把响应信号采进来。monitor如果是用于观察总线上的完整活动它连接的是slave_mp视角也能采因为从从设备角度看所有进入的信号它都能读到输出的响应它自己也知道。关键是你的monitor设计成哪种用途是只监控DUT收到的请求还是连DUT的响应也一起观测。我用modport还有一个额外的好处在编译阶段就能查出方向接反的问题不用等到仿真跑起来报X态。尤其是项目后期一个interface被多个组件引用modport的方向还能起到阅档的作用。2.2 clocking block 的取舍与复位处理接口里要不要用clocking block我见过两派。一派觉得clocking block是SystemVerilog提供的同步机制能自动处理驱动和采样的时序偏移必须用另一派觉得clocking block容易引入额外的层次调试信号不太直观不如直接(posedge clk)手动控制。我个人的习惯是小项目、信号少的接口不用clocking block手动控制时序反而更直观大项目、信号多、多master多slave的复杂互联用clocking block能少写很多重复的时序控制代码。AHB-RAM这个项目只有一个master一个slave为了保持代码简洁不引入clocking block在driver里统一用(posedge hclk)配合适当的非阻塞驱动方式来做时序控制。但不管用不用clocking block复位处理是一定要注意的。AHB的复位是异步复位的典型场景。接口本身不需要对复位信号做特殊处理但DUT在复位期间对输入信号有确定性的要求接口上不能悬空。比如复位释放后第一拍master驱动出去的HTRANS应该是IDLEHWRITE应该是低电平HADDR可以是任意值但最好不要有X态。我见过一个很典型的问题interface里的信号没有在复位时被初始化成确定值导致复位释放后第一个时钟沿DUT看到HADDR是X直接跑飞了。这种问题在波形上看起来像是DUT的错实际上责任在验证环境的激励侧。// 在driver的reset任务里把输出信号置为确定的默认值 task driver::reset_ahb(); vif.master_mp.haddr 32h0; vif.master_mp.htrans 2b00; // IDLE vif.master_mp.hwrite 1b0; vif.master_mp.hsize 3b010; // word vif.master_mp.hburst 3b000; // SINGLE vif.master_mp.hwdata 32h0; endtask虽然这段代码写在driver里但真正跑起来之前你得在接口层面就想好这些默认值是什么。所以我通常会在interface里加一个initial块把这些输出信号在仿真一开始就初始化掉避免在driver启动之前出现X态。2.3 两个方向的驱动接口该如何组织AHB总线是读写共用地址通道的但数据通道是分开的写数据通道从master到slave读数据通道从slave到master。这个特点在接口设计时就被体现出来hwdata在master视角是输出在slave视角是输入hrdata刚好相反。这里有一个容易踩的坑AHB规定写数据必须在地址之后的第二个周期才有效也就是说如果当前周期master发出写命令和地址对应的写数据要延迟一拍才能出现在总线上。在接口层面这不算接口的问题但你在设计transaction和driver的接口配合时一定要把这个一拍延迟考虑进去。有些同学设计transaction时把地址和数据放在同一个字段组合里结果driver里发现数据比地址晚了一拍从transaction里取出来的数据和当前周期对不上就会乱。其实解决办法也简单。在interface里把hwdata正常声明在driver里用一拍寄存的方式把写数据打一拍再输出。这样接口定义不需要为这个延迟做特殊处理但driver内部要明确知道这个阶段关系。3. 事务类的事务代码设计3.1 transaction 的字段设计事务类继承自uvm_sequence_item这个不用多说。它描述的是“一次AHB访问请求”的完整属性。字段怎么定我先列一下我这个项目里用的class ahb_transaction extends uvm_sequence_item; uvm_object_utils(ahb_transaction) rand bit [31:0] haddr; rand bit [1:0] htrans; rand bit hwrite; rand bit [2:0] hsize; rand bit [2:0] hburst; rand bit [31:0] hwdata; rand bit [31:0] hrdata; rand bit [1:0] hresp; constraint c_trans_valid { htrans inside { [2b00:2b11] }; } constraint c_burst_single { hburst 3b000; } constraint c_size_word { hsize 3b010; } // 后续可以再增加其他约束 function new(string name ahb_transaction); super.new(name); endfunction function string convert2string(); string s; s $sformatf(addr%0h trans%0b write%0b size%0h burst%0h wdata%0h rdata%0h resp%0b, haddr, htrans, hwrite, hsize, hburst, hwdata, hrdata, hresp); return s; endfunction endclass这里有几个字段我觉得是必须的地址、传输类型、读写方向、数据宽度、突发类型、写数据、读数据、响应状态。读数据和响应虽然是DUT返回给验证环境的但放不放在transaction里取决于你的用法。如果你打算用同一个transaction类在driver回读结果然后直接传给scoreboard比对那就把hrdata和hresp也放进来driver在接收阶段把它们更新到transaction中。如果读写分开成两个类那就另当别论。我在这类项目里倾向于读写共用一个transaction用hwrite这个位区分。这样比较简洁sequence里也不需要创建两种不同类型的对象。3.2 约束——让随机化不瞎跑事务类是激励的载体UVM的精髓在于可约束的随机化。约束写得好随机出来的激励既有覆盖面又不会违反协议。约束写得烂轻则随机不到目标场景重则约束冲突仿真一跑就挂。AHB协议的约束有几个重点第一个是HTRANS和HBURST的配合。当突发是INCR或WRAP类型时burst的第一个beat必须是NONSEQ2b10后续beat是SEQ2b11。如果混入IDLE或BUSYdriver就要特殊处理而大多数简单AHB-RAM的实现并不支持在突发中间插入BUSY周期。我的项目里就直接约束成最简形态要么SINGLE NONSEQ要么固定长度的INCR/WRAP不产生中间BUSY。第二个是地址和数据宽度的对齐。如果HSIZE表示byte宽度那么HADDR必须对齐到该传输宽度。比如HSIZE3b0104字节word地址的低两位就应该是2b00。这个约束如果不写随机出来的地址很可能是非对齐的DUT的行为就很难预测。当然如果你想验证非对齐访问的错误处理那就得在其他地方做例外约束。// 地址对齐约束示例 constraint c_addr_aligned { if (hsize 3b000) haddr[0] 1b0; else if (hsize 3b001) haddr[1:0] 2b00; else if (hsize 3b010) haddr[1:0] 2b00; } // burst长度与传输类型的组合约束 constraint c_trans_burst { if (hburst 3b000) htrans 2b10; // SINGLE - NONSEQ else htrans inside { 2b10, 2b11 }; // INCR/WRAP - NONSEQ or SEQ }第三个约束是写数据的随机化。如果是写操作hwdata应该随机成合理的数据如果是读操作hwdata就不用管。在UVM里可以用if (hwrite) hwdata.rand_mode(1); else hwdata.rand_mode(0);这种方式但不建议在约束里动态开关。更稳妥的做法是在sequence里根据读写方向去控制。3.3 打印与记录排查问题的基础事务类的打印功能听起来不起眼实际调试的时候能救命。UVM自带的uvm_object的print方法可以打印字段但那格式太啰嗦了波形上一堆字段真出了错反而不容易一眼定位。我在ahb_transaction里重写了convert2string()把关键信息压缩成一行。这样在driver里打印“当前驱动的事务”在monitor里打印“当前采样到的事务”日志文件短且定位快。function string convert2string(); string s; s $sformatf(addr%0h trans%0b write%0b size%0h burst%0h wdata%0h rdata%0h resp%0b, haddr, htrans, hwrite, hsize, hburst, hwdata, hrdata, hresp); return s; endfunction还有个习惯在transaction里加上do_copy、do_compare等UVM标准回调函数的实现。虽然对简单字段UVM默认的uvm_object的字段自动操作field automation也能搞定但我更喜欢显式写清楚。特别是后续scoreboard里要做比对如果没有显式的do_compare两个transaction对象的比对可能只比了部分字段漏掉你真正关心的数据。手写一遍虽然麻烦但绝对不会因为字段自动操作宏的某些边界问题而出错。4. 驱动时序与接口握手的几个关键细节4.1 HREADY 与 HRESP 的握手节奏AHB的握手核心是HREADY。从设备AHB-RAM通过HREADY信号告诉总线“我能不能接收当前传输”。高电平时表示可以接收低电平时表示需要插入等待周期。在AHB-RAM这种没有等待周期的简单实现里HREADY通常会被拉高一直处于就绪状态。但验证环境不能因此就把HREADY忽略掉。我在monitor里一定会采样HREADY只有HREADY为高的时钟沿才算完成一次有效传输。否则当你在做burst传输时中间插了一个等待周期地址通道和数据通道的对应关系就会错位。有一个细节特别容易忽略HREADY是输入到master的所以在driver的视角里判断一次传输是否完成必须在(posedge hclk)之后立刻采样hready。如果hready为高说明这次传输被DUT接收了如果为低说明还需要把当前的地址和控制信号继续保持一个周期。很多初学者把驱动逻辑写成“在posedge之前把信号准备好posedge之后立即换下一笔”这样在DUT插入wait state时就会出大问题。HRESP一般和HREADY配合使用。AHB-RAM如果访问地址越界或传输类型非法返回ERROR响应。在接口代码里hresp这个字段必须保留即便DUT的简单实现里永远返回OKAY也要在monitor里把这个信号采回来因为不定什么时候你就要验一个带错误处理的RAM变体。4.2 复位后接口信号初始化的坑复位是验证环境里问题最多的地方之一。interface的信号在仿真一开始如果没初始化默认值是X。DUT的复位逻辑看到X可能会产生意想不到的行为比如把内部状态机打到非法状态或者因为X在比较器里传播把后续所有逻辑全部带偏。我处理的办法是在interface里用一个initial块把所有需要master驱动的信号赋默认值。这样即使driver还没启动总线上也处于“空闲且确定”的状态。initial begin haddr 32h0; htrans 2b00; hwrite 1b0; hsize 3b010; hburst 3b000; hwdata 32h0; end有的工程师觉得这样不够规范因为它们希望在复位期间保持信号为X以便检查DUT的复位行为是否正常。但我的经验是X态在验证环境里更容易制造噪音而不是发现bug。真正要检查DUT的复位行为应该用formal或断言而不是靠接口信号的X传播。接口默认值这块稳妥优先。4.3 地址对齐与突发长度的边界处理AHB的突发传输有INCR自增和WRAP回卷两种。INCR突发里地址按传输大小递增。比如4拍的INCR4、word传输每拍地址加4。WRAP突发则是在一个边界内循环比如4拍的WRAP4、word传输从0x4开始到0xC回卷到0x0。这个边界处理如果放在driver里写容易乱。但如果你在transaction的字段里把起始地址、突发长度、数据大小都约束好顺序问题其实可以提前算好。我在AHB-RAM这个项目里把事务类和接口设计成“地址生成在transaction里完成driver只负责按拍把地址打出去”。也就是说burst的第一拍地址由sequence随机产生之后每一拍的地址是在driver内部通过组合计数器算出来的。这样transaction不需要存一个地址数组driver也不需要对每个burst类型做分支处理因为AHB的地址递增规律本来就是固定的。不过要注意一点WRAP的地址回卷边界等于burst长度乘以传输大小。比如WRAP4、word传输回卷边界是16字节。如果起始地址的低4位不是0那中间一定会跨边界回卷。这个你心里要有数否则比对数据时会发现地址突然跳变容易误判为bug。5. 实际问题排查与技巧5.1 常见问题速查表我把AHB-RAM验证环境搭建初期最容易遇到的几个问题整理成了一张表都是我实际调试时踩过的坑你在写接口和事务的时候遇到类似现象可以直接对着查现象大概率原因排查手段仿真一开始总线就是X态DUT内部逻辑异常接口信号没有复位初始化加initial块把所有物理输出信号赋确定默认值driver发出的HTRANS永远不对DUT总是在等待htrans的驱动时序比地址晚了一拍检查driver里是否用了非阻塞驱动且与地址赋值有依赖关系burst传输中第二次数据比对失败monitor采到了无效周期把等待周期当成有效beat判断有效传输时必须同时满足hready1和htransNONSEQ/SEQtransaction约束随机化失败多个constraint之间互相矛盾比如既要求SINGLE burst又要求htrans是SEQ用solve...before...指定优先级或拆分成不同的sequence约束monitor打出来的transaction总是少字段依赖field automation宏但没有把关键字段登记完整改用显式的convert2string或手写do_compare复位释放后第一个传输的地址带Xreset任务里的信号初始化时机太晚在driver run_phase之前在interface的initial里先初始化而不是依赖driver启动5.2 一点心得接口写错了后面全乱从实际经验来说接口和事务代码的编写并不是“照着协议文档敲一遍”那么简单。协议文档告诉你的是信号规则但验证环境还要求这些规则在时序上精确无误地落到interface上。我建议你在动手写driver之前先用一个简单的testbench直接驱动一轮读写确认interface上信号的波形和预期一致。这一步只需要十几分钟但能帮你省掉后面好几天排查环境问题的时间。事务类的约束部分也要从“最小可用”开始迭代。先把SINGLE burst、word传输、地址对齐这几条基础约束跑通再逐渐放开到INCR、WRAP最后再考虑非法地址、错误响应这些异常场景。不要一上来就堆一大堆约束否则一旦随机化失败你根本分不清是哪个约束写错了。另外我个人还有个习惯每写完一个部分会立刻用一小段仿真验证一下。写完interface就裸驱动看波形写完transaction就随机打印一批看看约束效果。这个习惯帮我挡掉了很多低级错误也让代码维护起来心里有底。AHB-RAM的接口和事务定下来之后后面写driver和monitor其实就是顺着这条道往下走下一篇笔记我打算继续讲driver的实现到时候再看看这个接口是怎么被实际驱动起来的。
返回列表