1 寄存器模型概览
了解硬件的读者都知道寄存器是模块之间互相交谈的窗口。 一方面, 通过读出寄存器的状态可以获取硬件的当前状况, 另一方面, 通过对寄存器进行配置, 可以使寄存器工作在一 定模式下。 在验证过程中, 寄存器的验证排在验证清单的前列, 因为只有首先保证寄存器的功能正确, 才能使硬件与硬件之间的交谈是 “语义一致" 的。 如果寄存器配置结果与寄存器配置内容不同, 那么硬件无法工作在预期的模式下, 寄存器可能无法正确反映硬件的状态。
本节我们关于 UVM 寄存器模型的介绍, 将设计 MCDF 中的寄存器模块简化出来, 通过硬件的寄存器模型和总线 UVC 建立一个小的验证环境:
• 寄存器有关的设计流程。
• 寄存器模型的相关类。
• 如何将寄存器模型集成到现有环境, 与总线 UVC 桥接, 与 DUT 模型绑定。
• 寄存器模型的常用方法和预定义的 sequence。
• 寄存器测试和功能覆盖率的实际用例。
处理器可以配置硬件中各功能模块的功能和访问状态, 而与处理器的对话就是通过寄存器的读写实现的。 寄存器的硬件实现是通过触发器, 每一个比特位的触发器都对应着寄存器的功能描述 (function specification)。 寄存器一般由 32 个比特位构成, 单个寄存器可分为多个域 (field), 不同的域往往代表一项独立的功能。 单个的域可能由多个比特位构成, 也可能由单一比特位构成, 这取决于该域的功能模式可配置的数量。 对外部的读写而言, 不同的域大致可以分为 WO (Write-Only, 只写), RO(Read-Only, 只读)和RW (Read and Write, 读写)。 除了这些常见的操作属性, 还有一些特殊行为 (quirky)的寄存器, 如读后擦除模式(Clean-on-Read, RC)和只写一次模式(Write-one-to-Set, WlS)。
一般的, 一个寄存器有32位宽,寄存器按照地址索引的关系是按字对齐的(word-align),上图中的寄存器有多个域,每个域的属性也可以不相同,reserved 域表示的是,该域包含的比特位暂时保留以作为日后功能的扩展使用,而对保留域的读写不起任何作用,即无法写入,且读出值是它的复位值。上面这些寄存器按照地址排列即构成寄存器列表,我们称之为寄存器块(register block)。实际上,寄存器块除了包含寄存器,也可以包含存储器,因为它们的属性都近乎于读写功能,以及表示为与外界通信的接口。如果将这些寄存器有机组合在一起,那么按照上面寄存器的地址描述,如下图所示,MCDF 的寄存器功能模块即可由这样一个register block 来表示。
在验证 MCDF 寄存器模块的过程中,首先要理清寄存器相关的概念,即一个寄存器由多个域构成,而单个域包含多个比特位; 一个功能模块中的多个寄存器组团构成一个寄存器模型(register model)。除了DUT中的寄存器模块(由硬件实现),还有属于验证环境的寄存器模型。这两个模块包含的寄存器信息是高度一致的,属千验证环境的寄存器模型也可以抽象出层次化的寄存器列表,该列表包含的地址、域、属性等信息与硬件一侧的寄存器内容一致。而由软件来建立的寄存器模型对软件开发和功能验证都有帮助。对于功能验证而言,可以将总线访问寄存器的方式抽象为寄存器模型访问的方式,这种方式使得寄存器后期的地址修改 (例如基地址更改)或域的添加都不会对已有的激励构成影响,从而提高已有测试序列的复用性。
那么,通过软件建立寄存器模型的方法如何保证与硬件寄存器的内容属性保持一致呢?这离不开一份中心化管理的寄存器描述文件,很多公司目前在使用 XML 格式的寄存器描述文件, 也有一些公司在使用Excel (CSV)或DOC等格式来保存寄存器的描述。 为什么寄存器描述应该被中心化管理呢?如下图所示,这种管理也被称之为单一源方式管理,与之相似的是在设计验证流程中, 设计人员与验证人员都应该将功能描述文档作为唯一的功能实现和测试方案的参考。 这两者之间相同的地方是都采用了单一源的管理方式来尽量降低出现分歧和错误的可能, 只不过与功能描述文档不同的是, 寄存器描述文档使用了更加结构化的文档描述方式, 这也解释了为什么可以通过XML或Excel (CSV)等数据结构化的方式来实现寄存器的功能描述。
通过数据结构化的存储方式, 可以在硬件和软件开发过程中以不同方式使用寄存器描述文档:
• 系统工程师撰写并维护寄存器描述文件,而后归置到中心化存储路径供其他工程师开发使用。
• 硬件工程师利用寄存器描述文件生成寄存器硬件模块(包含各个寄存器的硬件实现和总线访问模块)。
• 验证工程师利用寄存器描述文件生成UVM寄存器模型, 供验证过程中的激励使用、寄存器测试和功能覆盖率收集。
• 软件工程师利用该文件生成用于软件开发的寄存器配置的头文件 (header file), 从而提高软件开发的可维护性。
• 寄存器描述文件也可以用来生成文档, 实现更好的可读性。
验证工程师需要的UVM寄存器模型, 既可以手写, 也可以由脚本实现转换。 笔者推荐读者找到适合自己的自动转换的流程, 原因很简单, 手动转换有潜在的错误, 寄存器越多出现错误的可能性越大, 这会使后期调试的难度更大, 因为验证师首先需要定位寄存器模型的错误是来自于转换过程还是来自硬件。 除此之外, 推荐使用寄存器生成器(脚本) 的原因还包括:
• 一个广义的寄存器生成器(register generator), 应该依据统一格式的寄存器描述文件,生成UVM寄存器模型(为验证) 或硬件寄存器模块(被集成到设计中), 或生成头件(C语言) 用于开发软件等。
• 一个稳定的寄存器不但可以保证从文本信息到寄存器模型的无错误转换,还可以在转换过程中通过语义检查发现寄存器描述文件违规的情况,从而帮助修正寄存器描述文件内容。
• 如果寄存器描述文件内容有更新, 寄存器生成器可以再次生成需要的相关文件格式,这对于流程化作业非常方便。
• 对于验证所需的寄存器模型而言, 一个更有效的做法是, 封装已有的寄存器生成器, 使得可以通过指定多个寄存器模块和其对应基地址 (base address) 生成一个层次化的 top register block, 包含多个 child register block。这种方式可以将更大的子系统级或系统级的寄存器模型归纳在一起,便于系统化操作管理。
在构建UVM寄存器模型的过程中, 读者需要了解下表中与模型构建相关的类及其功能。
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg)
uvm_reg_field reserved;
rand uvm_reg_field pkt_len;
rand uvm_reg_field prio_level;
rand uvm_reg_field chnl_en;
function new(string name= "ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function build();
reserved = uvm_reg_field::type_id::create("reserved");
pkt_len = uvm_reg_field::type_id::create ("pkt_len");
prio_level = uvm_reg_field::type_id::create ("prio_level");
chnl_en = uvm_reg_field::type_id::create ("chnl_en");
reserved.configure(this, 26, 6, "RO", 0, 26'h0, 1, 0, 0);
pkt_len.configure(this, 3, 3, "RW", 0, 3'h0, 1, 1, 0);
prio level.configure(this, 2, 1, "RW", 0, 2'h3, 1, 1, 0);
chnl en.configure(this, 1, 0, "RW", 0, 1'h0, 1, 1, 0);
endfunction
endclass
class stat_reg extends uvm_reg;
`uvm_object_utils(stat_reg)
uvm_reg_field reserved;
rand uvm_reg_field fifo_avail;
function new(string name = "stat_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function build () ;
reserved = uvm_reg_field:: type_id:: create ("reserved");
fifo_avail = uvm_reg_field::type_id::create ("fifo_avail");
reserved.configure(this, 24, 8, "RO", 0, 24'h0, 1, 0, 0);
fifo_avail.configure(this, 8, 0, "RO", 0, 8'h0, 1, 1, 0);
endfunction
endclass
class mcdf_rgm extends uvm_reg_block;
`uvm_object_utils(mcdf_rgm)
rand ctrl_reg chnl0_ctrl_reg;
rand ctrl reg chnl1_crrl_reg;
rand ctrl_reg chnl2_ctrl_reg;
rand stat_reg chnl0_stat_reg;
rand stat_reg chnll_stat_reg;
rand stat_reg chnl2_stat_reg;
uvm_reg_map map;
function new(string name = "mcdf_rgm");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function build();
chnl0_ctrl_reg = ctrl_reg::type_id::create("chnl0_ctrl_reg");
chnlO_ctrl_reg.configure(this);
chnl0 ctrl_reg.build();
chnll ctrl_reg = ctrl reg::type_id::create("chnll_ctrl_reg") ;
chnll_ctrl_reg.configure(this);
chnll ctrl_reg. build() ;
chnl2 ctrl_reg = ctrl reg::type_id::create("chnl2_ctrl_reg");
chnl2_ctrl_reg.configure(this);
chnl2_ctrl_reg. build();
chnlO_stat_reg = stat_reg::type_id::create ("chnl0_stat_reg");
chnlO_stat_reg.configure(this);
chnlO_stat_reg.build();
chnll_stat_reg = stat_reg::type_id::create("chnll_stat_reg");
chnll_stat_reg.configure(this);
chnll_stat_reg .build();
chnl2_stat_reg = stat_reg::type_id::create("chnl2_stat_reg");
chnl2_stat_reg.configure(this);
chnl2_stat_reg.build();
// map name, offset, number of bytes, endianess
map= create_map("map",'h0, 4, UVM_LITTLE_ENDIAN);
//h0为base addr,4为offset addr,完整的配置base+offset
map.add_reg(chnl0_ctrl_reg, 32'h00000000, "RW");
map.add_reg(chnll_ctrl_reg, 32'h00000004, "RW");
map.add_reg(chnl2_ctrl_reg, 32'h00000008, "RW");
map.add_reg(chnl0_stat_reg, 32'h00000010, "RO");
map.add_reg(chnll_stat_reg, 32'h00000014, "RO");
map.add_reg(chnl2_stat_reg, 32'h00000018, "RO");
lock_model ();
//锁住reg,
endfunction
endclass
从上面的定义中, 可以整理出关于寄存器建模的基本要点和顺序:
• 在定义单个寄存器时, 需要将寄存器的各个域整理出来, 在创建之后还应当通过uvm _reg_ field: :configure()函数来进一步配置各自属性。 考虑到 uvm_ reg_ field: :configure 函数自身的参数较多, 且都要求指定出来, 读者需要保证参数的一一对应。
• 在定义 uvm_reg_ block 时, 读者需要注意 reg_block 与 uvm_mem、uvm_reg 以及 uvm _reg _ map 的包含关系。首先 uvm_reg 和 uvm_mem 分别对应着硬件中独立的寄存或存储, 而一个 uvm_reg_block 可以用来模拟 一个功能模块的寄存器模型, 其中可以容纳多个uvm_reg 和 uvm_mem 实例;其次 map 的作用一方面用来表示寄存器和存储对应的偏移地址, 同时由于一个 reg_block 可以包含多个 map, 各个 map 可以分别对应不同总线或不同地址段。 在 reg_block 中创建了各个 uvm_reg 之后, 需要调用 uvm_reg::configure()去配置各个 uvm_reg 实例的属性。
• 考虑到 uvm_reg_ map 也会在 uvm_reg_ block 中例化, 在例化之后需要通过 uvm_reg_ map: :add _reg()函数来添加各个 uvm_reg 对应的偏移地址和访问属性等。 只有规定了这些属性, 才可以在稍后的前门访问 (frontdoor) 中给出正确的地址。
• uvm_reg_block 可以对更大的系统做寄存器建模, 这意味着 uvm_reg_ block 之间也可以存在层次关系,上层uvm_reg_ block 的 uvm_reg_map可以添加子一级uvm_reg_ block的uvm_reg_ map, 来构建更全局的“版图”,继而通过 uvm_reg_block与uvm_ reg_ map之间的层次关系来构建更系统的寄存器模型。
验证过程中的不同角色,参与上述流程的不同部分:
• 系统工程师需要提供寄存器描述文件。
• 模块验证人员需要生成寄存器模型。
• VIP 开发人员需要提供总线适配器。
• TB 构建人员(与模块验证人员有时不是同一个人)需要集成寄存器模型。• 模块验证人员还需要完成后续的寄存器模型检查和功能覆盖率收集。