AXI4 UVM (9) — base_test

Seungyun Lee·2026년 7월 30일

AXI4_UVM_FULL

목록 보기
12/16

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


Test란

"어떤 환경을 만들고, 어떤 자극(sequence)을 흘려보낼지" 결정하는 최고 지휘자
run_test()가 부르는 바로 그 클래스

지금까지 흐름의 시작점이다. tb_top의 run_test("axi4_base_test")가 이 파일을 깨운다.


전체 구조

axi4_base_test (부모 — 뼈대)
    │
    ├── build_phase   : env 생성, vif 받기
    ├── run_phase     : sequence 실행 + drain
    ├── drain()       : 파이프라인 배출 대기
    └── get_seq()     : 어떤 sequence 쓸지 (override 대상!)

    ↓ 상속 (get_seq만 바꿈)

axi4_write_test / read_test / rand_test / directed_test / ...
    → 각자 다른 sequence를 선택

핵심 설계: 부모가 뼈대를 다 만들고, 자식은 get_seq() 하나만 바꿔서 다른 테스트가 됨


Part 1: axi4_base_test — 뼈대

build_phase — 환경 조립

function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    cfg = axi4_agent_cfg::type_id::create("cfg");
    if (!uvm_config_db#(virtual axi4_if)::get(this, "", "vif", cfg.vif))
        `uvm_fatal("NOVIF", "virtual interface 'vif' was not set by tb_top")
    cfg.is_active = UVM_ACTIVE;

    uvm_config_db#(axi4_agent_cfg)::set(this, "env", "cfg", cfg);
    env = axi4_env::type_id::create("env", this);
endfunction

단계별

1. cfg 객체 생성 (아직 비어있음)

2. tb_top이 넣어둔 vif를 꺼내서 cfg에 담기
   ⭐ 여기가 하드웨어-UVM 연결의 완성점!
   tb_top: config_db::set(null, "*", "vif", axi)   ← 넣음
   test:   config_db::get(this, "", "vif", cfg.vif) ← 꺼냄
   → axi 인터페이스가 cfg.vif에 담김
   → cfg를 내려보내면 driver/monitor가 하드웨어 접근 가능

3. active 모드 설정 (sequencer+driver 생성)

4. cfg를 env에 전달 + env 생성
   cfg 전달 체인 시작: test → env → agent → driver/monitor

end_of_elaboration_phase — 계층 출력

function void end_of_elaboration_phase(uvm_phase phase);
    super.end_of_elaboration_phase(phase);
    uvm_top.print_topology();
endfunction
모든 컴포넌트가 만들어진 후 계층 구조를 로그에 출력

print_topology() 출력 예:
  uvm_test_top
    env
      agent
        sqr
        drv
        mon
      sb
      cov

→ "내가 만든 환경이 제대로 조립됐나" 확인용 (디버깅에 유용)
end_of_elaboration_phase = build/connect 다음 (모든 게 생성+연결된 후)

get_seq — 자극 선택 (override 포인트)

virtual function axi4_base_seq get_seq();
    axi4_wr_rd_seq s = axi4_wr_rd_seq::type_id::create("seq");
    return s;
endfunction
virtual 함수 = 자식이 오버라이드할 수 있음!

기본값: axi4_wr_rd_seq (쓰기+읽기 시퀀스)

이게 핵심 설계:
  부모는 "어떤 sequence든 실행"하는 뼈대만 제공
  자식은 get_seq만 바꿔서 다른 sequence 선택
  → polymorphism + factory 패턴!

반환 타입이 axi4_base_seq (부모 타입):
  실제로는 자식 시퀀스를 반환해도 됨 (polymorphism)
  → run_phase는 axi4_base_seq로 받아서 실행 → 어떤 시퀀스든 동일 처리

run_phase — 실행

task run_phase(uvm_phase phase);
    axi4_base_seq seq = get_seq();
    phase.raise_objection(this, "stimulus running");
    `uvm_info("TEST", $sformatf("starting sequence '%s' (num=%0d)",
                                seq.get_type_name(), seq.num), UVM_LOW)
    seq.start(env.agent.sqr);
    drain();
    phase.drop_objection(this, "stimulus done");
endtask

objection 메커니즘

objection = "나 아직 할 일 있어, 끝내지 마" 신호

raise_objection → "시뮬레이션 끝내지 마"
drop_objection  → "이제 끝내도 돼"

모든 objection이 drop되면 → run_phase 종료 → 시뮬레이션 끝

왜 필요? 이거 없으면 run_phase가 바로 끝나버림
        → 시퀀스 실행 전에 시뮬레이션 종료

sequence 실행

seq.start(env.agent.sqr);
get_seq()로 만든 시퀀스를 시퀀서에서 실행!

env.agent.sqr = agent가 만든 시퀀서
→ 시퀀스가 여기에 아이템을 밀어넣음
→ 드라이버가 seq_item_port로 받아서 구동

전체 연결:
  seq.start(sqr) → sqr → drv.seq_item_port → 버스 구동

drain — 파이프라인 배출 (매우 중요)

protected task drain();
    int unsigned guard = 0;
    while (!env.agent.drv.is_idle() && guard < 10000) begin
        #100ns;
        guard++;
    end
    if (guard >= 10000)
        `uvm_error("DRAIN", "driver did not go idle - transactions stuck?")
    #200ns;   // let the final response propagate through the monitor
endtask

주석이 왜 필요한지 설명한다:

// The driver is pipelined: item_done() returns as soon as a burst is
// queued, not when it finishes on the bus.

왜 drain이 필요한가

파이프라인 드라이버의 함정:
  item_done()이 큐에 넣자마자 반환
  → 시퀀스가 "다 보냈다" 생각하고 끝남
  → 근데 버스엔 아직 버스트가 날아다니는 중!

drain 없이 끝내면:
  마지막 버스트들이 응답 못 받고 잘림
  → 스코어보드가 미완료 상태로 종료 → 검증 누락!

drain 동작

while (!env.agent.drv.is_idle() && guard < 10000) begin ... end
  드라이버가 idle 될 때까지 대기
  is_idle() = 큐 비었고 + inflight 0  (driver에서 배운 함수!)

guard = 무한루프 방지 안전장치
  10000번(100ns × 10000 = 1ms) 넘으면 강제 탈출 + 에러
  → 트랜잭션이 멈춰서 영원히 안 끝나는 상황 방지

#200ns = 마지막 응답이 모니터 → 스코어보드까지 도달할 시간 확보


Part 2: 파생 테스트들 — get_seq만 바꿈

단일 방향 테스트 3종

class axi4_write_test extends axi4_base_test;
    virtual function axi4_base_seq get_seq();
        axi4_write_seq s = axi4_write_seq::type_id::create("seq");
        return s;
    endfunction
endclass
write_test → 쓰기만
read_test  → 읽기만
rand_test  → 랜덤

세 클래스 전부 get_seq() 하나만 다름!
나머지(build/run/drain)는 부모 것 그대로 상속
→ 코드 중복 최소화, 뼈대 재사용

polymorphism의 실전 활용:

부모 run_phase에서:
  axi4_base_seq seq = get_seq();  ← virtual이라 자식 버전 호출!

write_test 실행 시 → write_test의 get_seq() → axi4_write_seq
read_test 실행 시  → read_test의 get_seq()  → axi4_read_seq

directed_test — 코너 케이스

directed = 특정 코너를 직접 겨냥
  single beat (len=0), max burst (len=15), 4KB 경계 근처,
  narrow transfer (size < 버스폭), FIXED 버스트

→ "랜덤이 plateau에서 못 가는 코너를 directed로 채움"

cov_test — 커버리지 채우기

class axi4_cov_test extends axi4_base_test;
    virtual function axi4_base_seq get_seq();
        axi4_cov_seq s = axi4_cov_seq::type_id::create("seq");
        s.num = 40;              // extra random traffic after the sweep
        return s;
    endfunction
endclass
커버리지 bin을 채우기 위한 넓은 자극
s.num = 40 → sweep 후 추가 랜덤 40개

주석: "May report WRAP mismatches - those are true positives"
  → WRAP 버그로 인한 미스매치가 나올 수 있고 그건 진짜 버그(true positive)

get_seq에서 시퀀스 필드(num)를 조정하는 것도 가능!

Part 3: 특수 테스트들 (구조가 다름)

outstanding_test — 파이프라인 검증

get_seq가 아니라 build_phase와 run_phase를 직접 오버라이드한다.

class axi4_outstanding_test extends axi4_base_test;

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        cfg.max_outstanding = 4;    // ⭐ 파이프라인 깊이 설정
    endfunction

max_outstanding 설정 타이밍이 핵심

super.build_phase(phase);       // 부모가 cfg 만듦
cfg.max_outstanding = 4;        // 그 cfg를 수정

주석: "The driver's build_phase runs after ours, so it picks this up."

순서 (UVM build_phase는 top-down):
  1. test.build_phase → cfg 생성, max_outstanding=4로 수정
  2. env.build_phase (나중)
  3. agent.build_phase (더 나중)
  4. driver.build_phase (가장 나중) → cfg.max_outstanding=4를 읽음

→ test가 먼저 실행되니 cfg 수정이 driver보다 앞섬 → driver가 4를 받음 ✅

run_phase — 쓰기/읽기 분리 pass

task run_phase(uvm_phase phase);
    axi4_os_write_seq wr = ...;
    axi4_os_read_seq  rd = ...;
    phase.raise_objection(this, "outstanding stimulus");

    wr.start(env.agent.sqr);
    drain();                     // 모든 쓰기가 끝나야 읽기 시작

    rd.start(env.agent.sqr);
    drain();

    phase.drop_objection(this, "done");
endtask
주석: "read must not overtake a write that has not completed yet"

왜 분리?
  AXI 읽기/쓰기 채널은 독립적
  → 파이프라인으로 섞으면 읽기가 쓰기를 추월할 수 있음
  → "아직 안 쓴 데이터를 읽는" 상황 → 잘못된 검증

해결:
  phase 1: 쓰기 전부 → drain (다 완료 대기)
  phase 2: 읽기 전부 → drain
  → 쓰기가 확실히 끝난 후 읽기 → 순서 보장

목적: peak 카운터로 파이프라인 동작 증명
  axi_ram(v1)    → peak 1 (한 번에 하나)
  axi_ram_v2     → peak >1 (outstanding 지원)

wrap_test — 일부러 실패하는 테스트

class axi4_wrap_test extends axi4_base_test;
    virtual function axi4_base_seq get_seq();
        axi4_wrap_seq s = axi4_wrap_seq::type_id::create("seq");
        return s;
    endfunction

    function void start_of_simulation_phase(uvm_phase phase);
        super.start_of_simulation_phase(phase);
        `uvm_info("TEST",
            "axi4_wrap_test: UVM_ERRORs below are EXPECTED - they demonstrate the DUT's WRAP-burst bug",
            UVM_LOW)
    endfunction
endclass
이 테스트는 "일부러 실패"함!

목적:
  DUT의 WRAP 버그를 문서화하고 재현
  → 회귀 테스트에는 포함 안 됨 (clean regression 아님)

start_of_simulation_phase:
  시뮬 시작 시 "아래 에러들은 예상된 것"이라고 미리 알림
  → 로그 보는 사람이 "진짜 실패"로 오해 안 하게

negative testing의 일부:
  "버그가 있으면 정말 FAIL 하는가"를 증명하는 테스트

전체 테스트 계층

테스트방식목적
base뼈대wr_rd 기본 자극
write/read/randget_seq만단일 방향/랜덤
directedget_seq만코너 케이스
covget_seq + num커버리지 채우기
outstandingbuild+run파이프라인 검증
wrapget_seq + 알림버그 재현 (FAIL 예상)

테스트를 어떻게 고르나 — 커맨드라인

test를 바꾸는 건 코드 수정이 아니라 실행 옵션이다.

# 기본 (base_test)
run_xsim.bat +UVM_TESTNAME=axi4_base_test

# WRAP 버그 재현
run_xsim.bat +UVM_TESTNAME=axi4_wrap_test

# 파이프라인
run_xsim.bat +UVM_TESTNAME=axi4_outstanding_test
한 번 실행 = test 하나 = 시퀀스 하나
여러 개 다 돌리기 = regression 스크립트가 여러 번 자동 실행

전체 흐름 완성

tb_top
  → config_db::set(vif)
  → run_test("axi4_base_test")
        ↓
test.build_phase → cfg 생성, vif를 cfg에 담기 → env 생성
        ↓
env → agent → driver/monitor 생성 (cfg 계속 전달)
        ↓
test.run_phase → get_seq()로 시퀀스 선택 → start → drain
        ↓
sequence → sequencer → driver → 버스 구동
        ↓
DUT 동작 → monitor 관찰 → ap → scoreboard/coverage
        ↓
scoreboard: ref_model과 비교 → pass/fail

한 줄 요약

Test = UVM 계층 최상단, "환경 + 자극"을 결정하는 지휘자

axi4_base_test (뼈대):
  build_phase → cfg 만들고 vif 받고 env 생성
  run_phase   → get_seq()로 시퀀스 선택 → start → drain
  drain()     → 파이프라인 드라이버 배출 대기 (핵심!)
  get_seq()   → virtual, 자식이 오버라이드 (polymorphism)

파생 테스트:
  대부분 get_seq()만 바꿔서 다른 자극 (재사용)
  outstanding → build/run 오버라이드 (max_outstanding=4)
  wrap → 일부러 FAIL (버그 재현)

핵심: 뼈대 하나 만들고 get_seq만 바꿔 수십 개 테스트 생성
     +UVM_TESTNAME으로 런타임에 선택
profile
Design Verification engineer

0개의 댓글