[UVM] TLM 완전 정리 | uvm_analysis_port, analysis_imp, FIFO - 컴포넌트 간 통신 초보자 가이드

2026. 4. 10. 20:02UVM

반응형

UVM을 공부하다 보면 Monitor가 Scoreboard에 데이터를 보내야 하는데, 어떻게 연결하지? 라는 질문이 생긴다. 직접 핸들을 넘겨서 함수를 호출하면 되지 않나 싶지만, 그렇게 하면 컴포넌트 간 결합도가 높아져서 재사용이 어려워진다. 이 문제를 우아하게 해결하는 것이 TLM(Transaction Level Modeling)이다. 이 글은 TLM을 처음 접하는 사람을 위해 개념부터 실전 코드까지 최대한 쉽게 설명한다.


1. TLM이 뭔데? — 비유로 이해하기

TLM을 한 마디로 설명하면 "컴포넌트 간 데이터를 함수 호출처럼 주고받는 통신 방식"이다.

신호 레벨(RTL)에서는 실제 wire에 0/1을 올리며 통신한다. 하지만 검증 환경에서는 그 수준까지 신경 쓸 필요 없이, 트랜잭션(패킷) 단위로 주고받으면 충분하다. TLM은 이 트랜잭션 전달을 위한 표준화된 인터페이스다.

📬 비유: 우체통과 집배원

  • Port (발신자 쪽) = 편지를 넣는 입구. "나 데이터 보낼게"라고 선언하는 쪽.
  • Export / Imp (수신자 쪽) = 실제 우체통. 데이터를 받아서 처리하는 구현이 여기 있음.
  • connect() = 특정 집배원(port)을 특정 우체통(imp)에 연결하는 행위.

한 번 연결해두면 발신자는 수신자가 누구인지 몰라도 데이터를 전달할 수 있고, 수신자도 발신자가 바뀌어도 코드를 고칠 필요가 없다. 결합도가 낮아지는 것이 핵심이다.

2. TLM 포트 종류 한눈에 보기

포트 이름 방향 특징 주로 쓰는 곳
uvm_blocking_put_port 송신 put()이 끝날 때까지 블로킹 1:1 통신, 속도 동기화 필요할 때
uvm_nonblocking_put_port 송신 즉시 반환, try_put()으로 확인 버퍼 오버플로 방지가 필요할 때
uvm_analysis_port 송신 (1:N) write() 한 번으로 여러 수신자에게 브로드캐스트 Monitor → Scoreboard / Coverage
uvm_analysis_imp 수신 analysis_port가 보낸 write()를 구현 Scoreboard, Coverage Collector
uvm_tlm_fifo 버퍼 내부에 큐(FIFO)를 갖는 중간 버퍼 생산자-소비자 속도 차이 완충

실제 UVM 검증 환경에서 가장 많이 쓰는 것은 단연 uvm_analysis_port + uvm_analysis_imp 조합이다. Monitor가 캡처한 트랜잭션을 Scoreboard에 보낼 때 이 패턴을 쓴다.

3. 가장 많이 쓰는 패턴 — Monitor → Scoreboard

아래가 UVM에서 가장 흔히 보게 될 TLM 패턴이다. Monitor가 버스에서 트랜잭션을 캡처하고, analysis_port를 통해 Scoreboard에 전달한다.

3-1. Monitor 쪽 (송신): uvm_analysis_port 선언

class my_monitor extends uvm_monitor;
  `uvm_component_utils(my_monitor)

  // analysis_port 선언 — 트랜잭션 타입을 제네릭으로 지정
  uvm_analysis_port #(my_transaction) ap;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap", this);  // 포트 생성
  endfunction

  task run_phase(uvm_phase phase);
    my_transaction tr;
    forever begin
      // 인터페이스에서 트랜잭션 캡처 (간략화)
      tr = my_transaction::type_id::create("tr");
      // ... 신호 샘플링 ...

      ap.write(tr);  // ← 이 한 줄로 연결된 모든 수신자에게 전송
    end
  endtask
endclass

ap.write(tr) 한 줄이 핵심이다. Monitor는 누가 받는지 전혀 신경 쓸 필요 없이 write()만 호출하면 된다.

3-2. Scoreboard 쪽 (수신): uvm_analysis_imp 선언

class my_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(my_scoreboard)

  // analysis_imp 선언 — write() 함수를 이 클래스 안에서 구현
  uvm_analysis_imp #(my_transaction, my_scoreboard) imp;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    imp = new("imp", this);  // imp 생성
  endfunction

  // Monitor가 ap.write(tr)를 호출하면 여기가 자동으로 실행됨
  function void write(my_transaction tr);
    if (tr.data !== expected_data)
      `uvm_error("SCB", $sformatf("데이터 불일치! 기대:%0h 실제:%0h", expected_data, tr.data))
    else
      `uvm_info("SCB", "Pass!", UVM_LOW)
  endfunction
endclass

포인트: uvm_analysis_imp #(트랜잭션_타입, write를_구현한_클래스)처럼 두 개의 제네릭 파라미터를 넣는다. 그리고 반드시 이 클래스 안에 function void write(T tr)를 구현해야 한다.

3-3. Environment: connect_phase에서 연결

class my_env extends uvm_env;
  my_agent      agent;
  my_scoreboard scb;

  function void connect_phase(uvm_phase phase);
    // Monitor의 ap를 Scoreboard의 imp에 연결
    agent.monitor.ap.connect(scb.imp);
    //                ↑ port    ↑ imp
    // 이제 ap.write(tr) 호출 시 scb.write(tr)가 실행됨
  endfunction
endclass

딱 한 줄 — ap.connect(imp). 이게 TLM 연결의 전부다. Monitor도, Scoreboard도 서로를 전혀 모른다. 연결은 오직 Environment가 담당한다.

4. 1:N 브로드캐스트 — analysis_port의 진짜 강점

analysis_port의 가장 강력한 특징은 하나의 포트에 여러 수신자를 동시에 연결할 수 있다는 점이다. Monitor 코드를 단 한 줄도 바꾸지 않고, Scoreboard와 Coverage Collector 둘 다에 트랜잭션을 보낼 수 있다.

// Environment의 connect_phase
function void connect_phase(uvm_phase phase);
  // 같은 ap에 두 개의 수신자 연결
  agent.monitor.ap.connect(scb.imp);   // Scoreboard
  agent.monitor.ap.connect(cov.imp);   // Coverage Collector

  // ap.write(tr) 한 번 호출 →
  //   scb.write(tr) 실행
  //   cov.write(tr) 실행  (동시에, 순서대로)
endfunction

Monitor는 자신이 Scoreboard에 보내는지, Coverage에 보내는지 전혀 모른다. 연결 구성은 전적으로 Environment의 connect_phase에서 결정한다. 나중에 Coverage Collector를 추가하더라도 Monitor 코드는 전혀 건드릴 필요가 없다. 이것이 TLM을 쓰는 이유다.

5. 여러 채널을 한 Scoreboard에서 받기 — uvm_analysis_imp_decl

Scoreboard가 AXI Monitor와 APB Monitor 양쪽에서 트랜잭션을 받아야 하는 경우를 생각해보자. 한 클래스에 write() 함수가 두 개 있으면 컴파일 에러가 난다. 이때 `uvm_analysis_imp_decl 매크로로 해결한다.

// 파일 상단 또는 패키지에서 선언 (클래스 밖에서!)
// 서픽스(_axi, _apb)를 붙여서 write 함수 이름을 각각 다르게 만듦
`uvm_analysis_imp_decl(_axi)
`uvm_analysis_imp_decl(_apb)

class my_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(my_scoreboard)

  // 각각 다른 타입의 imp
  uvm_analysis_imp_axi #(axi_transaction, my_scoreboard) axi_imp;
  uvm_analysis_imp_apb #(apb_transaction, my_scoreboard) apb_imp;

  function void build_phase(uvm_phase phase);
    axi_imp = new("axi_imp", this);
    apb_imp = new("apb_imp", this);
  endfunction

  // AXI Monitor가 write() 했을 때 호출됨
  function void write_axi(axi_transaction tr);
    `uvm_info("SCB", $sformatf("AXI tr 수신: addr=%0h", tr.addr), UVM_LOW)
    // AXI 검증 로직
  endfunction

  // APB Monitor가 write() 했을 때 호출됨
  function void write_apb(apb_transaction tr);
    `uvm_info("SCB", $sformatf("APB tr 수신: addr=%0h", tr.addr), UVM_LOW)
    // APB 검증 로직
  endfunction
endclass
// Environment의 connect_phase
function void connect_phase(uvm_phase phase);
  axi_agent.monitor.ap.connect(scb.axi_imp);  // AXI → axi_imp → write_axi()
  apb_agent.monitor.ap.connect(scb.apb_imp);  // APB → apb_imp → write_apb()
endfunction

`uvm_analysis_imp_decl(_axi)는 내부적으로 uvm_analysis_imp_axi라는 새 클래스를 생성하고, write() 호출을 write_axi()로 매핑한다. 서픽스만 다르게 지정하면 채널이 몇 개든 깔끔하게 분리할 수 있다.

6. uvm_tlm_fifo — 속도 차이를 완충하는 버퍼

생산자(Producer)가 데이터를 빠르게 쏘는데 소비자(Consumer)가 처리가 느릴 때, 중간에 FIFO 버퍼를 두면 서로 독립적으로 동작할 수 있다. UVM은 이를 위해 uvm_tlm_fifo를 제공한다.

class my_env extends uvm_env;
  my_producer  prod;
  my_consumer  cons;
  uvm_tlm_fifo #(my_transaction) fifo;  // 버퍼 선언

  function void build_phase(uvm_phase phase);
    prod = my_producer::type_id::create("prod", this);
    cons = my_consumer::type_id::create("cons", this);
    fifo = new("fifo", this, 16);  // 크기 16짜리 FIFO 생성
  endfunction

  function void connect_phase(uvm_phase phase);
    prod.put_port.connect(fifo.put_export);  // 생산자 → FIFO
    cons.get_port.connect(fifo.get_export);  // FIFO → 소비자
  endfunction
endclass
// 생산자 — 데이터를 FIFO에 넣음
task run_phase(uvm_phase phase);
  my_transaction tr;
  repeat(20) begin
    tr = my_transaction::type_id::create("tr");
    put_port.put(tr);  // FIFO가 꽉 차면 자동으로 대기
  end
endtask

// 소비자 — FIFO에서 꺼내서 처리
task run_phase(uvm_phase phase);
  my_transaction tr;
  forever begin
    get_port.get(tr);  // FIFO가 비어 있으면 자동으로 대기
    // ... tr 처리 ...
  end
endtask

put()과 get() 모두 blocking 방식이라, FIFO가 꽉 찼을 때 put()은 자동으로 대기하고 FIFO가 비었을 때 get()은 자동으로 대기한다. 명시적인 핸드쉐이크 없이도 안전한 통신이 가능하다.

7. Sequencer ↔ Driver 내장 TLM — seq_item_port

사실 Sequencer와 Driver 사이도 TLM으로 연결된다. UVM이 내부적으로 이미 구현해 뒀기 때문에 직접 구현할 필요는 없지만, 구조를 알면 동작 이해에 큰 도움이 된다.

컴포넌트 포트/익스포트 역할
Driver seq_item_port Sequencer에게 "다음 item 줘" 요청하는 쪽
Sequencer seq_item_export 현재 실행 중인 Sequence에서 item을 꺼내 Driver에 전달
// Agent의 connect_phase — 딱 한 줄
function void connect_phase(uvm_phase phase);
  driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction

// Driver의 run_phase — 이 패턴이 UVM 표준
task run_phase(uvm_phase phase);
  forever begin
    seq_item_port.get_next_item(req);  // Sequencer에서 item 가져옴 (blocking)
    
    // DUT 인터페이스에 req 내용을 구동
    vif.addr  <= req.addr;
    vif.data  <= req.data;
    vif.valid <= 1;
    @(posedge vif.clk);
    
    seq_item_port.item_done();  // "처리 완료" → Sequencer에 알림
  end
endtask

Driver는 get_next_item()으로 item을 요청하고, Sequencer는 현재 실행 중인 Sequence의 body()에서 item을 꺼내 전달한다. 처리가 끝나면 item_done()으로 Sequencer에 알린다. 이 흐름 전체가 TLM 위에서 동작한다.

8. 자주 하는 실수 3가지

실수 1: build_phase에서 포트/imp new()를 빠뜨림

// ❌ 잘못된 예 — ap 선언만 하고 new() 안 함
uvm_analysis_port #(my_transaction) ap;
// run_phase에서 ap.write(tr) 호출 시 null pointer 에러

// ✅ 올바른 예
function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  ap = new("ap", this);  // 반드시 build_phase에서 생성
endfunction

실수 2: connect_phase가 아닌 build_phase에서 connect() 호출

// ❌ 잘못된 예 — build_phase는 Top-Down이라 하위 컴포넌트가
//    아직 생성 중일 수 있음
function void build_phase(uvm_phase phase);
  agent.monitor.ap.connect(scb.imp);  // 에러 또는 예상치 못한 동작

// ✅ 올바른 예 — connect_phase(Bottom-Up)에서 연결
function void connect_phase(uvm_phase phase);
  agent.monitor.ap.connect(scb.imp);  // 모든 컴포넌트가 이미 생성된 후

실수 3: uvm_analysis_imp_decl을 클래스 안에서 선언

// ❌ 잘못된 예 — 클래스 내부에서 선언하면 컴파일 에러
class my_scoreboard extends uvm_scoreboard;
  `uvm_analysis_imp_decl(_axi)  // 여기 아님!

// ✅ 올바른 예 — 클래스 밖, 패키지 레벨에서 선언
`uvm_analysis_imp_decl(_axi)
`uvm_analysis_imp_decl(_apb)

class my_scoreboard extends uvm_scoreboard;
  // 이제 uvm_analysis_imp_axi, uvm_analysis_imp_apb 사용 가능

9. 전체 TLM 연결 구조 한눈에 보기

┌──────────────────────────────────────────────────────────┐
│                       Environment                         │
│                                                           │
│  ┌───────────┐  seq_item_port                             │
│  │  Driver   │──────────────────► seq_item_export         │
│  └───────────┘                   ┌──────────────┐        │
│                                  │  Sequencer   │        │
│                                  └──────────────┘        │
│                                                           │
│  ┌───────────┐                                            │
│  │  Monitor  │──ap.write()──►─────────────┐              │
│  └───────────┘               │            │              │
│                        ┌─────▼──────┐ ┌──▼──────────┐   │
│                        │Scoreboard  │ │  Coverage   │   │
│                        │  .imp      │ │  .imp       │   │
│                        └────────────┘ └─────────────┘   │
│                                                           │
│  connect_phase 연결:                                      │
│    driver.seq_item_port  .connect( sequencer.seq_item_export )│
│    monitor.ap            .connect( scb.imp )              │
│    monitor.ap            .connect( cov.imp )  ← 1:N       │
└──────────────────────────────────────────────────────────┘

10. 핵심 요약

개념 한 줄 요약
TLM 트랜잭션 단위로 컴포넌트 간 통신하는 표준 인터페이스. 결합도를 낮춘다.
uvm_analysis_port Monitor에 선언. ap.write(tr) 한 번으로 N개 수신자에게 브로드캐스트.
uvm_analysis_imp Scoreboard/Coverage에 선언. write(tr) 함수를 이 클래스 안에서 구현.
ap.connect(imp) connect_phase에서 포트와 imp를 연결하는 딱 한 줄.
uvm_analysis_imp_decl 한 클래스가 여러 채널을 받을 때 서픽스로 write 함수 이름을 구분.
uvm_tlm_fifo 생산자-소비자 속도 차이를 완충하는 내장 FIFO 버퍼.
seq_item_port Driver↔Sequencer 내장 TLM. get_next_item() / item_done() 패턴.

한 줄 요약: Monitor는 ap.write()로 던지고, Scoreboard는 write()로 받는다. connect_phase에서 ap.connect(imp) 한 줄로 연결한다. 이게 UVM TLM의 핵심이다.