[UVM源代码研究] 当我们使用寄存器模型里的寄存器调用write/read方法,数据包是如何在寄存器模型、adapter、sequencer中传递的
要想通过一篇文章就把register model的所有概念都讲清楚显然不切实际,咱们这边仅仅从一个小的方面进行切入,主要研究下我们实际的sequence_item在register model、adapter以及sequencer之间是如何调度传播的,这里选择的切入点就是使用寄存器模型里的寄存器调用write/read方法前门访问时数据包是如何在寄存器模型、adapter、sequencer中传递的,我们自然就联想到了adapter内的两个函数reg2bus和bus2reg。
要实现register model与adapter和sequencer的关联我们通常在寄存器模型所在组件的build_phase执行下面这段代码
也就是说register model中的default_map里的set_sequencer方法实现了将三者的关联,传递的两个参数分别对应着实际的sequencer和adapter。
我们看看这个default_map的声明和类型定义
于是寄存器模型的default_map就把对应的sequencer和adapter存放在了default_map中的m_sequencer和m_adapter中。
下面在看看uvm_reg_adapter中reg2bus和bus2reg原型的定义
我们实际写的adapter中都会对这两个函数做override,所以里面执行的具体内容其实并不重要,只是给我们自己写的函数提供了个参考的思路,真正对我们有意义的时这两个函数的原型(返回值、参数类型),它约束了我们override的函数必须与之保持一致。
下面我们进入本文讨论的精华部分,reg2bus和bus2reg这两个函数何时何地被uvm源代码调用。
我们这里就不通过grep来寻找调用的文件了,我们选择顺藤摸瓜,例如我们知道调用寄存器模型里某个寄存器的write方法是可以实现reg2bus的行为的,那么就从这个write入手来摸出来reg2bus在哪里调用的,这样我们队reg2bus的数据流向就会有个比较清晰的认识。
uvm_reg的write
我们先看看uvm_reg中write任务内容
write操作主要是上图中1-4步骤,中间部分就是给write的参数赋值给新创建的一个uvm_reg_item(与接口协议无关的寄存器模型中寄存器操作的transaction级的抽象,是一个class),包含了某次寄存器操作的相关信息。这里我们需要提到另一个与之类似的概念叫uvm_reg_bus_op,也就是我们在reg2bus和bus2reg函数参数列表里表征寄存器模型里传播的transaction类型,它是一个struct类型。两者都定义在文件uvm_reg_item.svh中,看下两者的描述大家就清楚他们的区别和使用场合了。
也就是说我们寄存器的一次操作(对应uvm_reg_item)有可能需要多次总线行为(对应uvm_reg_op),当我们寄存器模型寄存器位宽(位宽在寄存器模型中指定,最大值由宏`UVM_REG_DATA_WIDTH约束,该宏默认是64,当总线位宽超过64时就需要修改该宏的值)与总线位宽(对应上面struct中的变量n_bits)想等时,两者表示同一意义。
接着回到我们上面讨论的write任务,XatomicX方法定义如下
其中m_process是全局声明的process,m_atomic是全局semaphore,这个Xatomic(1)就可以简单理解为等待访问寄存器的进程(process)授权(semaphore)。Xatomic(0)则是结束归还进程授权。两者中间就执行寄存器write的主体部分。
set()函数的描述和定义如下,本质上只是更新寄存器在寄存器模型中的期望值。
源代码中2194-2379这接近两百行的代码都是完成的do_write的内容,也就是write任务的主体部分,我们这里就不全部截图了,只选择关键的几个部分来做出解释
这段case语句执行的就是do_write的主体部分,分成了UVM_BACKDOOR和UVM_FRONTDOOR两种分支,我么知道UVM_BACKDOOR是通过hdl_path的方式去操作dut的并不会经过physical interface,我们这篇文章主要讨论的是reg2bus和bus2reg两条数据流的问题,UVM_BACKDOOR留给以后的议题再讨论,这里把主要研究精力放在UVM_FRONTDOOR上。
2299行我们先看看uvm_reg_item中关于local_map的描述
也就是说uvm_reg_item中的这个local_map可以通过获取root map的方式间接拿到sequencer和adapter的句柄,那么这个local_map的赋值是什么时候执行的呢?它又是怎么通过get_root_map来获取root map的呢?
没有办法,这里不可避免地又需要截取do_write开始的部分代码(虽然我们在竭力浓缩那100多行的代码)
而我们的rw里的local_map就是Xcheck_accessX() 这个函数里赋值的
可以看到这段代码是包含在了非UVM_BACKDOOR的场景下的,我们继续看get_local_map的定义
通常我们执行到1652就会返回我们在register model中设置的default_map,至于其他分支乃至get_default_map里具体的实现原理,我们这里就不做展开分析了,大家可以认为,此时我们rw里local_map已经获取到了我们的default_map了,在不涉及到多层次的map的情况下,我们的get_root_map拿到的就是我们设置的default_map。
而一开始我们就知道我们sequencer和adapter都是通过default_map.set_sequencer的方式在default_map中用m_sequencer和m_adapter获取了,因而这里我们在do_write中就可以拿到相应的sequencer和adapter的句柄。
2303-2317这段代码分成了user frontdoor和built-in frontdoor两种,我们需要看下map_info的定义
其内容也是在Xcheck_accessX()调用时获取的
而这个m_regs_info[]的数组是在reg_map的add_reg时赋值的
包含了该uvm_reg相关的配置信息,我们看看通常我们生成的寄存器模型都是咱们add_reg的
可以看到我们一般都是缺省最后一个frontdoor参数的,所以一般都是null,即用户不会自己定义自己的frontdoor行为,而是使用内置的frontdoor行为,所以uvm_reg里的do_write()任务执行主体(2315行)又转化为了调用uvm_reg_map里的do_write()
核心代码就是2000行的do_bus_write(),对应着2051-2184行,这个函数参数就包含了uvm_reg_item、sequencer和adapter,也就是一次register写操作所需的所有参数,而我们在adapter中override的reg2bus函数就是再2130行被调用的,具体的代码我们就不去细细分析了。
这里我们还有一个疑问,我们调用的reg2bus所产生的transfer是怎么交给sequencer乃至发给driver的呢?那就得看do_bus_write()里的这段核心代码了
第一段红色框产生了要发送给sequencer的包,第二段通过调用uvm_reg_item里的parent(uvm_sequence_base类型,adapter.parent_sequence是否为null分为1978-1986和1988-1991两种赋值途径)这个sequence里的start_item把包发给了sequencer,同伙finish_item()还可以将返回的参数通过bus_req带出来,再通过调用bus2reg反馈给寄存器模型
以上就完整的介绍了在寄存器模型对寄存器的的write操作时,整个数据流是如何传播的,涉及到的相关的组件都是如何相互协调工作的。下面我们再看看read行为。
uvm_reg的read
我们这里还是只关注UVM_FRONTDOOR时的数据流
我们就快速过一下相关的文件方法,与write类似的分析我们就不去仔细讨论了,重要内容我们用红色框圈出
总结
至此,咱们关于uvm_reg的write和read的UVM_FRONTDOOR时,数据流是如何在寄存器模型和adapter、sequencer中运转的过程相关的源代码就分析到这儿了,其中不乏对一些复杂流程的简化分析,寄存器模型相关的源代码是一个复杂的工程,我们这里仅仅是窥见了其冰山一角,让我们对一直在用的bus2reg、reg2bus乃至寄存器uvm_reg的write/read具体的工作有了些稍微具体的认识,关于寄存器模型更多的知识,我们还会在后面开更多的话题来展开讨论,希望用更多的冰山一角让我们对寄存器模型的轮廓有更加清晰的认识。