UVM Blocks

January 22, 2019

UVM 첫화면

  1. UVM 개요
  2. Design Under Test (DUT)
  3. UVM Test Flow
  4. UVM Blocks

이번 글에서는 UVM 환경을 만들기 위한 블럭을 구현해 보겠습니다. 전체를 다 구현하기엔 분량이 너무 많아서 오늘은 uvm_agent, uvm_driver, uvm_sequencer 세개를 구현해 볼까 합니다. 이 세개를 구현한다고 해서 곧장 테스트를 할 수는 없으니, 다음에 나올 uvm_test, uvm_env, uvm_sequence 까지는 구현해야 기본 UVM 시뮬레이션을 실행해 볼 수 있겠네요.

Virtual Interface & uvm_confib_db

DUT 글에서 언급했던 것 처럼, UVM 클래스와 DUT는 interface로 연결되어 있습니다. tb.sv에서 본것 처럼 실제 인터페이스는 tb module에 있고, 클래스는 그 인터페이스의 reference를 받아서 사용합니다. C 언어의 포인터같은 개념으로 생각하면 좀 쉬워요. 이걸 virtual interface라고 부릅니다.

connect_phase 단계에서 tb의 실제 interface, bif, iifuvm_driver에서 연결합니다. 이 연결이 조금은 독특한 방식으로 이뤄지는데요. 여기서 새로운 개념이 등장합니다. uvm_config_db 인데, 이것은 공용 저장소라고 생각하면 될 것 같네요. tb에서 이곳에 virtual interface를 저장(set)하고, uvm_driver에서 virtual interface 를 가져옵니다(get). 일단 저장하는 코드를 볼까요.

module tb;

  import uvm_pkg::*;
  // ...
  bus_if bif(clk, rst_n);
  intr_if iif(clk);

  initial begin
    uvm_config_db#(virtual bus_if)::set(null,"*","bif", bif);
    uvm_config_db#(virtual intr_if)::set(null,"*","iif", iif);
  end

  // ...
endmodule

tb에 이전보다 몇 줄이 추가되었습니다. 일단 uvm_config_db를 사용하기 위해 tbimport uvm_pkg::*; 가 추가되었네요. 이건 systemverilog에 익숙하다면 알 수 있을텐데 uvm_pkg 패키지에 선언된 클래스, 함수등을 불러오는 코드입니다. 이 안에는 config db 클래스도 있는거죠.

그럼 그 다음에 나오는 파라미터는 뭐냐면, 어느 타입의 config를 접근할 것인지 알려주는 역할입니다. c++코드의 generic (template)이라고 보면 되구요. 그래서 첫번째 uvm_config_dbvirtual bus_if 타입의 환경설정에 key는 “bif”bif 인터페이스를 저장하는 코드이겠네요. 두번째는 virtual intr_if 타입의 환경설정에 iif 인터페이스를 저장하겠구요.

그러면 이것을 어떻게 불러오는지는 set 대신에 get 함수를 쓰면 되겠죠?

// base/uvm_config_dv.svh

  // @uvm-ieee 1800.2-2017 auto C.4.2.2.2
  static function bit get(uvm_component cntxt,
                          string inst_name,
                          string field_name,
                          inout T value);

실제로 uvm_driver에서는 아래처럼 쓰입니다.


class bus_driver extends uvm_driver #(bus_seq_item);

  // ....
  virtual bus_if vif;

  // ...
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (!uvm_config_db#(virtual bus_if)::get(this,"","bif",vif)) begin
      `uvm_fatal("NO_VIF", {"virtual interface must be set for:",
        get_full_name(),".vif"});
    end
  endfunction : connect_phase

  // ...
endclass

Sequence Item

위의 코드에서 bus_seq_item 이라는 새로운게 등장했네요. 이것은 버스 인터페이스를 동작시키기 위한 레시피입니다. 버스 트랜잭션을 한번 동작하기 위해 충분한 정보가 있어야 하니 아래처럼 기술해 봅시다.

class bus_seq_item extends uvm_sequence_item;

  rand bit [31:0] address;
  rand bit        write;
  rand bit [31:0] wdata;
       bit [31:0] rdata;

  `uvm_object_utils_begin(bus_seq_item)
    `uvm_field_int (address, UVM_DEFAULT)
    `uvm_field_int (write,   UVM_DEFAULT)
    `uvm_field_int (wdata,   UVM_DEFAULT)
    `uvm_field_int (rdata,   UVM_DEFAULT)
  `uvm_object_utils_end

  function new(string name = "bus_seq_item");
    super.new(name);
  endfunction : new

endclass

간단히 어드레스, 읽기/쓰기 상태, 쓰기값, 읽기값을 선언했습니다. 이 중에 처음 세개는 rand 설정을 했는데요, 나중에 uvm_sequence에서 randomize 할 때 지정된 필드가 랜덤값이 할당됩니다. 이 클래스에 systemverilog의 constraint 를 선언해서 randomize 수행될 때 지정된 범위내에서 값이 할당되도록 설정할 수 있습니다.

그 다음에 나오는 복잡한 \uvm_object_utils_begin(), `uvm_field_int()같은 매크로는 무엇인지 이해가 안가실 수도 있겠습니다. 이것은macros/uvm_object_defines.svh에 정의되어 있는데요. 저는 저 매크로를 따라가기 벅차더군요. 대충 훑어본 바로는 UVM에서 공통으로 사용되는 함수에 맞춰서 각 field가 동작할 수 있도록 이런 저런 함수를 추가하는 매크로더군요. Copy를 하고, compare, pack, print를 하는 코드를 추가해 줍니다. 즉, 위의bus_seq_item`은 보이는 코드는 간단하지만 실제론 매우 복잡한 클래스라는거죠.

UVM Driver

이제 sequence item도 선언했으니, 해당 아이템을 받은 uvm_driver가 어떻게 bus를 동작시키는 지 구현해 볼 차례네요.

위에서 언급한 대로 connect_phase에서 bus interface를 내부 vif 멤버변수에 할당했으면, run_phase에서 bus_seq_item이 주어질 때 마다 버스를 동작시키면 됩니다. 코드를 하나 하나 살펴보죠.

class bus_driver extends uvm_driver #(bus_seq_item);

  virtual bus_if vif;

  `uvm_component_utils(bus_driver)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);

    if (!uvm_config_db#(virtual bus_if)::get(this,"","bif",vif)) begin
      `uvm_fatal("NO_VIF", {"virtual interface must be set for:",
        get_full_name(),".vif"});
    end
  endfunction : connect_phase

  virtual task run_phase(uvm_phase phase);
    // One request at a time
    vif.m2s_valid = 1'b0;
    vif.s2m_ready = 1'b0;

    wait(vif.rst_n == 1'b0);
    wait(vif.rst_n == 1'b1);

    @(negedge vif.clk);

    forever begin
      seq_item_port.get_next_item(req);

      // Send request
      vif.m2s_address = req.address;
      vif.m2s_write   = req.write;
      vif.m2s_data    = req.wdata;
      vif.m2s_valid   = 1'b1;
      @(posedge vif.clk iff vif.m2s_ready == 1'b1);
      vif.m2s_valid   = 1'b0;

      // Receive response
      vif.s2m_ready = 1'b1;
      @(posedge vif.clk iff vif.s2m_valid == 1'b1);
      assert (0 == vif.s2m_error)
        else `uvm_error(get_name(),
          $sformatf("BUS Error occurs: ADDR('h%08x)", req.address));
      req.rdata = vif.s2m_data;
      vif.s2m_ready = 1'b0;

      // Return response to Sequencer (eventually goes to sequence)
      seq_item_port.item_done(req);
    end
  endtask : run_phase

endclass : bus_driver

이번 코드는 좀 길죠? 그래도 위에서 한번 봤던 부분이 있으니 그 부분을 넘어가고 실제로 봐야 할 곳은 run_phase 밖에 없는 것 같네요. 여기서 첫줄과 마지막 줄을 제외하고는 일반적인 forever loop task와 비슷합니다. 버스에 address, data를 싣고 response로 들어온 것을 rdata에 저장하는, 이해하기 쉬운 코드죠. 그러면 첫줄과 마지막줄의 seq_item_portget_next_item() 함수와 item_done() 함수를 살펴보면 되겠네요.

seq_item_portuvm_driveruvm_sequencer와 이야기할 수 있는 수단입니다. 이 포트를 통해 (uvm_sequence_pull_port) uvm_sequencerget_next_item() 함수를 호출해서 다음 bus_seq_item을 하나 가져오는 거죠. 그리고 모든 버스 동작이 끝나면 item_done(req)을 호출해서 해당 트랜잭션이 끝났다고 알려줍니다. 만일 리턴할 내용이 없다면 item_done()으로 인자를 비워두고 호출하면 됩니다.

좀 자세히 설명을 하면 더 복잡해 질 수 있는데 (예를 들면 uvm_driver는 parameter를하나가 아닌 두개, REQ, RSP 로 받는다던지 하는 것) 그 부분은 일단 생략하고 먼저 이 버스 드라이버를 동작하게 해서 시뮬레이션을 한번 돌려보는 걸로 하죠.

UVM Sequencer

bus_driver가 sequencer에게서 bus_seq_item을 받아와야 하니 이제 bus_sequencer를 만들어봅시다. Sequencer는 이정도 복잡도의 검증환경에서는 딱히 할 일 없이 그저 uvm_sequence에서 전달해 주는 트랜잭션을 그대로 Driver가 가져가게 해주는 역할만 합니다.

class bus_sequencer extends uvm_sequencer #(bus_seq_item);

  `uvm_sequencer_utils(bus_sequencer)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

endclass : bus_sequencer

간단하죠? 상속받은 클래스인 uvm_sequencer 안에 왠만한 함수가 다 들어가 있어서 특별히 수정할 필요가 없네요. 그러면 이제 bus_driverbus_sequencer를 관리하는 bus_agent 클래스를 만들면 proactive agent 구현은 거의 끝나게 됩니다. bus_monitor는 나중에 scoreboard를 만들 때 같이 구현하도록 할게요.

Agent

Agent도 사실 거의 형식적이고 그다지 수정할 부분이 없습니다.

class bus_agent extends uvm_agent;
  bus_driver     m_drv;
  bus_sequencer  m_seqr;

  `uvm_component_utils(bus_agent)

  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    if (get_is_active() == UVM_ACTIVE) begin
      m_drv  = bus_driver::type_id::create("m_drv", this);
      m_seqr = bus_sequencer::type_id::create("m_seqr", this);
    end
  endfunction : build_phase

  function void connect_phase(uvm_phase phase);
    if (get_is_active() == UVM_ACTIVE) begin
      m_drv.seq_item_port.connect(m_seqr.seq_item_export);
    end
  endfunction : connect_phase

endclass : bus_agent

여기서 한가지 눈에 띄이는 것이 있다면 connect_phase에서 bus_driverseq_item_portbus_sequencerseq_item_export를 연결하는 부분이겠네요. 이렇게 둘이 연결해 줌으로써 bus_driverget_next_item()을 호출할 때 bus_sequencer의 TLM FIFO에 접근하게 됩니다.