导师教我的UVM谎言:基本激励篇

SNUG 2015 2015 16 页

导师教我的UVM谎言:基本激励篇

作者: Justin Refice (NVIDIA Corporation) 会议: SNUG 2015 页数: 16 源文件: SNUG_TPC_UVM_Poppen_Lies_Teacher_Told_About_The_UVM_Basic_Stimulus_paper.pdf


Page 1

Figure

导师教我的UVM谎言:基本激励篇

Justin Refice NVIDIA Corporation Santa Clara, USA www.nvidia.com

摘要

许多UVM 通用验证方法学用户购买书籍和其他培训材料,其中提供了关于如何搭建第一个测试平台的简单描述。不幸的是,这些材料往往过度简化,提供了易于理解的答案,但这些答案可扩展性差、复用潜力小,并且经常遗漏重要功能。

本文将展示常见的激励观念在何处失效,以及简化示例中的陷阱在哪里。此外,还将提出一种更好的方法,借鉴网络模型中的熟悉概念,从对底层的sequence item的新视角开始。


Page 2

Figure

目录

1. 引言 2. 那看起来有点眼熟... 3. 通信层次 4. UVM Sequencer传输层 5. UVM Sequence/Driver应用层 6. 混淆应用层和传输层 7. 你在跟我说话吗? 8. 实战演练 9. 为什么不...? 10. 结论 11. 参考文献

图表目录

图1 — UVM Sequence/Driver vs 客户端/服务器协议 图2 — TCP/IP数据流 图3 — UVM Sequence/Driver数据流 图4 — UVM Sequencer传输层协议

表格目录

表1 — Sequence Item端口方法


Page 3

Figure

1. 引言

UVM 通用验证方法学 [1]为测试平台主动域中的激励创建提供了基本结构:

- Sequence Items:总线上事务的抽象表示 - Drivers:负责处理/执行sequence items - Sequences:负责生成sequence items流并将其提供给driver - Sequencers:负责仲裁哪个sequence可以向driver发送下一个item

虽然对于每种构造的用途存在普遍共识,但这些构造的使用方式因公司而异,甚至在同一公司内的不同团队之间也存在巨大差异,往往以不幸地不兼容的方式呈现。考虑到如此大的差异,人们开始质疑在此语境中的"通用(Universal)"的定义。

说明这些结构的通用化形式比大多数入门教程愿意深入的要复杂得多。这意味着我们最终得到了大量培训材料,呈现了一个简化的常见示例,虽然这个示例乍看起来容易理解,但在现实世界中很快就失效了。

本文提出了一种稳健的通用方法,利用TCP/IP通信模型的封装特性,使其能够良好扩展、轻松适应,并为VIP 验证IP开发人员和终端用户提供一种功能齐全、一致且熟悉的激励生成模式。换句话说:一种通用的激励生成模式。

2. 那看起来有点眼熟...

泛化我们激励流程的第一步是重新评估sequence和driver之间的交互。

Sequences本质上是"瞬态的",意味着sequence可以在仿真期间的任何时候被创建,也可以同样轻易地被销毁。而driver本质上是"静态的",也就是说一旦driver被构造出来,它将在整个仿真过程中持续存在。Driver不断等待sequences发起新的请求,然后这些请求被确认、服务,并可能发送响应。

对于那些熟悉网络通信的开发人员来说,这种模式应该非常熟悉,因为它是经典的客户端-服务器通信模型。在该模型中,客户端负责向不断等待新请求的服务器发起新的会话。该模型的一个常见现实世界示例就是万维网(World Wide Web),每个网页浏览器充当客户端,向网络服务器发起新的请求。事实上,互联网上使用的大多数常见协议(Web、文件传输、电子邮件等)都是基于客户端-服务器模型的。


Page 4

Figure

通过将UVM激励生成映射到经典网络模型,我们可以开始构建一个能够处理所有形式激励的基础:

图1 — UVM Sequence/Driver vs 客户端/服务器协议

图中故意省略了UVM Sequencer,这是因为在sequence和driver的客户端/服务器视角中,它根本不存在。

3. 通信层次

在实现任何网络通信(包括UVM接口VIP 验证IP)时,需要记住的一个基本细节是将"什么"正在通信与"如何"通信分离开来。

在计算机网络中,互联网协议套件(通常称为"TCP/IP")是描述这些概念之间差异的常见机制。该模型由4层构成:应用层、传输层、互联网层和链路层[2]。每个后续层都能够完全封装前一层,提供清晰的服务抽象。通过清晰分离这些层,一个典型用户可以在不知道其数据包是通过以太网、Wi-Fi还是信鸽路由的情况下浏览网页……这些信息被TCP/IP隐藏了。


Page 5

Figure

下图说明了TCP/IP提供的封装和分层[3]:

图2 — TCP/IP数据流

在TCP/IP协议栈中,客户端和服务器进程使用HTTP相互通信,但它们将这些信息封装在一个基本的TCP数据包中,该数据包通过网络路由。TCP协议并不明确理解HTTP协议,它只是一个发送HTTP的机制。这是因为TCP位于传输层,而HTTP位于应用层。

虽然不如TCP/IP模型那样健壮,但相同的关注点分离可以应用于处理UVM sequence通信。UVM Sequence/Driver数据流不是像TCP/IP那样有4层,而是只包含两层:应用层和传输层。下图说明了激励通过UVM sequencer的流程:

图3 — UVM Sequence/Driver数据流

正如TCP层能够发送HTTP(Web)、SMTP(电子邮件)和许多其他协议一样,sequencer传输层可以用于发送许多不同类型的应用层请求和响应。


Page 6

Figure

4. UVM Sequencer传输层

在UVM激励的"传输"层有两个独立但相关的通信过程:sequence与sequencer交互,driver与sequencer交互。两者都使用直接方法调用实现,整个协议由UVM严格定义。在此通信过程中,交互可以分三个基本阶段:

- 对齐(Alignment):在此阶段,sequence、sequencer和driver彼此"排队对齐"。Sequence发起一个新的请求,如果driver尚未准备好接受,或者sequencer确定轮到另一个sequence,或者sequence自身确定该请求不是立即相关的,则会被阻塞。如果driver期待一个新的请求,它将被阻塞直到有sequence可用。

- 请求传输(Request Transmission):进入此阶段时,sequence、sequencer和driver全部对齐,sequence必须在不消耗任何额外时间的情况下将请求传输给driver。零时间要求存在的原因是,如果时间推进,sequence或sequencer可能再次不对齐。发送请求后,sequence在此阶段被阻塞,直到driver确认该item。虽然在driver接收item和确认item之间可以消耗时间,但driver只允许有一个未确认的未完成请求,因此在此期间的任何处于对齐阶段的sequence都将继续被阻塞。

- 响应处理(Response Handling):技术上可选的,此阶段可能根据发送给driver的请求的性质而跳过。如果请求需要超出driver基本"确认"之外的额外信息,则该信息以额外响应的形式提供。任何数量的sequence可以同时处于此阶段。

请注意,没有一个阶段明确提到被驱动的总线/接口。这是有意的,因为与实际接口的交互被认为是"更高层次"的。传输层阶段仅仅处理在sequence和driver之间来回传递请求和响应。


Page 7

Figure

下图说明了传输层交互的三个阶段以及每个阶段所使用的方法:

图4 — UVM Sequencer传输层协议

注意:wait_for_grant()get_next_item(req) 不是严格有序的,任何一个调用都可能先发生。

5. UVM Sequence/Driver应用层

sequence和driver之间的交互被视为应用层(与sequencer提供的传输层相对),而UVM对这种交互的性质描述得出奇地少。事实上,对应用层只有少量的要求:

1. Sequence和driver之间的交互必须采用由sequence发起、发往driver的请求形式。 2. Driver必须确认每个请求的接收。 3. Driver可以选择性地向原始sequence发送响应。 4. 单个请求可能导致多个响应。 5. 请求和响应必须扩展UVM sequence item类。

除了这些要求之外,UVM对sequence/driver交互的确切性质保持沉默。这不是方法学的疏忽,因为每个接口/总线等都有特定于应用的要求,无法以比"请求"和"响应"更具体的方式通用表达。

6. 混淆应用层和传输层

虽然可以将高层含义映射到底层交互,但开发人员这样做是冒险的,会导致测试编写者不必要的工作量。不幸的是,这是UVM VIP 验证IP开发人员(和教程)最常见的错误之一,混淆了与sequencer的低层交互和高层VIP开发人员意图(即混淆了传输层与应用层)。


Page 8

Figure

为了说明这种混淆,让我们从两个相对简单的接口开始:APB [4] 和 AHB-Lite [5]。

APB接口是非流水线的,也就是说该接口一次只能处理一个请求。因此,VIP开发人员可能倾向于生成以下item和示例sequence:

class apb_item extends uvm_sequence_item;
  // 32 bit address
  rand logic [31:0] addr;

// 1 indicates a WRITE // 0 indicates a READ rand logic write;

// Request data // Set by sequence in a write // Set by driver in a read rand logic [31:0] data;

// Constructor, macros, etc... endclass : apb_item

class apb_sequence extends uvm_sequence#(apb_item); // Constructor, macros, etc...

virtual task body(); apb_item tmp;

// Read address 0x500 uvm_create(req) req.addr = 32'h0000_0500; req.write = 0; // READ uvm_send(req)

// Save off the response tmp = req

// Write inverted data back uvm_create(req) req.addr = 32'h0000_0500; req.write = 1; // WRITE req.data = !tmp.data; uvm_send(req) endtask : body endclass : apb_sequence


Page 9

Figure

与APB接口不同,AHB-Lite是流水线的,支持接口上同时有多个请求。因此,同一个VIP开发人员可能倾向于生成另一种item和示例sequence:

class ahb_item extends uvm_sequence_item;
  // 32 bit address
  rand logic [31:0] addr;

// 1 indicates a WRITE // 0 indicates a READ rand logic write;

// Request data // Set by sequence in a write // Set by driver in a read rand logic [31:0] data[];

// burst, lock, prot, size, etc...

// Constructor, macros, etc... endclass : ahb_item

class ahb_sequence extends uvm_sequence#(ahb_item); // Constructor, macros, etc...

virtual task body(); apb_item tmp;

// Read address 0x500 uvm_create(req) req.addr = 32'h0000_0500; req.write = 0; // READ req.size = 2; // DWORD uvm_send(req) get_response(rsp);

// Write inverted data back uvm_create(req) req.addr = 32'h0000_0500; req.write = 1; // WRITE req.size = 2; req.data[0] = !rsp.data[0]; uvm_send(req) get_response(rsp)

endtask : body endclass : ahb_sequence


Page 10

Figure

两个示例都完全可以正常工作,但有一个小问题:我们的VIP开发人员强制测试编写者根据接口的性质以不同的方式与sequencer交互。虽然测试理解接口的流水线特性可能是有意义的,但是没有理由让底层交互需要感知这些高层细节。

如果VIP开发人员不是依赖item_done()来指示请求已完成,而是将该信息通过响应路径发送上去,那么APB和AHB-Lite VIP都将呈现相同的交互模型,即使它们的高层接口支持不同的能力。换句话说,我们告诉VIP *做什么*可以并且应该与*如何*告诉VIP去做保持正交。

7. 你在跟我说话吗?

除了混合应用层和传输层通信造成的混淆之外,VIP开发人员还有另一个障碍需要跨越,以便将响应信息正确路由回原始sequence。

当向sequence发送响应时,driver必须向sequencer提供路由信息。该信息通过set_id_info()方法提供:

// Copy the routing info from the request
rsp.set_id_info(req);
// Send the response
seq_item_port.put(rsp);

这个"ID"允许sequencer同时处理多个未完成的请求。每个请求都有唯一的标识符,该标识符就是sequencer确保响应回到原始sequence所需的全部信息。只有一个问题...如果原始sequence有多个未完成的请求怎么办?

例如:

virtual task body();
  REQ req1, req2;
  fork : multi_req
    uvm_do(req1)
    uvm_do(req2)
  join_none : multi_req
  get_response(rsp);
  // Is this rsp to req1 or req2!?
endtask : body

Page 11

Figure

幸运的是,UVM提供了通过get_transaction_id()方法在sequence内将响应连接到其原始请求的能力。如果sequence编写者希望显式等待特定的响应,可以将transaction id传递给get_response()方法:

  get_response(rsp, req1.get_transaction_id());

但需要注意,如果sequence期望的顺序与driver提供的顺序不符,sequence机制可能会死锁:

virtual task body();
  REQ req1, req2;
  fork : multi_req
    uvm_do(req1)
    uvm_do(req2)
  join_none : multi_req
  get_response(rsp, req1.get_transaction_id);
  get_response(rsp, req2.get_transaction_id);
  // What happens if req2 is responded to first?
endtask : body

更安全的机制是在接收响应后检查transaction id,防止死锁,并允许捕获更好的调试信息:

virtual task body();
  REQ req1, req2;
  fork : multi_req
    uvm_do(req1)
    uvm_do(req2)
  join_none : multi_req
  get_response(rsp);
  case (rsp.get_transaction_id())
    (req1.get_transaction_id()): //... req1
    (req2.get_transaction_id()): //... req2
    default: //... error!
  endcase
endtask : body

还值得注意的是,sequence不应请求尚未发送的请求的响应,因为发送请求的动作才是触发设置transaction ID的原因。

8. 实战演练

虽然sequence和driver之间应用层接口的具体细节不能被泛化,但有一种响应类型在这一层经常被需要,那就是driver已完成请求的指示。使用这种类型的响应,我们可以展示一个简单示例,说明如何以一致且可扩展的方式在sequence和driver之间进行通信。


Page 12

Figure

在我们的示例中,VIP支持以下高层会话状态:

- ACCEPTED:Driver已收到请求,但尚未开始在总线上发送 - BEGIN_REQ:Driver已开始在总线上发送请求 - END_REQ:Driver已完成在总线上发送请求

当请求到达这些状态中的每一个时,driver将向上发送一个响应给原始sequence。

请记住,这些状态仅作为示例提供...请求可以有任意数量的状态。虽然这种分组是一个常见的子集(实际上在TLM 2.0 [6]中也有出现),但VIP开发人员最终负责确定哪些状态对传播回原始sequence有意义。由于driver被允许为给定的请求发送任意数量的响应,因此状态空间是高度可扩展的。

我们的sequences可以使用这些高层状态来等待请求完成:

virtual task body();
  REQ req1, req2;
  // Generate a random write
  uvm_do_with(req1, {addr > 32'h0000_F000;
                      rw == WRITE;})
  // Wait for the request to complete
  do
    get_response(rsp);
  while (rsp.req_state != END_REQ);
endtask : body

由于sequence正在使用响应路径来获取高层信息,driver实现的具体细节现在从底层交互中被抽象出来了。


Page 13

Figure

Driver可以不使用流水线来实现:

task my_driver::run_phase(uvm_phase phase);
  forever
    begin : get_and_drive
      seq_item_port.get(req);
      // Prepare the response
      rsp = RSP::type_id::create("rsp", this);
      rsp.set_id_info(req);
      // Mark as ACCEPTED while waiting for the
      // bus to be ready
      rsp.req_state = ACCEPTED;
      seq_item_port.put(rsp);
      // Wait for bus to be ready
      wait_for_ready();
      // Once the bus is ready, mark as BEGIN_REQ
      rsp.req_state = BEGIN_REQ;
      seq_item_port.put(rsp);
      drive_request(req);
      // Once the request is complete, mark as
      // END_REQ
      rsp.req_state = END_REQ;
      seq_item_port.put(rsp);
    end : get_and_drive
endtask : run_phase

或者也可以使用流水线实现:

task my_driver::run_phase(uvm_phase phase);
  forever
    begin : get_and_drive
      // Wait for pipeline space to be available
      wait (num_requests < max_requests);
      // Retrieve the next request
      seq_item_port.get(req);
      num_requests++;
      // Prepare the response
      rsp = RSP::type_id::create("rsp", this);
      rsp.set_id_info(req);
      // Mark as ACCEPTED
      rsp.req_state = ACCEPTED;
      seq_item_port.put(rsp);
      // Fork off the processing
      fork : pipeline_fork
        // Save off the request/response
        automatic REQ forked_req = req;
        automatic RSP forked_rsp = rsp;
        // Handle the pipe
        begin : request_process
          // Wait for bus to be ready
          wait_for_ready(forked_req);
          // Once the bus is ready, mark as BEGIN_REQ
          forked_rsp.req_state = BEGIN_REQ;
          seq_item_port.put(forked_rsp);
          // Drive the request on the bus
          drive_request(forked_req);
          // Once the request is complete, mark as
          // END_REQ
          forked_rsp.req_state = END_REQ;
          seq_item_port.put(forked_rsp);
          num_requests--;
        end : request_process

Page 14

Figure
      join_none : pipeline_fork
    end : get_and_drive
endtask : run_phase

无论是流水线还是单线程,我们的VIP开发人员都可以自由地按自己的意愿实现driver,而在任何时候测试编写者与sequencer的交互都不需要改变。

9. 为什么不...?

虽然第8节中的编码模式可以通用地工作,但开发人员可能希望使用driver的一些变体。虽然这些变体存在,但它们并不总是像这些模式那样有能力,而且往往会导致不必要的混淆。我们在此讨论其中一些替代方案。

替代方案1:为什么不使用 get_next_item/item_done 而不是 get?

由于UVM(及其前身OVM [7])的演进过程,driver的sequence item port为完成相同的目标提供了多种机制,如下表所示:

表1 — Sequence Item端口方法

方法"A"方法"B"
get(req);get_next_item(req); item_done(req);
if (has_do_available()) get(req); else req = null;try_next_item(req); item_done(req)

虽然两种方法在最终结果上几乎相同,但方法"B"中显式需要的item_done()调用可能会诱使开发人员将某种特殊含义映射到item_done()上……可能会混淆高层和低层。如果VIP开发人员从不使用item_done(),他们就不太可能将任何特殊含义映射到它上面。

替代方案2:为什么不使用内置事件?

由于sequence items派生自uvm_transaction,它们具有某些内置事件,可用于表达当前状态:begin_eventend_event。人们可能会被诱惑使用sequencer的引用传递实现来绕过使用响应路径,而改为使用这些事件来传达相同的信息。


Page 15

Figure

不幸的是,这些事件严格绑定到记录接口,这意味着它们可能不会与VIP开发人员的意图正确对齐。

例如,一个接口可能支持具有相关协议响应的读请求。在这种情况下,会话状态可能包括BEGIN_REQ、END_REQ、BEGIN_RSP和END_RSP。请求的end_event可能在会话达到END_REQ状态时触发……然而还有更多的信息需要sequence等待。

虽然这不是第7节中描述的层次违规,但原因本质上是相同的:重载一个接口的定义来试图映射另一个接口。

值得注意的是,即使是UVM寄存器层也犯了这个特定错误,依赖end_event来表示请求的响应就绪完成。这个不幸的实现产物一直是许多VIP开发人员和寄存器测试编写者的痛苦之源。

替代方案3:uvm_push_driver 呢?

UVM中一个经常被忽视的元素是替代的"push"模式sequencer和driver。这些构造旨在提供基于推送的流程,其中不是driver在专用的uvm_seq_item_pull_port上调用get(req),而是sequencer在经典的uvm_tlm_blocking_put_port上调用put(req)。这些替代模式sequencer和driver是UVM演进过程中的又一个产物,在标准中提供了进一步的重复区域。

虽然push模式完全可以作为传输层使用,但它确实有一个可能不希望的后果。push模式sequencer的底层实现不断尝试发送新的请求,类似于这个示例代码:

forever
  begin : push_loop
    // Arbitrate next item
    this.get_next_item(req);
    // Push to driver
    req_port.put(req);
    // Mark item as done
    this.item_done();
  end : push_loop

虽然这看起来无害,但有可能put(req)调用在driver中被阻塞,因为driver尚未准备好处理新请求。换句话说,虽然sequence和sequencer是对齐的,但driver没有对齐。不幸的是,如果sequence在driver准备好之前偏离对齐状态,那么sequencer最终将传输一个无效的请求。如果sequence从相关变为不相关,或者如果有更高优先级的sequence上线,这种情况就可能发生。

值得注意的是,这种替代模式机制是应用层和传输层之间区别的又一个很好的例子。Sequences通常不知道sequencer和driver选择实现了哪种传输层实现。


Page 16

Figure

10. 结论

虽然UVM 通用验证方法学为测试平台主动域中的激励创建提供了基本结构,但它未能说明这些结构如何能够以一致的方式被VIP 验证IP开发人员使用,导致行业中出现了分歧的实现和培训材料。

通过借鉴现代网络中使用的通信模型,我们可以将VIP开发人员和测试编写者的意图适当地封装在高层请求和响应中,将这些信息与这些请求和响应如何在sequence和driver之间传输的底层细节分开。这种解决方案为VIP开发人员和终端用户提供了一种功能齐全、可扩展、一致且熟悉的激励生成模式。

11. 参考文献

[1] Universal Verification Methodology (UVM), "1.2 Class Reference," Accellera Systems Initiative, 2014.
[2] Network Working Group, "RFC-1122," Internet Engineering Task Force (IETF), 1989.
[3] "Internet protocol suite," 16 June 2015. [Online]. Available: https://en.wikipedia.org/wiki/Internet_protocol_suite.
[4] ARM Limited, "AMBA 3 APB Protocol Specification," 2004.
[5] ARM Limited, "AMBA 3 AHB-Lite Protocol," 2006.
[6] IEEE 1666, "Open SystemC Language Reference Manual," 2011.
[7] Open Verification Methodology (OVM), "Class Reference," Cadence Design Systems, Inc. and Mentor Graphics, Corp., 2011.


图片索引

本文共 4 张图片,存放于 SNUG_TPC_UVM_Poppen_Lies_Teacher_Told_About_The_UVM_Basic_Stimulus_paper_images/` 目录。