시리즈: ... · tb_top · agent_cfg/agent/env · base_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() 하나만 바꿔서 다른 테스트가 됨
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

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 다음 (모든 게 생성+연결된 후)
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로 받아서 실행 → 어떤 시퀀스든 동일 처리
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 = "나 아직 할 일 있어, 끝내지 마" 신호
raise_objection → "시뮬레이션 끝내지 마"
drop_objection → "이제 끝내도 돼"
모든 objection이 drop되면 → run_phase 종료 → 시뮬레이션 끝
왜 필요? 이거 없으면 run_phase가 바로 끝나버림
→ 시퀀스 실행 전에 시뮬레이션 종료
seq.start(env.agent.sqr);
get_seq()로 만든 시퀀스를 시퀀서에서 실행!
env.agent.sqr = agent가 만든 시퀀서
→ 시퀀스가 여기에 아이템을 밀어넣음
→ 드라이버가 seq_item_port로 받아서 구동
전체 연결:
seq.start(sqr) → sqr → drv.seq_item_port → 버스 구동
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.
파이프라인 드라이버의 함정:
item_done()이 큐에 넣자마자 반환
→ 시퀀스가 "다 보냈다" 생각하고 끝남
→ 근데 버스엔 아직 버스트가 날아다니는 중!
drain 없이 끝내면:
마지막 버스트들이 응답 못 받고 잘림
→ 스코어보드가 미완료 상태로 종료 → 검증 누락!
while (!env.agent.drv.is_idle() && guard < 10000) begin ... end
드라이버가 idle 될 때까지 대기
is_idle() = 큐 비었고 + inflight 0 (driver에서 배운 함수!)
guard = 무한루프 방지 안전장치
10000번(100ns × 10000 = 1ms) 넘으면 강제 탈출 + 에러
→ 트랜잭션이 멈춰서 영원히 안 끝나는 상황 방지
#200ns = 마지막 응답이 모니터 → 스코어보드까지 도달할 시간 확보

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 = 특정 코너를 직접 겨냥
single beat (len=0), max burst (len=15), 4KB 경계 근처,
narrow transfer (size < 버스폭), FIXED 버스트
→ "랜덤이 plateau에서 못 가는 코너를 directed로 채움"
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)를 조정하는 것도 가능!
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
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를 받음 ✅
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 지원)
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/rand | get_seq만 | 단일 방향/랜덤 |
| directed | get_seq만 | 코너 케이스 |
| cov | get_seq + num | 커버리지 채우기 |
| outstanding | build+run | 파이프라인 검증 |
| wrap | get_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으로 런타임에 선택