ARTICLE DETAIL

资讯详情

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

UVM寄存器模型深度解析:sequence、adapter与镜像值机制及常见卡死问题排查

UVM寄存器模型深度解析:sequence、adapter与镜像值机制及常见卡死问题排查 写这一篇笔记的时候我已经把寄存器模型的骨架搭起来了也顺利在验证环境里挂上了default_map能用reg_model.xxx_reg.write()和read()往DUT里写值、读值了。按理说这就算入门了可真正开始写用例、跑回归、查问题的时候才发现前面那点操作只是冰山一角。这篇笔记记录的是我继续往寄存器模型深处走时踩过的坑以及反复查源码才想明白的几个关键机制sequence到底是怎么和寄存器模型配合的、adapter里那两个函数在干什么、镜像值mirrored value到底是怎么来的还有一个很有意思的现场问题——如果driver不回response为什么寄存器模型的sequence只能发有限的几个包就卡住了。这篇内容适合两类人一类是刚把UVM寄存器模型搭起来、能用但说不清内部过程的同学另一类是已经用了一段时间、正被“mirror对不上”“环境卡死”这类问题折磨的验证工程师。我尽量把原理和实操串起来讲有些坑不是看书能看出来的是仿真器帮你踩出来的。1. 先把三件事说清楚结构、映射、预测1.1 你搭好的reg_model本质上是一个“影子数据库”很多人一开始容易把寄存器模型理解成一个“自动化寄存器读写工具”觉得它就是把write和read封装了一下方便在sequence里调用。这个理解不算错但会限制后面排查问题的思路。我自己的感受是寄存器模型真正核心的价值在于它在验证环境里维护了一份关于DUT寄存器的软件视图这份视图不是实时读出来的而是靠一套规则“推演”出来的。你可以把它想象成一个影子账本。DUT内部的寄存器是现实世界寄存器模型是外部挂的一本镜像账。你自己通过总线写寄存器相当于在账本上记了一笔硬件逻辑自己改了寄存器比如DMA搬运完成之后把done位置1如果没有人通知模型去改账账本就是错的。等你在测试结束时候去检查“DUT寄存器是不是符合预期”拿到的就是个过期数据折腾半天还以为是DUT的bug。搞清楚这个定位接下来很多设计意图就顺了。寄存器模型内部同时保存了desired value和mirrored value这两个值加上DUT内部的真实值构成了三个层次的概念。很多新手初期只关注write和read两个操作完全忽略另外几个操作导致后面看别人的验证环境时总觉得代码云里雾里。先把“影子数据库”这个模型装进脑子里后面讲predict、讲update、讲mirror你都能找到对应的位置。1.2 map、adapter、predictor三方分工搭建寄存器模型时有三个组件经常被混在一起说但它们的职责完全不同组件职责最容易踩的坑uvm_reg_map地址映射把寄存器偏移地址变成总线地址同时决定前门访问走哪条sequencer忘了在default_map上调用set_sequencer前面访问直接报致命错误uvm_reg_adapter协议翻译把寄存器模型标准的uvm_reg_bus_op转成具体总线transaction再把总线transaction转回标准结构reg2bus和bus2reg里的地址/数据没对齐读写值看起来总是不对uvm_reg_predictor状态同步监听总线上的真实事务把镜像值刷新为最新monitor没连好镜像值常年不更新检查时疯狂报mismatch这三个部件不是可选项是寄存器模型前门访问的必经之路。map负责“去哪”adapter负责“怎么说”predictor负责“记住看到什么”。后门访问不走map和adapter这个后面单独讲。有人可能会问为什么寄存器模型不直接和driver相连非要绕一道adapter因为UVM寄存器模型是协议无关的它定义了一套读写寄存器的通用描述结构uvm_reg_bus_op里面无非是地址、数据、读写类型、状态这几个字段。而实际项目里的总线协议五花八门APB、AHB、AXI、自定义协议时序和事务格式完全不同。adapter就是中间那个翻译官把通用请求翻译成总线能听懂的话再把总线返回的结果翻译回通用结构。理解了这一层你就知道为什么adapter里两个函数是必须成对实现的。2. sequence与reg_model的对接一个write()的前世今生2.1 在sequence里调用write/read的标准姿势使用寄存器模型的第一件事是在sequence里拿到reg_model的句柄。常见的做法是在build_phase里通过config_db把reg_model传下去然后在sequence的body里get出来class ral_seq extends uvm_sequence #(uvm_sequence_item); uvm_object_utils(ral_seq) my_regmodel reg_model; function new(string name ral_seq); super.new(name); endfunction task body(); uvm_status_e status; uvm_reg_data_t value; if (!uvm_config_db#(my_regmodel)::get(this, , reg_model, reg_model)) uvm_fatal(RAL_SEQ, reg_model not found!) reg_model.cfg_reg.write(status, 32h1F, .parent(this)); reg_model.cfg_reg.read(status, value, .parent(this)); if (status ! UVM_IS_OK) uvm_error(RAL_SEQ, $sformatf(cfg_reg access failed, status%s, status.name())) endtask endclass注意最后那个.parent(this)有些初学阶段图省事会省略这个参数。省略之后寄存器操作会挂在默认的sequencer下面轻则影响phase的自动结束重则在你同时有多个sequencer时找不到正确的目标sequencer。我建议任何前门读写都显式传parent这算是个习惯问题。调用reg_model.cfg_reg.write之后背后发生的事一串串的。简化描述如下write任务创建一个uvm_reg_item存下读写类型、要写的值、目标寄存器等信息。通过所在uvm_reg_block的default_map计算出这个寄存器的物理地址。调用adapter的reg2bus把标准读写描述转成总线transaction。把这个transaction发给default_map上配置的sequencer然后由sequencer仲裁交给driver。driver执行完真正总线操作后通过item_done返回rsp。adapter的bus2reg把rsp转回uvm_reg_bus_op结构。如果predict开关打开寄存器模型会根据这次操作结果更新mirrored value。有了这条链路你才能理解为什么前门访问必须把sequencer和adapter都配置好。少了任何一环write只是在软件层面“假装写了一下”DUT里该没变还是没变。2.2 adapter里的reg2bus和bus2reg为什么不能偷懒以一个APB总线为例一个最小可用的adapter长这样class apb_reg_adapter extends uvm_reg_adapter; uvm_object_utils(apb_reg_adapter) function new(string name apb_reg_adapter); super.new(name); supports_byte_enable 0; provides_responses 1; endfunction function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw); apb_transfer tr apb_transfer::type_id::create(tr); tr.addr rw.addr; tr.kind (rw.kind UVM_READ) ? APB_READ : APB_WRITE; tr.data rw.data; return tr; endfunction function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw); apb_transfer tr; if (!$cast(tr, bus_item)) uvm_fatal(ADAPTER, bus_item is not apb_transfer!) rw.kind (tr.kind APB_READ) ? UVM_READ : UVM_WRITE; rw.addr tr.addr; rw.data tr.data; rw.status UVM_IS_OK; endfunction endclass两个成员变量要特别说清楚。supports_byte_enable表示总线是否支持字节使能。如果你访问的寄存器按字节使能拆分而寄存器模型里的field又不是整数个字节对齐这个位不设对读写结果很容易出现“高字节被吞”之类的诡异问题。最直接的表现是单看transaction里数据是对的但DUT里寄存器值怎么都不对。provides_responses表示总线协议是否有独立的response返回。对于有握手完成信号的总线我一般置1因为我的driver会在item_done时填好rsp让sequence去取。如果这里置0寄存器模型不会等待responsebus2reg被调用的时机就会变得很微妙后面第4节会详细讲这个问题。再强调一点bus2reg里一定要做类型检查用$cast的返回值判断而不是盲目转换。否则一旦monitor或者其它路径送进来一个非预期类型的事务你会得到一个空指针引用的fatal并且报错位置离真正出问题的地方十万八千里。我见过一个环境就是因为bus2reg没检查类型最后定位到问题是其它组件把错误的对象连到了adapter端口上那个排查过程真是一言难尽。3. 镜像值机制从desired到mirrored再回到DUT3.1 三种值到底存的是什么寄存器模型里经常出现desired value、mirrored value这两个术语。用最直白的话说desired value你想让寄存器变成的值可以理解为“期望值”或“设置目标”。mirrored value寄存器模型认为DUT寄存器此刻的值本质是一个软件缓存。DUT内部值硬件寄存器里真正的值只有通过总线读或后门读才能精确知道。再打个比方。你往一个远程服务器提交配置desired是你本机编辑器里改好的那份配置mirrored是你本地缓存里记录的“服务器当前配置”而服务器硬盘上真正生效的配置是实际值。如果服务器上的配置被别人改过而没人通知你本地缓存就过期了。寄存器模型的predict机制就是用来尽量减少这种过期窗口的。不同操作对这几种值的影响是面试和实际调试验证环境时经常被问到的内容。常见的操作可以整理成下面这张表操作desiredmirroredDUT典型使用场景set()更新不变不变批量准备配置后续配合update下发write(value)视设计意图predict成功后更新更新立即写某个寄存器read()不变predict成功后更新不变回读确认或获取状态update()不变更新更新为desired把一批set好的配置统一下发mirror()不变更新不变读回并与mirror值比对poke()不变更新更新后门快速写peek()不变更新不变后门快速读关于write对desired的影响我特意打了个“视设计意图”因为实际工程里很多人的用法并不一致。如果你后面还想用update做批量恢复最稳妥的方式是先用set把期望值准备好最后调update。把write当成唯一的配置入口同时又依赖update去做二次下发很容易出现desired值不是自己预期值的坑。我建议你把desired value理解成set和update这一条链路上的概念write和read主要影响mirrored value。3.2 auto_predict和predictor选错了一样会“灵异”predict是更新mirrored value的核心动作。UVM里打开predict有两种方式自动predict和显式predict。自动predict最简单在env的connect_phase里对default_map调用一句reg_model.default_map.set_auto_predict(1);寄存器的每次前门读写操作完成后寄存器模型会自动调一次predict把结果同步到mirrored value。优点是实现简单不用额外连predictor缺点是它只能感知通过寄存器模型自己发起的操作。如果DUT内部有模块直接改了寄存器比如DMA搬运完成把done位置1或者中断控制器硬件清了状态位寄存器模型是完全不知道的。显式predict需要单独实例化uvm_reg_predictor并且把总线monitor采集到的事务送进去代码大概是这样的class my_predictor extends uvm_reg_predictor #(apb_transfer); uvm_component_utils(my_predictor) function new(string name, uvm_component parent); super.new(name, parent); endfunction endclass // 在env里创建并连接 my_predictor reg_predictor; reg_predictor my_predictor::type_id::create(reg_predictor, this); reg_predictor.map reg_model.default_map; reg_predictor.bus_in.connect(apb_agent.monitor.item_collected_port); reg_model.default_map.set_predictor(reg_predictor);这样总线上任何一笔真实读写事务被monitor抓到后都会经过predictor通知寄存器模型更新镜像值。哪怕其它master、其它sequence甚至DUT内部逻辑通过总线发起的访问只要monitor能看到镜像值就能跟上。实际项目中只要存在多个总线master或者寄存器存在硬件自动修改的场景我都建议用显式predict。反过来环境很简单、所有寄存器访问都由验证环境自己发起auto_predict完全够用。有一点必须注意auto_predict和显式predict不要同时开否则同一次总线操作会被predict两次第一次结果可能很快被第二次覆盖虽然大多数寄存器值相同覆盖看不出来但对某些状态类寄存器重复predict可能造成镜像值临时错乱调试起来非常迷惑。3.3 mirror、update、poke、peek的实际使用场景镜像值机制不是学完就完了工程里几个常用操作组合起来能省不少事。先说update。假设你要初始化一个外设涉及几十个寄存器常规做法是一条条write每一条都要经历完整的sequence握手仿真时间长代码也啰嗦。我习惯先把这些配置值通过set()写进各寄存器的desired value然后统一调用update()让寄存器模型只把发生变化的那部分寄存器给写进DUT。UVM的update还会对比desired和mirror如果一样就跳过减少冗余总线访问。这个特性在复位测试和低功耗用例里特别有用。再说mirror。测试最后检查关键寄存器有没有被硬件意外改掉最直接的办法是reg_model.status_reg.mirror(status, UVM_CHECK, .parent(this));这句话会发起一次读操作读回的值如果和mirrored value不一致自动报mismatch。它比手动read再比对省掉了你自己写if判断的麻烦而且错误信息里会带上寄存器路径、期望值、实际值定位起来很方便。poke和peek是后门访问的入口走的是uvm_hdl_path不经过总线。它们常用来在仿真里快速灌初值比如开机后想给某个计数器清零用poke一下就搞定不需要等待总线握手。peek则用来快速读取DUT当前值。这两个操作默认也会更新mirrored value使用时要留意。另外后门访问要求寄存器在reg_block里正确配置HDL路径否则调用时会报“no HDL path”的错误。4. driver不回response为什么只能发有限的几个包就卡住4.1 从现场说起有一回我在环境里写新用例发现一个奇怪现象同一个APB agent直接用uvm_do_on发原始apb_transfer的sequence可以连续发很多笔没有任何问题。但一旦换成寄存器模型的sequence跑着跑着就卡住了波形上看最后一笔总线操作其实已经完成但仿真时间不再往前走。后来加了打印发现问题出在driver的回包动作上。我的driver在最开始写的时候为了省事执行完总线操作只调了item_done()没有显式传rsp。直接用uvm_do_on发原始item时sequence不依赖rsp内容所以感觉不到问题。可寄存器模型的前门访问不一样它必须拿到rsp才能继续走bus2reg和predict的流程。再往后排查发现环境里另一个同事的sequence虽然不关心rsp但也在同一个sequencer上跑他从不调用get_response去清理队列。结果就是response队列越积越多跑到一定数量后整个sequencer的调度就卡住了。他那边现象是“只能发八个包”我这边是“跑到第8个寄存器操作开始挂起”。这个数字不是绝对的取决于UVM版本、response队列深度和当前环境中挂了多少个sequence但根因都一样rsp链路断了。4.2 为什么前门访问必须依赖response先简单回顾一下UVM的sequence机制。一个完整的事务交互包括两个方向sequence通过send_request把item发给sequencerdriver拿到后执行执行完调用item_done把rsp传回sequence的response队列sequence再通过get_response把rsp取出来。对于前门访问寄存器模型内部正是用这套握手机制完成一次总线读写的。如果driver只调item_done不填rsp在UVM底层sequence端仍然会收到一个默认生成的response item看起来像是“回了”但如果这个response里没有携带正确的数据或者根本没有被deliver到正确的队列reg_sequence就会一直等不到预期的rsp。更典型的情况是driver连item_done都没调用或者adapter的provides_responses配错sequence就一直阻塞在get_response上。所以排查这类问题顺序很重要。我一般会先确认driver是不是在item_done里把rsp完整填好再看adapter的provides_responses是否和实际总线行为一致。如果总线协议有独立response置1如果协议确实没有response概念置0。两边不匹配是配置错误的重灾区。还有一点如果你自己写原始sequence时根本不用rsp也一定要在sequence里主动调用get_response(rsp)把队列清掉。UVM不会因为你“不关心”就自动丢弃response队列里的item会一直攒着攒到一定数量就报错或者阻塞。之前那个“只能发八个包”的现场本质就是队列积压到临界点的表现。我自己现在写driver的习惯是任何总线事务执行完都先构造好一个完整的rsp再调用item_done(rsp)无论上层用不用都保证这条链路是通的。这样寄存器模型和普通sequence都能正常工作排查问题时也不会因为rsp缺失引入额外的干扰项。5. 常见问题与排查速查表5.1 高频错误速查表把这段时间踩过的坑和周围同事遇到的问题整理成一张速查表遇到类似现象可以直接对着查现象可能原因排查方向write之后DUT寄存器还是旧值adapter的reg2bus里addr或data没赋值set_sequencer没配前门访问没建好打印reg2bus输出确认transaction内容读写操作卡死不返回driver没有回rspprovides_responses配置和driver行为不匹配看driver的item_done检查rsp队列mirrored value始终不更新auto_predict关了predictor没连或bus_in连接错误临时打开auto_predict做对照实验read回读值总是0但波形上总线读到了非0值adapter的bus2reg里data方向弄反monitor采集的事务类型不对在bus2reg打印读到的数据update不生效desired value没设置或update前没有调用set打印update前后desired和mirrored报“no HDL path”错误后门访问没有配置uvm_hdl_pathreg_model里补全hdl_path多个sequence同时访问寄存器出现乱序没有统一通过寄存器模型访问多个sequence直接发总线item统一走reg_model避免绕过这张表看着简单但每一条都是我至少花了一下午才定位到的。尤其是前两条一个是看不见的数据错一个是环境直接卡死都很折磨人。5.2 调试三板斧第一板斧在adapter里加打印。reg2bus和bus2reg两个函数是前门访问的必经之路在里面打印地址、数据、读写类型、返回status能直接看到寄存器模型眼中的每一次总线操作长什么样。很多“DUT寄存器没变”的问题打印完reg2bus就真相大白了。第二板斧时常输出寄存器模型状态。在env的check_phase或者测试收尾时调用一次reg_model.print()会把所有寄存器的desired value和mirrored value都打出来。对比日志中DUT实际读到的值能很快判断问题出在配置链路还是预测链路。第三板斧做对照实验。怀疑predict链路有问题时先在default_map上临时打开set_auto_predict(1)同时断开predictor。如果镜像值立刻恢复正常说明问题在explicit predictor这一路重点查monitor连接和predictor的bus_in如果问题依旧重点查adapter的转换逻辑而不是继续在predictor上浪费时间。还有一个心法遇到“寄存器模型和DUT不一致”的报错不要一上来就怀疑是DUT的bug。按出问题的概率排序差不多是adapter转换出错、predictor没接好、map地址配置错、最后才是DUT真的被意外改写。先按这个顺序排查效率会高很多。我个人在实际操作中最深刻的体会是寄存器模型不是一个简单的读写工具它是一套让验证环境的“意图”和DUT的“现实”保持同步的机制。想把这套机制用好adapter、predict、response这三座山绕不开。我最初照着别人的代码抄能跑通流程直到遇到mirror对不上、环境卡死这类问题才逼着自己回去把这些内部细节一遍遍捋清楚。最后再分享一个小技巧调试阶段可以临时建一份“不参与比对”的寄存器黑名单把中断清除、硬件自清零这类不适合predict的寄存器从自动比对里摘出来等回归稳定之后再逐个放回去能少很多干扰。下一篇文章我准备写寄存器模型的后门访问和ralgen自动生成那又是一片新的深水区到时候再继续整理笔记。
返回列表