AXI4 UVM (7) — tb_top

Seungyun Lee·2026년 7월 30일

AXI4_UVM_FULL

목록 보기
10/16

시리즈: ... · ref_model/scoreboard · tb_top · agent_cfg · agent · env · base_test


tb_top이란

지금까지 본 모든 부품(인터페이스, 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()                  │
│                                                       │
│  ⑤ 파형 덤프                                            │
└───────────────────────────────────────────────────────┘

0. 헤더 — DUT 선택 스위치

`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" 증명 (테스트 신뢰성)

1. 패키지 import

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 등)

2. 파라미터 — 버스 폭

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 = 지역 상수 (외부에서 못 바꿈) → 실수 방지

3. 클록 & 리셋 생성

클록

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으로 떨어지면 드라이버와 모니터가 깨어나 동작 시작.


4. 인터페이스 인스턴스

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로 관찰할 대상

5. DUT 인스턴스 — 신호 연결

`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로 → 드라이버가 읽음

6. UVM에 인터페이스 전달 + 테스트 시작 (핵심)

initial begin
    uvm_config_db#(virtual axi4_if)::set(null, "*", "vif", axi);
    run_test("axi4_base_test");
end

이 두 줄이 하드웨어 세계와 UVM 세계를 연결하는 다리다.

config_db::set — 인터페이스 넘기기

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 — 테스트 시작

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 끝나면 시뮬레이션 종료

7. 파형 덤프

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로 버그/수정 버전 둘 다 검증
profile
Design Verification engineer

0개의 댓글