2026. 4. 10. 20:02ㆍUVM
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의 핵심이다.