ARTICLE DETAIL

资讯详情

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

从零搭建UVM验证平台:AHB SRAM控制器实战指南

从零搭建UVM验证平台:AHB SRAM控制器实战指南 围观过不少UVM教程也照着跑过几个demo但真正到“自己从零搭一个能用的验证平台”这个坎上很多人就卡住了。项目三我选了ahb_sramc也就是AHB总线上的SRAM控制器作为UVM自学路上第一个完整体验“搭平台”的练手对象。选它的理由很直接AHB协议不复杂但足够典型SRAM控制器的功能点清晰、比对结果容易判断仿真也很快。这篇文章把我从空目录开始到最终跑通测试并完成功能收敛的完整过程记录下来包括我踩过的坑、改过的代码和想明白的道理给同样卡在“UVM基础看完但不会组织平台”阶段的读者做一个参考。1. 项目到底在做什么先把验证目标焊死1.1 为什么偏偏是AHB SRAM控制器很多初学者喜欢一上来就挑多核总线、GPU流水线这种大块头结果平台没搭完就被协议和巨型DUT折磨到放弃。AHB SRAM Controller是个非常理想的中间难度项目AHB协议本身是AMBA 2.0时代的经典总线信号就那么十几个状态机也不复杂但它包含了单拍读写、突发传输、等待状态、错误响应这些在真实项目中一定会碰到的机制。而SRAM控制器一端接AHB总线一端接SRAM存储阵列DUT行为本质上是“把总线操作翻译成存储器的读写时序”对不对一目了然。从验证角度看这个DUT的接口清晰地分成了两层AHB侧和SRAM侧。验证平台的核心任务就是验证这两层协议转换是否正确地址译码有没有错读写控制信号时序是否满足SRAM的时序要求以及在异常情况下行为是否可控。功能点可控就意味着一开始就能写出明确的功能覆盖率和定向测试用例这对平台自身的调试也友好。还有一个隐性收益AHB SRAM Controller自带一个极佳的参考模型就是一块普通的内存数组。你不需要模拟复杂算法只需要在ref model里用二维数组模拟存储行为所有读写操作的结果天然可预测。这让scoreboard的编写难度降到了最低你能把更多精力花在UVM机制本身。1.2 平台要回答哪些问题动手写代码之前我先把这个DUT的功能验证点列了出来避免后面在过程中迷失方向基本读写单拍写、单拍读数据比对是否正确。地址译码不同地址区间是否映射到正确的SRAM bank。突发传输INCR、WRAP等burst类型下地址递增和回卷是否正确。等待状态SRAM busy时HREADY拉低总线是否插入等待周期且数据不丢失。复位行为复位释放后内部状态机和对外信号是否正确回到默认值。寄存器访问控制/状态寄存器的复位值、读写属性、bit位行为是否正确。这些功能点不只是在项目开头用一次后面写sequence、搭建reference model、定义覆盖组全部要回来对照这个清单。建议你也先花半小时把DUT的验证功能点列清楚不要边写边想。2. 从零搭建目录结构与组件规划2.1 先把目录结构长这样我不喜欢把所有文件堆在同一个目录里项目第三天开始代码量就爆炸了后期找文件能把人逼疯。这次我直接按UVM验证组件类型分目录结构如下dut/DUT的RTL代码和testbench顶层需要include的文件tb/模块级testbench顶层包含接口、DUT例化和时钟复位生成env/uvm_env以及env内各个agent、scoreboard、reference model的封装agents/AHB agent包含driver、monitor、sequencer和transaction定义reg/寄存器模型相关文件reg block、reg adapter、predictortests/所有testcase包括base_test和各定向用例seqs/sequence库集中存放各类读写序列目录规划看起来是个纯体力活但它决定了后面工期的效率。UVM本身就是组件化框架目录和组件一一对应后每个文件该改哪里、该查哪里都极其清楚。尤其是项目做到一半发现bug要回来追问题的时候干净的结构能省下一整天。2.2 组件连接关系与层次设计AHB agent我选择了UVM中最常用的配置agent内部例化sequencer、driver和monitor。driver通过sequencer拿到sequence产生的事务解析后驱动到AHB接口上monitor只负责观测总线把总线上实际发生的传输收集下来作为分析数据发送给ref model和scoreboard。在env层我例化了两个AHB agent一个连接在DUT的AHB Slave接口上作为总线从机侧观测另一个作为控制平面访问寄存器用。其实很多设计里寄存器和数据通路共用一套AHB口这时候只需要一个agent但寄存器模型要通过它来读写。我为了模拟真实项目中的主从接口分离做了两个agent并在env中组织好连接关系这样后续单独维护reg access和data access也更方便。scoreboard在这里扮演两个角色一是接收monitor收集到的AHB总线事务二是把期望结果和实际结果做比对。reference model则接收相同的事务产出期望的SRAM写入值或读取值。整体上所有check不在driver或monitor里做统一收敛到scoreboard保证每个组件职责单一。这一点是UVM架构比较重要的原则data check集中做方便debug也方便统计pass/fail。3. 核心组件实现思路与关键代码3.1 transaction与interface设计要点transaction是所有组件之间传递数据的载体。AHB传输的transaction我定义的字段包括addr32位地址direction读/写size传输位宽1/2/4字节burst单拍、INCR、WRAP4/8/16等data[]写数据队列rdata[]读数据队列error是否返回ERROR响应transaction里我特别加了两个约束地址对齐约束burst长度与类型一致性约束。前者是为了符合AHB协议里地址必须按size对齐的要求后者是为了避免sequence随机出不合法的传输组合直接在源头减少无效测试。interface设计上AHB总线的信号按照协议分成三组全局信号HCLK、HRESETn、读写地址/控制信号、数据信号。我使用了clocking block来定义采样和驱动的时钟方向。这里有一个关键点driver侧用output clockingmonitor侧用input clocking两边看到的时钟边沿必须一致否则会出现激励先于时钟或采样错位的诡异问题。interface的modport规划也同样重要我分别定义了ahb_drv、ahb_mon两个modport避免driver和monitor在同一个interface上互相干扰。所有信号宽度、方向、时序都在interface这一层约束好后面对RTL集成时非常省心。3.2 driver、sequencer、monitor三件套配合UVM中sequence只是产生“意图”真正把意图变成物理信号的是driver。driver的run_phase核心逻辑我简化为一个死循环从seq_item_port获取一个事务。处理reset信号状态等待DUT就绪。根据事务的burst和size计算本次传输占用的周期数。逐个周期驱动HADDR、HTRANS、HWRITE、HSIZE、HBURST、HWDATA等信号。每个周期采样HREADY为低时插入等待周期保持信号不变。传输完成后调用item_done通知sequencersequence收到response后才能继续下一个包。很多人第一次写driver时容易忽略HREADY的处理。AHB是流水线结构driver驱动当前传输的同时总线可能还在处理上一拍的数据所以每一拍都必须同步检查HREADY。如果这个地方不处理仿真里会看到数据提前被覆盖或采样到错误的值。我当时的做法是把它做成状态机address phase和data phase分开推进每一拍都用clocking block采样HREADY且采样结果立刻参与状态机的下一拍跳转。monitor的实现相对简单但同样要用clocking block保证采样时序。它持续观察总线捕获HSEL、HADDR、HWRITE、HREADY等信号的跳变每完成一笔AHB传输就打包一个transaction然后通过analysis port广播出去。这里有一个容易踩的坑monitor要区分“传输正在进行”和“传输刚完成”的边沿我通过在总线空闲时记录上一周期状态设置了一个transfer_done标志确保每个cycle只在正确的时点发送事务避免重复发包或漏发包。注意driver和monitor如果共同驱动同一个interface有些团队会把driver和monitor挂在同一个agent下共享interface但请务必分清方向。AHB是单向主从架构driver只驱动主设备侧信号monitor则观测同一总线两边不要试图“互相修复”否则仿真时间长了波形里会出现奇怪的竞争。3.3 reference model与scoreboard让比对自动化Reference model我直接用SystemVerilog的关联数组模拟SRAM存储空间。每个address对应一个4字节数据写操作更新数组读操作从数组取数。针对SRAM控制器的地址译码我维护了多bank映射实际项目里会有bank选择逻辑这里用hash表自然能覆盖。Scoreboard的比对逻辑其实很直观它接收monitor广播的真实总线事务同时接收ref model算出的期望事务然后按事务序号匹配比较地址、数据、方向和error标志。如果整个平台只有单个master访问比对可以做得简单但为了更接近真实场景我在scoreboard里维护了一个expect_fifo队列ref model产生期望结果时压入队列monitor产生实际结果时从队列弹出比对这样即使总线乱序也能通过事务ID或地址匹配找回对应关系。值得提醒的是reference model里必须处理等待状态对时序的影响。AHB传输中如果主机插入了等待周期控制器的数据采样时点会相应推迟。ref model不需要模拟每个cycle的波形但要保证“事件顺序”和“最终结果”与实际一致否则scoreboard会发生误报。我第一次实现时直接忽略了等待周期结果随机测试一跑就挂后来才把“等待周期同样会延迟数据写入”这一点补上。4. 寄存器模型与镜像值处理别让reg model变成摆设4.1 用寄存器模型管理配置和控制UVM项目里寄存器模型几乎是标配。它本质上是一种高级抽象层让test就不用再手写一堆driver transaction去访问寄存器而是通过reg sequence直接进行读写操作。省下的代码量是一个方面更重要的是它提供了镜像值、预测值、寄存器覆盖率这些机制。搭建寄存器模型时我定义了一个reg_block里面映射了控制寄存器R/W、状态寄存器RO和版本寄存器。利用uvm_reg_map将寄存器映射到AHB总线的地址段上再通过一个reg_adapter把寄存器操作转换成AHB的sequence item。简单说reg_adapter是reg模型和底层总线协议之间的翻译官。4.2 镜像值mirror value的坑镜像值是寄存器模型中比较容易懵的概念。简单理解镜像值是软件认为的寄存器当前内容它是硬件寄存器值的“软件副本”。寄存器模型会通过predictor自动更新镜像值也可以主动调用mirror/update来同步。实际使用中我踩过一个坑刚开始我没有连接predictor只用了reg model的peek和poke读写功能导致镜像值一直不更新。后来想用reg_model.mirror()去检查寄存器内容怎么都比对不上排了半天才发现是predictor的predict操作没有自动触发。UVM默认行为下只有通过reg_sequence发起的读操作才能自动更新镜像值而DUT内部硬件自己改寄存器时必须接入predictor通常是在ahb_adapter里调用reg_model.predict()才能保持镜像值与硬件一致。另一个镜像值相关的踩坑点是update操作。当你对被硬件自动修改的寄存器调用update()时如果镜像值还是旧值update会把旧值重新写回硬件反而造成寄存器被“纠正”成错误的值。所以在写寄存器配置sequence时我养成了一个习惯只对真正由软件控制的R/W寄存器使用update对硬件可修改的状态寄存器要先用mirror()同步之后再决定是否需要write。寄存器模型不是越多用越好理解它背后的“预测-镜像-更新”机制才是关键。如果只是为了读写方便你甚至可以不用reg model但一旦要检查复位值、做回归检查的时候reg model的价值就完全体现出来了。5. 仿真调试实录那些你一定会遇到的经典问题5.1 sequence启动与响应链路问题UVM初学者最先遇到的坑往往是testcase跑了但sequence一个包都没发。排查顺序我总结为“四查”一查default_sequence有没有正确配置在sequencer上二查启动前有没有raise_objection三查sequence的body里事务类型和sequencer的req类型是否匹配四查driver有没有正常获取到事务。这里特别提一下“不回respond但也只能发八个包”的现象。我在调试时遇到过sequence只发出固定数量的事务之后就不再继续的情况很像UVM通道满了被阻塞。后来检查发现driver在接收sequence事务后确实做了处理和驱动但没有正确使用seq_item_port.item_done()或者response队列没人读取导致sequencer认为sequence的事务还没处理完内部阻塞了后续的包发送。这种问题不会立刻报错而是在长时间跑随机用例时暴露出“跑了一会儿就停”的假死症状。所以driver里务必在事务驱动完成后调用item_done并且按照实际需求设置rsp端口与sequence的连接避免响应链路堵死。5.2 复位与等待周期的时序问题第二个高频问题出在复位。DUT有全局异步复位但monitor在复位期间依然会采样到不定态的信号。我的处理方式是在monitor的run_phase里先等待复位释放用wait(!rst_n)或wait(rst_n)再开始采样总线。如果不加这个等待复位期间采集到乱数据会被误当成有效传输发往scoreboard导致大量误报。等待周期相关的另一个坑是driver驱动数据与HREADY握手时的时序。AHB要求地址相位与数据相位重叠如果你在driver里写的是“先驱动地址等一个cycle再驱动数据”低效但也可能碰巧对一旦遇到多个burst或插入等待周期时序就乱了。建议严格按照AHB的流水线行为来建模每个cycle先采样HREADY再根据当前状态决定驱动下一拍内容。这样无论插多少等待周期数据都不会丢。5.3 常见问题速查表现象可能原因排查办法sequence一个包都不发default_sequence未配置、objection未raise检查test的build/connect阶段配置run_phase开头raise_objection只发少量包后停止未调用item_doneresponse链路阻塞driver中确保每个事务完成后调用item_done并连接rsp端口scoreboard大量误报monitor在复位期间采样到无效数据monitor等待复位释放后再采样过滤复位段事务reg_model.mirror()总是比对失败predictor未连接镜像值没有预测更新在adapter中连接predictor或用reg_model.predict更新镜像值随机测试偶尔失败约束没有覆盖边界未对齐地址、burst长度不符在transaction中补充协议约束增加定向边界用例仿真卡死或超时某个component的objection一直挂着phase无法结束检查每个run_phase的raise/drop配对必要时使用phase timeout机制5.4 排查问题的工具与技巧你不要指望只靠打log就能找到这些隐蔽问题。UVM自带的信息打印机制要用好重点阶段打开UVM_HIGH或UVM_DEBUG级别尤其是sequence的启动和事务流转过程。另外我习惯在monitor和scoreboard里分别加上事务计数器每次打出transaction序号两边数字一旦不一致问题就锁定在传输链路中段。波形文件也是debug利器。很多问题在log里看起来都是随机出现的但拉到波形上一看就清楚比如driver驱动的HWDATA在HREADY拉低的周期里变了这明显是状态机没有锁存数据或者monitor在复位释放前的第一个上升沿就采到了HSEL有效这是采样窗口没设对。把这些case存档回归时遇到同类问题能快速定位。6. 项目收尾把平台用起来并持续补强平台跑通一轮基本功能之后我做的第一件事不是去写更多sequence而是先跑一遍定向的所有验证点确认每一个功能点都至少有一个对应的testcase。然后才开始做随机约束测试把地址、size、burst类型、数据内容作为随机变量覆盖组统计哪些组合还没测到。这个过程能非常直观地检验平台健壮性。之后我又给平台加了一个小功能在env层加了一个reference model的reset同步逻辑并在scoreboard里增加对复位后状态的检查。这一步让复位相关的验证点完全自动化不需要人工去翻波形确认回归的时候省了很多事。最后再分享一个体会UVM学习最大的瓶颈往往不在语法而在“组织”能力。如何把一个验证任务拆解成清晰的组件如何让每个组件各司其职如何用正确的方式连接它们这些才是平台能不能稳定跑起来的关键。ahb_sramc这个项目规模不大却能完整覆盖从DUT分析、组件搭建、寄存器模型到随机约束测试的整个流程跑通它之后再看其他IP的UVM验证环境你会有一种“原来到处都长这样”的通透感。我在实际做这个项目的过程中最大的收获不是掌握了几段代码而是学会了怎么带着“验证目标”去设计平台架构。先想清楚要验证什么再决定搭什么组件最后才写代码这个顺序不要反。后续如果你想继续深入可以试试在这个平台里加入AHB总线功能覆盖率模型或者把SRAM换成伪双口RAM模型改动量不大但每一次改造都会让UVM理解深一层。
返回列表