Adder UVM Practice

Seungyun Lee·2026년 2월 20일

UVM

목록 보기
3/14

Arcitecture Diagram

===================================================================================
 [하드웨어 영역 - 물리적 보드]                   [소프트웨어(UVM) 영역 - 동적 객체]
===================================================================================

 ┌──────────────────────────────────┐        ┌──────────────────────────────────┐
 │ tb_top.sv (최상위 모듈)             │        │ adder_test.sv (지휘관 상자)         │
 │                                  │        │  - 전체 테스트 시작 (run_test)       │
 │  ┌────────────────────────────┐  │        │  - adder_seq(대본) 실행 명령        │
 │  │ adder_if.sv (인터페이스)      │◀─┼─(vif)──┼─┐                                │
 │  │ - clk, rst, a, b, y        │  │        │ │ ┌────────────────────────────┐ │
 │  └──────┬───────────▲─────────┘  │        │ │ │ adder_env.sv (환경 상자)     │ │
 │         │           │            │        │ │ │                            │ │
 │         │ 물리적 핀   │            │        │ │ │ ┌────────────────────────┐ │ │
 │         ▼           │            │        │ │ │ │ adder_agent.sv (에이전트)│ │ │
 │  ┌────────────────────────────┐  │        │ └─┼─┼─▶ adder_driver.sv      │ │ │
 │  │ adder.sv (DUT, 검증할 칩)    │  │        │   │ │    (vif에 0과 1로 쏘기)   │ │ │
 │  │ - input a, b               │  │        │   │ │           ▲            │ │ │
 │  │ - output y                 │  │        │   │ │           │ (택배 상자)  │ │ │
 │  └────────────────────────────┘  │        │   │ │     uvm_sequencer      │ │ │
 └──────────────────────────────────┘        │   │ │           ▲            │ │ │
                                             │   │ │           │            │ │ │
      [데이터 패킷(트랜잭션)]                     │   │ │      adder_seq.sv      │ │ │
 📦 adder_item.sv (택배 상자)                   │   │ │   (a,b 랜덤 무한 생성)    │ │ │
    - rand int a, b;                         │   │ │                        │ │ │
    - int y;                                 │   │ │ ┌────────────────────┐ │ │ │
                                             │   │ │ │ adder_monitor.sv   │ │ │ │
                                             │   │ │ │(vif에서 결과 훔쳐봄)   │ │ │ │
                                             │   │ │ └─────────┬──────────┘ │ │ │
                                             │   │ └───────────┼────────────┘ │ │
                                             │   │             │ (결과 복사본)   │ │
                                             │   │ ┌───────────▼────────────┐ │ │
                                             │   │ │ adder_scoreboard.sv    │ │ │
                                             │   │ │ (a + b == y 정답 채점)   │ │ │
                                             │   │ └────────────────────────┘ │ │
                                             │   └────────────────────────────┘ │
                                             └──────────────────────────────────┘

각 유닛에 대충 뭐가 들어가는지

item
input, output 작성

sequencer
 virtual task body()
 uvm_do()
 
driver
 set_item_port.get_next_item(req)
 @(posedge vif.clk)
 
monitor
 adder_item captured_item;
 ap.write(captured_item)
 
scoreboard
 uvm_analysis_imp #(adder_item, adder_scoreboard) item_export;
 virtual function void write(adder.item.imp)
 logic [8:0] expected y
 expected y = item.a + item.b

데이터의 흐름

  1. 대본 작성
    adder_seq : "오늘은 A=5, B=10을 담은 택배 상자(adder_item)를 만들어야지!" 하고 무작위로 상자를 찍어내서 아래로 던집니다.

  2. 하드웨어 구동
    adder_driver: 위에서 떨어진 택배 상자를 열어보니 A=5, B=10이 들어있습니다. 드라이버는 가상 리모컨(vif)을 조작해서 진짜 하드웨어 인터페이스(adder_if)의 선에 5와 10이라는 전기 신호를 인가합니다.

  3. 칩 동작
    adder DUT: 하드웨어 칩은 선으로 들어온 5와 10을 받아 덧셈을 수행하고, 출력 핀 y에 15라는 결과를 내보냅니다.

  4. 결과 관찰
    adder_monitor : 칩의 입출력 핀을 몰래 지켜보고 있던 모니터가 "오! A=5, B=10이 들어갔고 Y=15가 나왔군!" 하고 새로운 택배 상자에 이 값들을 싹 복사해서 채점기에게 몰래 넘겨줍니다.

  5. 정답 채점
    adder_scoreboard: 모니터가 던져준 상자를 받은 채점기는 계산기를 두드립니다. "A(5) + B(10)은 15가 맞나? 칩에서 나온 Y도 15네? 통과(PASS)!" 만약 값이 다르면 에러(UVM_ERROR)를 터뜨립니다.

File 설명

1. 하드웨어 영역 (물리적 뼈대)

RTL 설계자에게 가장 익숙한 부분입니다. 핀을 정의하고 칩을 올려놓는 물리적인 작업 공간입니다.

  • adder.sv (DUT): 우리가 검증할 실제 하드웨어 칩입니다. (A 입력, B 입력, 결과 Y 출력)
  • adder_if.sv (인터페이스): UVM 소프트웨어가 하드웨어 핀에 접근할 수 있도록 도와주는 연결 단자대입니다.
  • tb_top.sv (최상위 보드): 클럭을 만들고, DUT와 인터페이스를 연결한 뒤 run_test()로 UVM을 깨우는 메인 실행 파일입니다.

2. 데이터와 시나리오 (소프트웨어 알맹이)

어떤 값을 테스트할지, 몇 번 테스트할지 결정하는 가장 중요한 핵심 알맹이입니다. 직접 머리를 써서 채워 넣어야 할 부분이죠.

  • adder_item.sv (택배 상자): 입력값 A, B와 결과값 Y를 담을 class입니다. A와 B에 rand 키워드를 붙여서 무작위 값을 생성할 준비를 합니다.
  • adder_seq.sv (테스트 대본): 위에서 만든 택배 상자를 100번, 1000번 반복해서 찍어내고 드라이버에게 던져주는 시나리오입니다.

3. 행동 대원들 (실제 일꾼들)

데이터를 하드웨어로 밀어 넣고, 결과를 빼와서 정답인지 확인하는 3인방입니다.

  • adder_driver.sv (입력기): 시나리오 대본(seq)에서 넘겨준 A와 B 값을 진짜 하드웨어 인터페이스 핀에 0과 1의 전기 신호로 쏘아줍니다.
  • adder_monitor.sv (CCTV 관찰자): 하드웨어 핀을 가만히 지켜보다가, A와 B가 들어가고 결과 Y가 나오는 순간을 포착해서 채점기에게 그 값들을 복사해 넘겨줍니다.
  • adder_scoreboard.sv (자동 채점기): 모니터가 넘겨준 A, B, Y 값을 보고 A + B == Y가 맞는지 계산하여 에러를 띄울지 패스를 띄울지 결정합니다.

4. 포장 상자들 (UVM 관리자)

위에서 만든 행동 대원들이 굴러다니지 않게 상자에 차곡차곡 담는 과정입니다. (이 파일들은 강의에서 본 껍데기 코드와 거의 100% 똑같습니다.)

  • adder_agent.sv: 드라이버와 모니터를 한 묶음으로 포장하는 상자입니다.
  • adder_env.sv: 에이전트 상자와 자동 채점기(Scoreboard)를 하나의 큰 세트로 포장하는 상자입니다.
  • adder_test.sv: 가장 큰 최종 지휘관 상자입니다. 환경(Env)을 세팅하고, 우리가 만든 대본(seq)을 시작하라고 명령을 내립니다.

Code Review

데이터, 시나리오

adder_item.sv


`ifndef ADDER_ITEM_SV   // ifndef(If Not Defined): "만약  ADDER_ITEM_SV라는 
`define ADDER_ITEM_SV  //이름표가 아직 안 붙어있다면, 지금 붙이고(define) 코드를 읽어라

//우리가 설계할 검증 환경의 이름을 adder_item로 지음, uvm_sequence_item 상속
class adder_item extends uvm_sequence_item;

  // Inputs: Randomized to generate various test cases
  rand logic [7:0] a;
  rand logic [7:0] b;

  // Output: Not randomized, captured from the DUT by the monitor
  logic [8:0] y;

  // Register this object to the UVM factory
  `uvm_object_utils(adder_item)

  // Constructor
  function new(string name = "adder_item");
    super.new(name);
  endfunction

  // Utility function to easily print the item's contents
  virtual function string convert2string();
    return $sformatf("a=%0d, b=%0d | y=%0d", a, b, y);
  endfunction

endclass

`endif

adder_seq.sv

`ifndef ADDER_SEQ_SV
`define ADDER_SEQ_SV

class adder_seq extends uvm_sequence #(adder_item); // adder_item 상자만 전담해서 다룬다고 선언

  `uvm_object_utils(adder_seq)

  // Constructor
  function new(string name = "adder_seq");
    super.new(name);
  endfunction

//------------------------------작성해야 하는 부분---------------------------

  // The main execution block for the sequence
  virtual task body(); // 시작 명령어
    
    `uvm_info("SEQ", "Starting the generation of 10 random adder items...", UVM_LOW)
    
    // Generate and send 10 random transactions
    // The `uvm_do macro handles creation, randomization, and sending
    
    repeat(10) begin
      `uvm_do(req)
    end
    
    `uvm_info("SEQ", "Finished generating items.", UVM_LOW)
    
  endtask

endclass

`endif

UVM 최고의 마법: `uvm_do(req)

원래대로라면 상자를 만들고(create), 드라이버에게 보낼 준비를 하고(start_item), 무작위 값을 채우고(randomize), 진짜로 전송하는(finish_item) 긴 코드를 짜야 합니다. 하지만 이 단 한 줄의 매크로가 그 모든 과정을 알아서 해줍니다.

req의 정체: 우리가 선언한 적도 없는데 튀어나온 이 req는 uvm_sequence 부모 클래스 안에 이미 기본으로 만들어져 있는 '내 전용 택배 상자 변수'입니다.
즉, repeat(10) 루프를 돌면서 "상자 만들고 무작위 값 채워서 던져!"를 10번 반복하는 아주 강력한 코드입니다.

일꾼들

adder_driver.sv

`ifndef ADDER_DRIVER_SV
`define ADDER_DRIVER_SV

class adder_driver extends uvm_driver #(adder_item);

  virtual adder_if vif;
  `uvm_component_utils(adder_driver)

  // Constructor
  
  function new(string name = "adder_driver", uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    if(!uvm_config_db#(virtual adder_if)::get(this, "", "vif", vif)) begin
      `uvm_fatal("DRV", "Failed to get virtual interface from config_db!")
    end
  endfunction

  virtual task run_phase(uvm_phase phase);
    
    
 //----------------------작성할 줄 알아햐 하는 부분------------------------------
    
   
    forever begin
    
      seq_item_port.get_next_item(req);
      
      `uvm_info("DRV", $sformatf("Driving data to DUT: %s", req.convert2string()), UVM_LOW)

      @(posedge vif.clk);
      vif.a <= req.a;
      vif.b <= req.b;

      @(posedge vif.clk);

      seq_item_port.item_done();
    end
    
  endtask

endclass

`endif
forever begin

왜 무한 루프를 쓸까요? 드라이버는 하드웨어의 핀과 직결된 녀석입니다. 하드웨어에 전원이 켜져 있는 한, 언제 데이터가 들어올지 모르니 항상 대기해야 합니다.

get_next_item: "다음 상자 주세요!" (Blocking)

seq_item_port.get_next_item(req);
  • 대본(sequence)이 uvm_do 매크로를 통해 위에서 택배 상자를 뚝 떨어뜨리면, 드라이버가 여기서 그 상자를 딱 받아냅니다.
  • 핵심 원리 (Blocking): 만약 위에서 떨어지는 상자가 없다면? 드라이버는 다음 코드로 넘어가지 않고 여기서 시간이 멈춘 채 영원히 기다립니다. 즉, 쓸데없이 클럭을 소모하며 쓰레기값을 핀에 쏘지 않도록 막아주는 완벽한 동기화 장치입니다.
  • req에는 앞서 우리가 adder_item에서 rand로 뽑아낸 무작위 a와 b 값이 들어있습니다.

Pin Wiggling: 진짜 0과 1의 전기 신호 쏘기
이곳이 바로 Verilog와 UVM이 만나는 가장 아름다운 구간입니다!

@(posedge vif.clk);
      vif.a <= req.a;
      vif.b <= req.b;

      // Wait one more clock cycle to hold the values
      @(posedge vif.clk);
  • @(posedge vif.clk);: "가상 리모컨(vif)에 연결된 진짜 클럭 신호가 '0에서 1로 뛰는 순간(Rising Edge)'까지 딱 기다려!"라는 뜻입니다. RTL 시뮬레이션의 물리적인 시간(Time)이 여기서 정확하게 소모됩니다.
    <= (Non-blocking assignment): RTL 설계의 기본이죠! 소프트웨어 클래스 안에서 놀고 있던 무작위 숫자(req.a)를, 실제 물리적인 선(vif.a)에 플립플롭 태우듯이 꽂아 넣습니다.
  • 두 번째 @(posedge vif.clk);: 덧셈기(DUT)가 이 값들을 안정적으로 인식하고 계산할 수 있도록(Setup/Hold time 확보), 클럭 한 번만큼 핀 상태를 꽉 붙잡고(Hold) 유지해 주는 센스 있는 딜레이입니다.
seq_item_port.item_done();

핀에 값을 다 쏘고 났으니, 자신에게 상자를 넘겨줬던 시퀀서(Sequencer)에게 "나 핀에 다 썼어! 이번 건 끝!" 하고 보고하는 핸드셰이크(Handshake) 신호입니다.

adder_monitor.sv

`ifndef ADDER_MONITOR_SV
`define ADDER_MONITOR_SV

// Extend uvm_monitor
class adder_monitor extends uvm_monitor;

  // Virtual interface to SPY on the physical pins
  virtual adder_if vif;

  // Analysis port: The "megaphone" to broadcast captured items to the scoreboard
  uvm_analysis_port #(adder_item) ap;

  // Register to the UVM factory
  `uvm_component_utils(adder_monitor)

  // Constructor
  function new(string name = "adder_monitor", uvm_component parent);
    super.new(name, parent);
  endfunction

  // Build phase: Set up the megaphone and get the remote control
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    // 1. Instantiate the megaphone (Analysis Port)
    ap = new("ap", this);

    // 2. Retrieve the virtual interface from config_db (Same as Driver!)
    if(!uvm_config_db#(virtual adder_if)::get(this, "", "vif", vif)) begin
      `uvm_fatal("MON", "Failed to get virtual interface from config_db!")
    end
  endfunction

  // Run phase: The infinite loop watching the hardware
  virtual task run_phase(uvm_phase phase);
    
    // Create an empty box to store the captured data
    adder_item captured_item;
    captured_item = adder_item::type_id::create("captured_item");

// -----------------이 아래부분이 핵심---------------------------
    // Run forever, spying on the bus
    forever begin
    
      // Wait for the clock edge where data becomes valid
      @(posedge vif.clk);

      // Capture the physical pin values and store them in our software box
      // (Notice we use '=', reading the pins, not driving them with '<=')
      captured_item.a = vif.a;
      captured_item.b = vif.b;
      captured_item.y = vif.y;

      `uvm_info("MON", $sformatf("Captured data: %s", captured_item.convert2string()), UVM_LOW)

      // 3. Broadcast the captured item to anyone listening (Scoreboard)
      ap.write(captured_item);
    end
    
  endtask

endclass

`endif
  1. 확성기 설치 (uvm_analysis_port)
    uvm_analysis_port #(adder_item) ap;
    ap 뜻: analysis_port
    Driver는 Sequencer와 1:1로 핑퐁(Handshake)을 했지만, Monitor는 방송국 확성기를 사용합니다.

  2. 수동적인 관찰 (captured_item.a = vif.a;)
    하지만 Monitor는 핀의 상태를 그저 읽어오기만 합니다. 이미 계산이 끝나서 핀에 흐르고 있는 진짜 전기 신호(vif.a, vif.b, vif.y)를 떠내어, 빈 택배 상자(captured_item)에 쏙쏙 담아줍니다.

  3. 방송 송출 (ap.write(captured_item))
    가장 통쾌한 부분입니다. 상자에 훔쳐 온 데이터를 다 담았다면, ap.write() 함수를 통해 확성기로 상자를 뻥 차서 날려 보냅니다.
    get_next_item처럼 누군가 받을 때까지 기다리지(Blocking) 않습니다. 듣는 사람이 있든 없든 쿨하게 쏴버리고 다음 클럭을 관찰하러 다시 루프를 돕니다.

adder_scoreboard.sv

`ifndef ADDER_SCOREBOARD_SV
`define ADDER_SCOREBOARD_SV

// Extend from uvm_scoreboard
class adder_scoreboard extends uvm_scoreboard;

 
  // [Block 1] Factory Registration
  `uvm_component_utils(adder_scoreboard)

  //-------------------------------------------------------------------------------------
  // Receiver port to catch the megaphone broadcast from the Monitor
  uvm_analysis_imp #(adder_item, adder_scoreboard) item_export;
  //-------------------------------------------------------------------------------------

  // [Block 2] Constructor

  function new(string name = "adder_scoreboard", uvm_component parent);
    super.new(name, parent);
  endfunction


  // [Block 3] Build Phase (Setup)
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    // Instantiate the receiver port
    item_export = new("item_export", this);
  endfunction


  // [Block 4] The Core Logic: write() function
  //------------------------------------------------------------------------
  // This function is AUTOMATICALLY called whenever the monitor broadcasts an item!
  
  virtual function void write(adder_item item);
    
    // 1. Declare a variable for the expected Reference Model
    logic [8:0] expected_y;

    // 2. Calculate the correct answer based on inputs (a + b)
    expected_y = item.a + item.b;

    // 3. Compare the expected answer with the actual DUT output (item.y)
    if (expected_y == item.y) begin
      `uvm_info("SCB", $sformatf("PASS! a(%0d) + b(%0d) = y(%0d)", item.a, item.b, item.y), UVM_LOW)
    end else begin
      `uvm_error("SCB", $sformatf("FAIL! a(%0d) + b(%0d) should be %0d, but got %0d", item.a, item.b, expected_y, item.y))
    end
    
  endfunction
//---------------------------------------------------------------------------
endclass

`endif
  1. 수신기와 자동 실행 (uvm_analysis_imp와 write)
  • uvm_analysis_imp: 모니터의 확성기(port)에서 날아오는 방송을 캐치하는 안테나(수신기)입니다.

  • 마법의 write() 함수: 모니터에서 ap.write(captured_item);를 실행하는 바로 그 순간! Scoreboard의 이 write() 함수가 자동으로 호출(Trigger)됩니다. * 따라서 무한 루프(forever)를 돌면서 언제 데이터가 오나 감시할 필요 없이, 데이터가 도착할 때만 깔끔하게 깨어나서 채점을 진행합니다. 소프트웨어적으로 아주 효율적인 구조죠.

  1. 내가 직접 설계하는 Reference Model (정답지)
  • 이 부분이 검증 엔지니어의 진짜 실력이 들어가는 곳입니다! 지금은 expected_y = item.a + item.b;라는 아주 단순한 한 줄이지만, 만약 DUT가 CNN 가속기라면?

  • 이 write 함수 안에서 파이썬이나 C++ 모델을 연동하거나, 복잡한 컨볼루션 연산 알고리즘을 직접 짜서 expected_y (정답)를 만들어내야 합니다.

  • 정답과 칩의 결과(item.y)가 다르면 `uvm_error를 터뜨립니다. 이 에러 하나를 보기 위해 지금까지 그 긴 UVM 뼈대를 세워온 것입니다!

포장상자들

adder_agent.sv

`ifndef ADDER_AGENT_SV
`define ADDER_AGENT_SV

// Extend from uvm_agent
class adder_agent extends uvm_agent;

  // ============================================================================
  // [Block 1] Declare the 3 core components of an Agent
  // ============================================================================
  // 1. Sequencer: The traffic controller that holds the items
  uvm_sequencer #(adder_item) sqr;
  
  // 2. Driver: The worker that wiggles the pins
  adder_driver drv;
  
  // 3. Monitor: The CCTV that observes the pins
  adder_monitor mon;

  // Register to the UVM factory
  `uvm_component_utils(adder_agent)

  // ============================================================================
  // [Block 2] Constructor
  // ============================================================================
  function new(string name = "adder_agent", uvm_component parent);
    super.new(name, parent);
  endfunction

  // ============================================================================
  // [Block 3] Build Phase: Create the components!
  // ============================================================================
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    // Create each component using the UVM Factory (type_id::create)
    sqr = uvm_sequencer#(adder_item)::type_id::create("sqr", this);
    drv = adder_driver::type_id::create("drv", this);
    mon = adder_monitor::type_id::create("mon", this);
  endfunction

  // ============================================================================
  // [Block 4] Connect Phase: Wire them together!
  // ============================================================================
  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    
    // Connect the Driver's port to the Sequencer's export
    // This allows the get_next_item() and item_done() handshake to work!
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction

endclass

`endif
  1. 부품 조립 (build_phase)

type_id::create라는 UVM 전용 공장 생성 명령어를 사용해서 sqr(시퀀서), drv(드라이버), mon(모니터) 객체를 메모리에 찍어냅니다.

참고: Sequencer는 우리가 직접 클래스를 만들지 않았죠? UVM 라이브러리에 이미 완벽하게 짜여진 uvm_sequencer라는 기본 부품이 있기 때문에, 우리가 만든 adder_item만 취급하라고 #(adder_item) 꼬리표만 달아서 그대로 가져다 쓴 것입니다.

  1. 통신 파이프 연결 (connect_phase)

build_phase에서 부품들이 다 만들어졌다면, 시간 0초의 그다음 단계인 connect_phase에서 선을 연결합니다.

drv.seq_item_port.connect(sqr.seq_item_export);

이 단 한 줄의 코드가 바로 Driver의 get_next_item()이 Sequencer로 무사히 전달되게 만드는 마법의 파이프라인입니다. 하드웨어에서 assign wire_a = wire_b; 하듯이, 소프트웨어 포트를 찰칵 끼워 맞춘 것입니다.

adder_env.sv

이 상자의 가장 핵심적인 임무는 앞서 만들었던 Monitor의 확성기(ap)와 Scoreboard의 수신기(item_export)를 파이프로 연결해 주는 것입니다. 코드를 바로 보겠습니다! (코드 주석은 영어로 작성했습니다.)

`ifndef ADDER_ENV_SV
`define ADDER_ENV_SV

// Extend from uvm_env
class adder_env extends uvm_env;

  // ============================================================================
  // [Block 1] Declare the components
  // ============================================================================
  // 1. The Agent (which contains Sequencer, Driver, Monitor)
  adder_agent agent;
  
  // 2. The Scoreboard (which checks the answer)
  adder_scoreboard scb;

  // Register to the UVM factory
  `uvm_component_utils(adder_env)

  // ============================================================================
  // [Block 2] Constructor
  // ============================================================================
  function new(string name = "adder_env", uvm_component parent);
    super.new(name, parent);
  endfunction

  // ============================================================================
  // [Block 3] Build Phase: Assemble the big parts!
  // ============================================================================
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    // Create the Agent and Scoreboard using the factory
    agent = adder_agent::type_id::create("agent", this);
    scb = adder_scoreboard::type_id::create("scb", this);
  endfunction

  // ============================================================================
  // [Block 4] Connect Phase: Wire the Monitor to the Scoreboard!
  // ============================================================================
  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    
    // Access the Monitor INSIDE the Agent, and connect its megaphone (ap) 
    // to the Scoreboard's receiver (item_export)
    agent.mon.ap.connect(scb.item_export);
  endfunction

endclass

`endif

더 큰 블록들의 생성 (build_phase)

  • 이제는 자잘한 Driver나 Monitor 단위가 아니라, 큼지막한 adder_agent와 adder_scoreboard를 공장(type_id::create)에서 찍어냅니다.
  • 만약 UART, SPI, I2C가 모두 있는 통신 가속기 IP를 검증한다면? 이 build_phase 안에 uart_agent, spi_agent, i2c_agent가 줄줄이 인스턴스화 될 것입니다. Env는 이 모든 것을 아우르는 거대한 보드판입니다.

계층을 뛰어넘는 통신선 연결 (connect_phase)

agent.mon.ap.connect(scb.item_export);

이 한 줄이 UVM 객체 지향 통신의 백미입니다! RTL에서 하위 모듈의 포트를 상위 모듈로 빼서(wire) 연결하듯, 객체 지향에서는 점(.)을 찍어서 하위 객체로 파고들어 갑니다.

"Agent 상자 안(agent.)에 있는 Monitor(mon.)의 확성기(ap)를 가져와서, Scoreboard 상자(scb.)의 수신기(item_export)에 연결(connect)해라!"

adder_test.sv

드디어 소프트웨어(UVM) 세상의 최고 지휘관, Test(adder_test.sv)를 만나볼 차례입니다!
지금까지 우리는 덧셈기(Adder)를 검증하기 위한 완벽한 '환경(Env)'이라는 거대한 기계를 조립했습니다. 하지만 이 기계는 스스로 굴러가지 않습니다. 누군가 전원 버튼을 누르고, "자, 지금부터 1번 대본(adder_seq)을 저 기계 안의 시퀀서에 넣고 돌려!"라고 명령을 내려야 합니다.

`ifndef ADDER_TEST_SV
`define ADDER_TEST_SV

// Extend from uvm_test
class adder_test extends uvm_test;

  // ============================================================================
  // [Block 1] Declare the Environment
  // ============================================================================
  // The grand container holding Agent and Scoreboard
  adder_env env;

  // Register to the UVM factory
  `uvm_component_utils(adder_test)

  // ============================================================================
  // [Block 2] Constructor
  // ============================================================================
  function new(string name = "adder_test", uvm_component parent);
    super.new(name, parent);
  endfunction

  // ============================================================================
  // [Block 3] Build Phase: Create the Environment!
  // ============================================================================
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    // Instantiate the environment using the factory
    env = adder_env::type_id::create("env", this);
  endfunction

  // ============================================================================
  // [Block 4] Run Phase: The Commander's Orders!
  // ============================================================================
  virtual task run_phase(uvm_phase phase);
    
    // 1. Declare the sequence (The script)
    adder_seq seq;
    
    // Create the sequence using the factory
    seq = adder_seq::type_id::create("seq");

    // 2. IMPORTANT: Raise objection to keep the simulation alive
    phase.raise_objection(this);

    `uvm_info("TEST", "Starting the adder sequence...", UVM_LOW)

    // 3. START the sequence on the specific sequencer inside the agent
    seq.start(env.agent.sqr);

    `uvm_info("TEST", "Finished the adder sequence.", UVM_LOW)

    // 4. IMPORTANT: Drop objection to allow the simulation to end
    phase.drop_objection(this);
    
  endtask

endclass

`endif

환경 세팅 (build_phase)

  • 가장 먼저 앞서 만든 거대한 adder_env 기계를 공장에서 찍어내어(create) 눈앞에 대령합니다.

대본 고르기 (run_phase의 seq 생성)

  • 여러 대본 중에서 오늘은 adder_seq(랜덤 값 10번 쏘기)를 실행하기로 마음먹고, 대본 책자(seq)를 공장에서 찍어냅니다.
  • 만약 내일 "오버플로우만 100번 테스트하는 대본"(adder_overflow_seq)을 새로 짰다면? Test 코드에서 이 줄의 이름만 살짝 바꿔주면 다른 테스트가 돌아갑니다!

★ UVM 최고의 권력: Objection (시뮬레이션 생사 여탈권)

  • phase.raise_objection(this);
    "나 지금부터 대본 시작할 거니까, 절대 시뮬레이션 끝내지 말고 기다려!" (손 번쩍 들기)

  • seq.start(env.agent.sqr);
    "자, 내가 고른 대본(seq)을, 환경(env) 안의 에이전트(agent) 안에 있는 시퀀서(sqr)에 꽂아서 당장 시작해!" (실제 실행)

  • phase.drop_objection(this);
    "대본 다 끝났다! 이제 더 볼일 없으니 시뮬레이션 종료해도 좋다!" (손 내리기)

하드웨어 영역

adder_if.sv

`ifndef ADDER_IF_SV
`define ADDER_IF_SV

// The interface acts as a bundle of wires connecting the DUT and the Testbench
interface adder_if(input logic clk, input logic rst_n);

  // ============================================================================
  // Physical signals connecting to the Adder DUT
  // ============================================================================
  // Inputs to the DUT
  logic [7:0] a;
  logic [7:0] b;

  // Output from the DUT
  logic [8:0] y;

endinterface

`endif
  1. "선 묶음(Bundle of Wires)"의 미학

    과거 Verilog에서는 모듈을 연결할 때 wire [7:0] a; wire [7:0] b; wire [8:0] y;를 일일이 선언하고 맵핑해야 했습니다.

    SystemVerilog의 interface는 이 선들을 하나의 굵은 케이블(단자대)로 묶어버립니다. 덕분에 나중에 신호가 수십 개인 AXI 버스를 연결할 때도, 포트 하나만 쓱 연결하면 끝나는 엄청난 편리함을 제공합니다.

  2. 드라이버와 모니터의 타겟 (vif.a, vif.b)

    우리가 Driver에서 vif.a <= req.a; 라고 썼던 것 기억하시죠?

    그 코드에서 vif가 가리키는 실제 도착지가 바로 이 인터페이스 안에 선언된 logic [7:0] a; 입니다. 소프트웨어 클래스 안에서 이 물리적인 선에 직접 전압(0과 1)을 인가할 수 있게 해주는 완벽한 브릿지 역할입니다.

  3. 시간의 기준점 (clk, rst_n)

    interface adder_if(input logic clk, input logic rst_n);

    인터페이스는 껍데기가 아니라 실제 시뮬레이션 시간을 공유하는 물리적 공간입니다. 그래서 최상위 테스트벤치(tb_top)에서 만들어진 진짜 클럭(clk)을 입력으로 받아서, 내부의 신호들이 이 클럭에 맞춰 춤출 수 있게 박자를 제공해 줍니다.

"인터페이스(Interface)와 가상 인터페이스(Virtual Interface)의 차이가 무엇인가요?"
이 질문에 이제 완벽하게 대답하실 수 있습니다.

Interface: 실제 하드웨어 모듈(DUT)과 연결되는 물리적인 구리선 묶음입니다. (지금 짜신 이 코드!)
Virtual Interface (vif): UVM이라는 '소프트웨어 클래스(동적 메모리)' 안에서, 저 멀리 있는 '물리적인 인터페이스(정적 메모리)'를 원격으로 조종하기 위해 들고 있는 포인터(리모컨)입니다.

adder.sv

`ifndef ADDER_SV
`define ADDER_SV

// The actual Design Under Test (DUT)
module adder (
  input  logic       clk,
  input  logic       rst_n,
  input  logic [7:0] a,
  input  logic [7:0] b,
  output logic [8:0] y
);

  // ============================================================================
  // Sequential Logic: Calculate a + b at every positive clock edge
  // ============================================================================
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      // Reset state: Clear the output
      y <= 9'd0;
    end else begin
      // Normal operation: Add inputs and register the output
      y <= a + b;
    end
  end

endmodule

`endif

최종 Testbench

tb_top.sv

`timescale 1ns/1ps

// Import the UVM library and macros
import uvm_pkg::*;
`include "uvm_macros.svh"

// Include all the files we created (Normally handled by a filelist or Makefile)
`include "adder_item.sv"
`include "adder_seq.sv"
`include "adder_driver.sv"
`include "adder_monitor.sv"
`include "adder_agent.sv"
`include "adder_scoreboard.sv"
`include "adder_env.sv"
`include "adder_test.sv"
`include "adder_if.sv"
`include "adder.sv"

module tb_top;

  // ============================================================================
  // [Block 1] Clock and Reset Generation
  // ============================================================================
  logic clk;
  logic rst_n;

  // Generate a 100MHz clock (Period = 10ns)
  initial begin
    clk = 0;
    forever #5 clk = ~clk; 
  end

  // Apply reset for the first 20ns
  initial begin
    rst_n = 0;
    #20 rst_n = 1;
  end

  // ============================================================================
  // [Block 2] Physical Connections (Breadboard)
  // ============================================================================
  // 1. Instantiate the physical Interface (The bundle of wires)
  adder_if vif(clk, rst_n);

  // 2. Instantiate the DUT and connect its pins to the Interface
  adder dut (
    .clk  (vif.clk),
    .rst_n(vif.rst_n),
    .a    (vif.a),
    .b    (vif.b),
    .y    (vif.y)
  );

  // ============================================================================
  // [Block 3] The UVM Bridge and Start Button!
  // ============================================================================
  initial begin
    
    // 1. Put the physical interface into the UVM Mailbox (config_db)
    // The Driver and Monitor will call get() to pick up this "vif"
    uvm_config_db#(virtual adder_if)::set(null, "uvm_test_top.env.agent.*", "vif", vif);

    // 2. Push the big START button!
    run_test("adder_test");
    
  end

  // ============================================================================
  // [Block 4] Waveform Dumping (Optional but crucial for RTL designers)
  // ============================================================================
  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb_top);
  end

endmodule
  1. 마법의 우체통 배달 (uvm_config_db::set)

    uvm_config_db#(virtual adder_if)::set(...)

    기억나시나요? Driver와 Monitor에서 get() 함수를 써서 가상 리모컨(vif)을 꺼내오려고 안달이 나 있었죠?

    그 리모컨을 가장 먼저 우체통에 넣어주는 사람이 바로 이 Top 모듈입니다. 내가 물리적으로 연결해 둔 진짜 단자대(vif)를 우체통(set)에 넣어주면, UVM 시스템 깊숙한 곳에 있는 일꾼들이 알아서 꺼내 갑니다.

  2. 경로 설정의 비밀 ("uvm_test_top.env.agent.*")

    이 리모컨을 아무나 가져가면 안 되겠죠? 그래서 이름표에 배송지 주소를 적어줍니다.

    "최상위 테스트(uvm_test_top) 밑에, 환경(env) 밑에, 에이전트(agent) 안에 있는 모든 부품(*)들만 이 리모컨을 가져갈 수 있어!"라는 뜻입니다.

  3. 가장 위대한 한 줄: run_test("adder_test");

    이 단 한 줄이 실행되는 순간, 시뮬레이션 시간 0초에 엄청난 일들이 연쇄적으로 폭발합니다.

    UVM 시스템이 깨어나고 ➔ adder_test를 만들고 ➔ env를 조립하고 ➔ agent, scoreboard를 찍어내고 ➔ driver, monitor에 선을 연결한 뒤 ➔ run_phase가 시작되면서 대본(seq)이 드라이버에게 상자를 마구 던지기 시작합니다.

profile
RTL, FPGA Engineer

0개의 댓글