APB Environment

Seungyun Lee·2026년 2월 16일

UVM

목록 보기
7/14

1. Testbench 의 필수 역할

UVM 클래스(소프트웨어)들을 실행하려면, 바탕이 되는 물리적 하드웨어 모듈(module top)이 필요합니다. 이 Testbench는 다음 세 가지 역할을 합니다.

  • DUT(Aligner) 인스턴스화: 검증할 하드웨어 블록을 불러옵니다.
  • Clock & Reset 생성: 시뮬레이션에 필요한 주기적인 클럭 신호와 초기 리셋 신호를 발생시킵니다.
  • run_test() 함수 호출: 이 함수를 부르는 순간, 정지해 있던 UVM 시스템이 깨어나며 전체 검증이 시작됩니다.

2. Instantiate the UVM test

run_test()가 호출되면 UVM은 1) 테스트 클래스를 생성하고, 2) Phase들을 실행합니다. 이때 어떤 테스트(예: Register Test, Random Test 등)를 실행할지 결정하는 방식이 중요합니다.

하드코딩 (비권장): run_test("my_test")처럼 코드 안에 테스트 이름을 직접 적는 방식입니다.

시뮬레이터 인자 사용 (현업 표준): SystemVerilog 코드는 run_test()로 비워두고, 시뮬레이터(Cadence, Synopsys 등)를 실행할 때 터미널 명령어 인자로 +UVM_TESTNAME=테스트클래스이름을 넘겨주는 방식입니다. 코드를 수정하고 다시 컴파일할 필요 없이 원하는 테스트만 골라서 돌릴 수 있어 훨씬 유연합니다.

3. Run UVM Phase


UVM의 모든 구조적 블록들(Agent, Env 등)은 uvm_component라는 기본 클래스를 상속받습니다. 이 컴포넌트들은 시뮬레이션 시작부터 끝까지 정해진 순서의 9가지 Phase(단계)를 거칩니다.

핵심 특징: run_phase만 유일하게 시뮬레이션 시간(Time)을 소모하는 task이고, 나머지 모든 phase는 0초에 실행되는 function입니다.

가장 많이 쓰는 3대 Phase:

  • build_phase: 컴포넌트들(파란색 네모 상자들)을 메모리에 생성합니다. 이 단계가 지나면 환경의 구조가 고정되어 더 이상 부품을 추가할 수 없습니다.
  • connect_phase: 생성된 컴포넌트들 간의 포트와 선을 연결합니다.
  • run_phase: 실제 시뮬레이션이 진행되며 테스트 데이터가 오가는 메인 단계입니다.

4. TEST Class의 상속 구조

수많은 종류의 테스트를 만들어야 하는데, 매번 테스트 코드 안에 'Environment 생성 로직'을 중복해서 적는 것은 비효율적입니다.

모든 테스트는 기본적으로 UVM 패키지의 uvm_test를 상속받아야 합니다.

base_test 클래스 도입: uvm_test를 상속받는 중간 단계인 base_test를 하나 만듭니다. 오직 이 안에서만 Environment 인스턴스를 생성합니다.

이후 만드는 모든 세부 테스트들(Register Access Test, Random Test 등)은 이 base_test를 상속받기만 하면 자동으로 Environment 구조를 물려받게 됩니다.


Code Architecture

=========================================================================
 [하드웨어 영역 - 물리적 보드] 

 1️⃣ testbench.sv (최상위 실행 파일)
    │
    ├── ⚡ 전원 및 클럭 (Reset & Clock) 인가
    ├── 🖲️ DUT (검증 대상 칩 - Aligner) 올려놓기
    │
    └── 🚀 run_test() 실행!! ──────────┐ (소프트웨어 세계를 깨움)
===================================== │===============================
                                      ▼
 [소프트웨어/UVM 영역 - 객체 지향 클래스]

  [대본 보관 상자] (테스트 시나리오)           [장비 보관 상자] 검증 장비 세트
 2️⃣ cfs_algn_test_pkg.sv                  5️⃣ cfs_algn_pkg.sv
    │                                         │
    │  (공통 뼈대 대본)                          │  (종합 검증 장비)
    ├──3️⃣ cfs_algn_test_base.sv ────────▶ 6️⃣ cfs_algn_env.sv (생성/설치)
    │       │     └─ "무조건 env를 설치해라!"  (Agent, Scoreboard 등이 들어갈 곳)
    │       │
    │       ▼ (상속: extends)
    │  (실제 액션 대본)
    └──4️⃣ cfs_algn_test_reg_access.sv
                  └─ "base가 장비 설치했지? 
                      나는 레지스터를 공격할게!"
======================================================================

전체 흐름
1. 우리가 시뮬레이터를 켜면 1️⃣ testbench.sv가 돌아갑니다.
2. testbench.sv가 run_test()를 부르면서, "오늘은 4️⃣번 대본(reg_access)으로 실행해!" 라고 명령합니다.
3. 시스템은 2️⃣ 대본 상자(test_pkg)를 열어서 4️⃣번 대본을 찾습니다.
4. 그런데 4️⃣번 대본은 3️⃣ base 대본의 자식(상속)입니다. 그래서 부모인 3️⃣번 대본의 규칙을 먼저 따릅니다.
5. 3️⃣번 대본의 규칙에 따라, 5️⃣ 장비 상자(pkg)를 열어서 6️⃣ env 장비를 꺼내 테스트장에 쫙 설치합니다. (이 과정을 build_phase라고 합니다.)
6. 장비 설치가 끝나면, 다시 4️⃣번 대본으로 돌아와서 본격적인 레지스터 공격(시뮬레이션)을 시작합니다. (이 과정을 run_phase라고 합니다.)

1. 물리적 테스트 공간(하드웨어)

가장 밑바탕이 되는 물리적인 작업장입니다. 6개 파일 중 유일하게 class(소프트웨어)가 아닌 module(하드웨어)로 작성됩니다.

testbench.sv (테스트 벤치 최상위 모듈)

  • 역할: 작업장에 칩(Aligner DUT)을 올려두고, 전원(Reset)을 켜고, 심장 박동(Clock)을 뛰게 하는 물리적인 보드입니다.
  • 핵심 동작: 하드웨어 세팅이 끝나면 run_test(); 라는 명령어를 외쳐서 잠자고 있던 UVM 소프트웨어(검증 시스템)를 깨우는 방아쇠 역할을 합니다.
`include "cfs_algn_test_pkg.sv"  // 대본상자 가져오기

module testbench();
  
  import uvm_pkg::*;  // UVM library(기본제공함수)불러오기
  import cfs_algn_test_pkg::*;  // 대본상자 안의 내용물 사용가능하게 불러오기
  
  //CLock signal
  reg clk;
  
  //Reset signal - active low (0일때 reset)
  reg reset_n;
  
  //Clock generator
  initial begin
    clk = 0;
    
    forever begin
      //Generate an 100MHz clock
      clk = #5ns ~clk;
    end
  end
  
  //Initial reset generator
  initial begin
    reset_n = 1;
    
    #6ns;
    
    reset_n = 0;
    
    #30ns;
    reset_n = 1;
  end
  
  initial begin
    $dumpfile("dump.vcd");  // waveform
    $dumpvars;
    
    //Start UVM test and phases
    run_test("");  // UVM system 실행, 괄호가 빈 이유는 시뮬레이터 실행
  end             // 할때 터미널 창에서 수동으로 입력 받기 위함
  
  //Instantiate the DUT  // DUT 장착
  cfs_aligner dut(
    .clk(    clk),
    .reset_n(reset_n)
  );
  
  
endmodule

2. 검증 장비 세트 (Environment)

테스트장에 설치할 첨단 장비들(센서, 모니터, 채점기 등)을 정의하는 곳입니다.
'종합 검증 장비 세트(빈 상자)'를 만드는 6️⃣번 파일

cfs_algn_env.sv (환경 클래스)

  • 역할: 검증에 필요한 장비들을 하나로 모아둔 '종합 검증 장비 세트'입니다.
  • 내용: 지금은 파일이 비어있지만, 나중에는 이 안에 Agent(입력기/출력기), Scoreboard(자동 채점기), Reference Model(정답지) 같은 부품들이 모두 조립되어 들어갈 것입니다.
`ifndef CFS_ALGN_ENV_SV    // ifndef(If Not Defined): "만약 CFS_ALGN_ENV_SV라는 이름표가
  `define CFS_ALGN_ENV_SV  // 아직 안 붙어있다면, 지금 붙이고(define) 코드를 읽어라

  class cfs_algn_env extends uvm_env; // 우리가 설계할 검증 환경의 이름을 cfs_algn_env로 지음
                                      // uvm_env(환경용 표준상자) 상속

    `uvm_component_utils(cfs_algn_env) // UVM Factory system에 등록
    
    function new(string name = "", uvm_component parent);
      super.new(name, parent);  //부모 클래스(uvm_env)의 생성자에게 그 이름과 족보 정보를 그대로 전달
    endfunction
    
  endclass

`endif

아직 Agent(데이터 입력기)나 Scoreboard(자동 채점기) 같은 진짜 장비들을 하나도 넣지 않은 '텅 빈 껍데기 상자'를 방금 막 종이접기로 완성한 상태

  • C언어나 C++에서 아주 흔하게 쓰는 매크로 기법입니다.
  • 파일들이 서로를 불러오다(include) 보면, 실수로 이 env 파일을 두 번, 세 번 중복해서 불러오는 일이 생길 수 있습니다.
  • ifndef(If Not Defined): "만약 CFS_ALGN_ENV_SV라는 이름표가 아직 안 붙어있다면, 지금 붙이고(define) 코드를 읽어라!"라는 뜻입니다. 덕분에 컴파일러가 이 파일을 단 한 번만 읽도록 완벽하게 보호해 줍니다.

생성자(constructor)

  • 객체 지향 프로그래밍(OOP)에서 클래스가 처음 메모리에 만들어질 때(태어날 때) 가장 먼저 실행되는 초기화 함수가 바로 new입니다.
  • UVM의 모든 컴포넌트는 태어날 때 '자신의 이름(name)''자신을 만든 부모(parent)'가 누구인지 족보를 무조건 밝혀야 합니다.
  • super.new(name, parent);: 부모 클래스(uvm_env)의 생성자에게 그 이름과 족보 정보를 그대로 전달해서, UVM 트리의 계층 구조가 정상적으로 연결되도록 합니다.

cfs_algn_pkg.sv (환경 패키지)

  • 역할: 위에서 만든 장비 세트(env.sv)를 담아두는 '보관 상자(폴더)'입니다.
  • 이유: 부품이 많아지면 이름이 섞일 수 있으므로(Name clash), package라는 상자에 깔끔하게 담아서 포장해 두는 소프트웨어적인 정리 도구입니다.
`ifndef CFS_ALGN_PKG_SV     //중복 방지 보호막 (Include Guards)
  `define CFS_ALGN_PKG_SV

  `include "uvm_macros.svh"  //UVM에서 제공하는 유용한 단축키(매크로)들을 
                            // 이 파일 안에서 쓸 수 있게 가져옵니다.

  package cfs_algn_pkg;  //패키지 선언 (Package Declaration)
     // 패키지 내부
     
    import uvm_pkg::*;  // 이 상자(cfs_algn_pkg) 안에서 UVM의 기본 클래스(uvm_env 등)를
                        // 인식할 수 있도록 UVM 라이브러리를 통째로 수입(Import)합니다.

    `include "cfs_algn_env.sv"  // 방금 전 우리가 살펴봤던 6️⃣번 파일(빈 환경 클래스)의 내용물을
                                // 이 패키지 상자 안으로 물리적으로 복사해 넣습니다.
  endpackage

`endif

3. 테스트 시나리오 (Tests)

장비 세팅이 끝났으니, 이제 "어떤 방식"으로 칩을 괴롭힐지 시나리오 대본을 작성하는 곳입니다.

cfs_algn_test_pkg.sv (테스트 패키지)

앞서 만든 공통 뼈대 대본(base)과 실제 액션 대본(reg_access)을 이 상자 하나에 깔끔하게 포장해 두는 역할을 합니다. 이렇게 해두면 최상위 하드웨어인 testbench.sv에서는 이 상자 하나만 import 하면 모든 테스트 대본을 사용할 수 있게 됩니다.

ifndef CFS_ALGN_TEST_PKG_SV
  `define CFS_ALGN_TEST_PKG_SV

  `include "uvm_macros.svh"  //외부 장비 상자 연결하기 (Dependency)
  `include "cfs_algn_pkg.sv"  // 장비 상자 파일 불러오기

  package cfs_algn_test_pkg; // 이 아래에 있는 클래스들은 모두 test_pkg라는 이름의 상자에 소속됨
    import uvm_pkg::*;
    import cfs_algn_pkg::*;

    `include "cfs_algn_test_base.sv"  // 대본들을 순서대로 상자에 넣기 (Compilation Order)
    `include "cfs_algn_test_reg_access.sv"

  endpackage

`endif

package cfs_algn_test_pkg;: 이제부터 이 아래에 있는 클래스들은 모두 test_pkg라는 이름의 상자에 소속됨을 선언합니다.

import cfs_algn_pkg::*;: 위에서 불러온 장비 상자 안의 내용물(클래스들)을 이 대본 상자 안에서도 자유롭게 쓸 수 있도록 수입(Import)해 줍니다.

cfs_algn_test_base.sv (기본 테스트 클래스)

  • 역할: 모든 테스트 시나리오의 '공통 뼈대'입니다.
  • 내용: "어떤 테스트를 하든 일단 검증 장비 세트(env)는 무조건 설치하고 시작하자!"라는 공통 규칙을 여기에 적어둡니다. (이 뼈대를 만들어두면 나중에 다른 테스트를 짤 때 장비 설치 코드를 매번 또 적을 필요가 없습니다.)
`ifndef CFS_ALGN_TEST_BASE_SV
  `define CFS_ALGN_TEST_BASE_SV

  class cfs_algn_test_base extends uvm_test;  //이 클래스가 UVM의 최고 권위자인 '테스트 대본'임을 선언 
    
    //Environment instance
    cfs_algn_env env;    // 포인터 선언, env라는 이름표를 가진 장비 세트를 조종할거야

    `uvm_component_utils(cfs_algn_test_base) //UVM factory 등록
    
    function new(string name = "", uvm_component parent);
      super.new(name, parent);  // 이름과, 족보를 물려받아 초기화
                                // 내가 태어나기(new) 전에, 내 부모인 uvm_test 당신 먼저 기본 세팅을 완료해 주세요!"
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);  // 시뮬레이션 시간이 0초일때 가장 먼저 실행되는 단계
                                 // 테스트벤치(testbench.sv)에서 run_test()를 외치면, 
                                 // UVM 시스템은 가장 먼저 이 build_phase 함수를 찾아내서 실행시킵니다.
      
      env = cfs_algn_env::type_id::create("env", this);
    endfunction
    
  endclass

`endif
  • 하드웨어 설계에서 하위 모듈을 불러올 때는 cfs_aligner dut (.clk(clk)); 처럼 썼지만, UVM에서는 절대 그렇게 쓰지 않습니다. 대신 type_id::create라는 마법의 주문을 씁니다.
  • "env": 이 장비 세트의 고유한 이름입니다.
  • this: 이 장비 세트를 만든 부모(Parent)가 누구인지 알려주는 키워드입니다. 여기서는 this(나 자신, 즉 cfs_algn_test_base)가 부모가 됩니다.
  • 이 한 줄이 실행되는 순간, 아까 1번에서 선언만 해두었던 빈 포인터 자리에 실제 장비 세트(객체)가 메모리상에 쾅! 하고 생성되어 연결됩니다.

cfs_algn_test_reg_access.sv (실제 테스트 클래스)

  • 역할: 구체적인 '액션 시나리오'입니다. 방금 만든 base.sv의 뼈대를 그대로 물려받아(상속, extends), 추가적인 행동만 지시합니다.
  • 내용: "장비 설치는 Base가 해놨으니, 나는 100나노초 동안 레지스터에 데이터를 썼다 지웠다 해볼게!" 라고 지시하고 시뮬레이션을 끝냅니다. (objection 메커니즘 사용)
`ifndef CFS_ALGN_TEST_REG_ACCESS_SV
  `define CFS_ALGN_TEST_REG_ACCESS_SV

  class cfs_algn_test_reg_access extends cfs_algn_test_base;  // base 상속

    `uvm_component_utils(cfs_algn_test_reg_access)
    
    function new(string name = "", uvm_component parent);
      super.new(name, parent);
    endfunction
    
    virtual task run_phase(uvm_phase phase);
    
      phase.raise_objection(this, "TEST_DONE"); // 1. 발언권 획득 (끝내지 마!)
      
      #(100ns); // 2. 실제 테스트 시간 소모
      
      `uvm_info("DEBUG", "this is the end of the test", UVM_LOW) //3.메시지 출력
      
      phase.drop_objection(this, "TEST_DONE");  // 4. 발언권 반납 (이제 끝내도 돼!)
    endtask
    
  endclass

`endif
  • extends cfs_algn_test_base: 부모를 uvm_test가 아니라, 우리가 직전에 만들었던 3️⃣번 공통 뼈대 대본(cfs_algn_test_base)으로 지정했습니다!
  • 마법의 결과: 부모(base)가 이미 build_phase에서 장비 세트(env)를 설치해 두었기 때문에, 자식인 이 클래스는 아무것도 안 해도 자동으로 완벽하게 세팅된 검증 장비들을 물려받고 시작합니다. 앞으로 수백 개의 테스트를 만들 때 코드가 엄청나게 짧아지는 이유입니다.

virtual task run_phase(uvm_phase phase);

  • 앞서 보셨던 build_phase는 시뮬레이션 시간 0초에 눈 깜짝할 새 장비를 세팅하는 함수(function)였습니다.
  • 반면, run_phase는 UVM의 9개 Phase 중 유일하게 태스크(task)입니다. 태스크 안에서는 물리적인 시간(Time)이 흐를 수 있습니다. 실제로 클럭이 뛰고 데이터가 오가는 메인 무대입니다.

Objection

  • raise_objection: "저 지금부터 레지스터 공격 시작할 거니까, 절대 시뮬레이션 끄지 마세요!" 하고 시스템에 손을 번쩍 듭니다.
  • #(100ns): 손을 든 상태로 100나노초 동안 열심히 테스트를 진행합니다. (지금은 비어있지만 나중엔 여기에 레지스터에 데이터를 쏘는 코드가 들어갑니다.)
  • drop_objection: "저 테스트 끝났습니다. 이제 시뮬레이션 꺼도 됩니다." 하고 손을 내립니다.
profile
RTL, FPGA Engineer

0개의 댓글