시리즈: ... · ref_model/scoreboard · tb_top · agent_cfg · agent · env · base_test
지금까지 본 모든 부품(인터페이스, DUT, UVM 환경)을
실제로 연결하고 클록을 돌려서 시뮬레이션을 시작하는 "조립 공장"
driver, monitor, scoreboard, ref_model이 어디서 시작되는지가 여기다.
┌─────────────────────── tb_top ────────────────────────┐
│ │
│ ① 클록/리셋 생성 │
│ clk (100MHz), rst (10사이클 유지) │
│ │
│ ② 인터페이스 인스턴스 │
│ axi4_if axi(.clk, .rst) │
│ ↕ (신호 연결) │
│ ③ DUT 인스턴스 │
│ axi_ram dut(.s_axi_awid(axi.awid), ...) │
│ │
│ ④ UVM에 인터페이스 전달 + 테스트 시작 │
│ config_db::set(vif) → run_test() │
│ │
│ ⑤ 파형 덤프 │
└───────────────────────────────────────────────────────┘
`ifdef DUT_V2
`define DUT_MODULE axi_ram_v2
`else
`define DUT_MODULE axi_ram
`endif
이게 "v1에서 fail, v2에서 pass"를 가능하게 하는 핵심이다.
컴파일 옵션에 따라 DUT가 바뀜:
기본: axi_ram (WRAP 버그 있는 원본)
+define+DUT_V2: axi_ram_v2 (WRAP 수정 + outstanding 지원)
두 DUT의 포트 목록이 완전히 동일 → 모듈 이름만 바뀜
→ 같은 테스트벤치로 두 버전 검증 가능!
v1에 돌리면 → 스코어보드가 WRAP 미스매치 잡음 → FAIL
v2에 돌리면 → 수정됐으니 → PASS
→ "버그가 있으면 FAIL, 없으면 PASS" 증명 (테스트 신뢰성)
module tb_top;
import uvm_pkg::*;
`include "uvm_macros.svh"
import axi4_pkg::*;
uvm_pkg → UVM 클래스들 (uvm_test, uvm_config_db 등)
uvm_macros.svh → `uvm_info, `uvm_error 같은 매크로
axi4_pkg → 우리가 만든 것들
(axi4_seq_item, AXI_DATA_WIDTH, axi_burst_e 등)
localparam int DATA_WIDTH = 32;
localparam int ADDR_WIDTH = 16;
localparam int STRB_WIDTH = DATA_WIDTH/8; // = 4
localparam int ID_WIDTH = 8;
주석: "must match axi4_pkg + the DUT instance below"
→ 이 값이 패키지, 인터페이스, DUT 세 곳에서 일치해야 함
→ 안 맞으면 신호 폭이 어긋나서 데이터 손실!
localparam = 지역 상수 (외부에서 못 바꿈) → 실수 방지
logic clk;
initial clk = 0;
always #5 clk = ~clk; // 100 MHz
initial clk = 0; → 시작할 때 0
always #5 clk = ~clk; → 5ns마다 반전(toggle)
5ns HIGH + 5ns LOW = 10ns 주기 = 100MHz
1초 / 10ns = 100,000,000 = 100MHz
┌──┐ ┌──┐ ┌──┐
─────┘ └──┘ └──┘ └──
5ns 10ns 15ns 20ns
logic rst;
initial begin
rst = 1'b1; // 리셋 ON
repeat (10) @(posedge clk); // 10 클록 유지
rst = 1'b0; // 리셋 OFF (해제)
end
┌────────────────┐
──┘ └──────────
0 10클록(100ns)
왜 10클록? DUT 내부 모든 FF/상태가 확실히 초기화되도록
이게 driver/monitor에서 봤던 @(negedge vif.rst)가 기다리던 그 신호다. 100ns 시점에 rst가 1→0으로 떨어지면 드라이버와 모니터가 깨어나 동작 시작.
axi4_if #(
.DATA_WIDTH(DATA_WIDTH),
.ADDR_WIDTH(ADDR_WIDTH),
.STRB_WIDTH(STRB_WIDTH),
.ID_WIDTH(ID_WIDTH)
) axi (
.clk(clk),
.rst(rst)
);
axi4_if를 "axi"라는 이름으로 실체화
.clk(clk), .rst(rst) → 위에서 만든 클록/리셋 주입
→ 인터페이스가 "clk, rst를 포트로 받는다"던 부분이 여기서 실제 주입됨!
→ 이제 axi.awvalid, axi.wdata 등 모든 AXI 신호가 존재
드라이버가 vif.master_cb로 흔들고, 모니터가 vif.monitor_cb로 관찰할 대상
`DUT_MODULE #(
.DATA_WIDTH(DATA_WIDTH), ...
) dut (
.clk(clk),
.rst(rst),
.s_axi_awid(axi.awid),
.s_axi_awaddr(axi.awaddr),
...
);
`DUT_MODULE → axi_ram (또는 axi_ram_v2)로 컴파일 시점에 치환
신호 연결:
.s_axi_awid(axi.awid)
// ↑ DUT 포트 ↑ 인터페이스 신호
DUT는 plain Verilog라 개별 신호로 직접 연결한다 (interface를 못 받는 순수 모듈).
인터페이스 axi DUT
┌──────────────┐ ┌──────────────┐
│ axi.awvalid │─────────────→│ s_axi_awvalid│
│ axi.awaddr │─────────────→│ s_axi_awaddr │
│ axi.awready │←─────────────│ s_axi_awready│
│ axi.rdata │←─────────────│ s_axi_rdata │
└──────────────┘ └──────────────┘
방향은 인터페이스 신호가 결정:
드라이버가 axi.awvalid 구동 → DUT의 s_axi_awvalid로 전달
DUT가 s_axi_awready 구동 → axi.awready로 → 드라이버가 읽음
initial begin
uvm_config_db#(virtual axi4_if)::set(null, "*", "vif", axi);
run_test("axi4_base_test");
end
이 두 줄이 하드웨어 세계와 UVM 세계를 연결하는 다리다.
uvm_config_db#(virtual axi4_if)::set(null, "*", "vif", axi);
// │ │ │ │
// │ │ │ └ 넘길 값 (실제 인터페이스)
// │ │ └ 키 이름 "vif"
// │ └ 경로 "*" (모든 컴포넌트)
// └ 시작점 null (전역)
"axi 인터페이스를 'vif'라는 이름으로 UVM 전체에 등록"
→ driver/monitor의 build_phase에서:
uvm_config_db#(...)::get(this, "", "vif", ...)
가 여기서 set한 걸 꺼내는 것!
set(여기서 넣음) ─────→ get(driver/monitor에서 꺼냄)
run_test("axi4_base_test");
UVM 테스트를 시작하는 함수
"axi4_base_test" → 기본 테스트 이름
→ 이 테스트가 env를 만들고 → agent → driver/monitor/sequencer
→ 전체 UVM 계층이 여기서 생성됨!
override 방법:
+UVM_TESTNAME=axi4_wrap_test (커맨드라인)
→ 코드 수정 없이 다른 테스트 실행
run_test가 하는 일:
1. +UVM_TESTNAME 확인 (없으면 인자값 사용)
2. 그 이름의 test 클래스를 factory로 생성
3. build → connect → run → ... phase 순서로 실행
4. 모든 phase 끝나면 시뮬레이션 종료
initial begin
$dumpfile("waves.vcd");
$dumpvars;
end
$dumpfile → 파형 저장할 파일 이름
$dumpvars → 모든 신호를 기록
→ waves.vcd 생성 → GTKWave 등으로 파형 디버깅
flowchart TB
subgraph TB["tb_top (최상단)"]
CLK["clk 생성<br/>#5 반전 = 100MHz"]
RST["rst 생성<br/>10클록 유지 후 해제"]
IF["axi4_if axi<br/>인터페이스 인스턴스"]
DUT["DUT 인스턴스<br/>axi_ram / axi_ram_v2"]
CFG["config_db::set(vif, axi)"]
RUN["run_test()"]
end
UVM["UVM 환경<br/>test→env→agent→<br/>driver/monitor/sequencer"]
CLK --> IF
RST --> IF
CLK --> DUT
RST --> DUT
IF <-->|s_axi_* 개별 연결| DUT
IF -.config_db.-> CFG
CFG -.vif 전달.-> UVM
RUN -->|테스트 시작| UVM
UVM -.vif.get.-> IF
tb_top이 만든 것 → 어디서 쓰이나
clk, rst → driver/monitor의 @(negedge vif.rst)가 기다림
axi (인터페이스) → driver가 master_cb로 구동, monitor가 monitor_cb로 관찰
config_db::set → driver/monitor의 build_phase에서 get으로 꺼냄
run_test → test→env→agent→driver/monitor/sequencer/scoreboard 생성
DUT_V2 스위치 → v1(버그)/v2(수정) 전환 → 회귀로 버그 증명
tb_top = 모든 부품을 조립하고 시뮬레이션을 시작하는 최상단
① 클록/리셋 생성 (100MHz, 10클록 리셋)
② 인터페이스 실체화 (axi)
③ DUT 연결 (s_axi_* 개별 신호 매핑)
④ config_db로 인터페이스를 UVM에 전달 + run_test로 테스트 시작
⑤ 파형 덤프
핵심 다리:
config_db::set(vif) → driver/monitor가 get으로 받아 하드웨어 접근
DUT_V2 define → 같은 TB로 버그/수정 버전 둘 다 검증