이 문서 하나로 공부할 수 있게 방법 + 내용을 모두 담았다.
⚠️ 코드 스니펫은 이해를 돕기 위한 것이고, 정답은 항상 실제 파일이다.
파일이 갱신되면 이 문서보다 파일을 믿을 것.
① 위에서 아래로 (Top-down).
코드부터 열지 말 것. 그림(아키텍처) → 실행(파형) → 코드 순서.
② 파일 순서가 아니라 "트랜잭션의 일생"을 따라가라.
알파벳순으로 읽으면 죽는다. write 버스트 하나가
태어나서(seq) → 핀을 흔들고(driver) → 관측되고(monitor) → 검사받는(scoreboard)
경로를 따라가면 자연스럽게 연결된다.
③ 수동 읽기 금지, 능동 학습.
"읽고 이해했다"는 착각이 제일 위험하다. 코드를 덮고 다시 써보기,
파형에서 신호↔코드 연결하기, 소리 내어 설명하기.
→ "인식(recognize)"과 "설명(explain)"의 차이. 면접은 후자를 본다.
| 순서 | 무엇 | 집중 |
|---|---|---|
| 1 | docs/ARCHITECTURE.md | 다이어그램 손으로 따라 그리기. 백지 재현되면 통과 |
| 2 | 파형 보기 (run_axi.bat → 1번) | 코드 전에 살아있는 걸 먼저. → Part 2.2 체크리스트 |
| 3 | rtl/axi_ram.v + docs/axi_ram_dut_analysis.pdf | write FSM 3상태. awready가 언제 1이 되는지 |
| 순서 | 파일 | 집중 |
|---|---|---|
| 4 | tb/uvm/axi4_seq_item.svh | rand 필드 + constraint. beat_addr()가 왜 필요한지 |
| 5 | tb/axi4_if.sv | clocking block이 왜 있나 |
| 6 | tb/uvm/axi4_driver.svh ⭐⭐ | 가장 중요. 6개 스레드 + 큐 구조 |
| 7 | tb/uvm/axi4_monitor.svh ⭐ | 핀만 보고 트랜잭션 복원. 채널 병렬 + FIFO |
| 8 | axi4_ref_model.svh + axi4_scoreboard.svh ⭐ | predict/check 패턴 |
| 순서 | 파일 | 집중 |
|---|---|---|
| 9 | tb/tb_top.sv | vif를 config_db에 넣고 run_test() |
| 10 | agent_cfg → agent → env → base_test | build/connect phase 흐름 |
| 11 | axi4_sequences.svh | start_item/finish_item |
| 12 | axi4_coverage.svh | covergroup, cross, ignore_bins |
| 13 | tb/axi4_sva.sv | bind, VALID/READY 규칙 |
| 14 | rtl/Axi_ram_v2.v | 큐 기반 outstanding |
| 채널 | 방향 | 역할 |
|---|---|---|
| AW (Write Address) | 마스터→슬레이브 | 쓸 주소/제어 |
| W (Write Data) | 마스터→슬레이브 | 쓸 데이터 beat들 |
| B (Write Response) | 슬레이브→마스터 | 쓰기 완료 응답 |
| AR (Read Address) | 마스터→슬레이브 | 읽을 주소/제어 |
| R (Read Data) | 슬레이브→마스터 | 읽은 데이터 beat들 |
핵심: 5개가 독립적이다. read와 write는 서로 순서 보장이 없다.
(그래서 axi4_outstanding_test에서 write 다 끝내고(drain) read를 시작한다.)
VALID와READY가 둘 다 1인 클럭 상승엣지에 전송이 성립한다.
파생 규칙:
→ 이 두 규칙이 tb/axi4_sva.sv의 AXI_VALID_HELD / AXI_STABLE 어서션이다.
| burst | 주소 변화 | 용도 |
|---|---|---|
FIXED (00) | 안 변함 | FIFO 같은 고정 주소 |
INCR (01) | + (1 << size) | 일반 메모리 접근 (제일 흔함) |
WRAP (10) | 윈도우 안에서 되돌아감 | 캐시 라인 fill |
WRAP 규칙: beat 수가 반드시 2/4/8/16 중 하나.
→ 윈도우 크기가 2의 거듭제곱이어야 비트마스크로 랩할 수 있기 때문.
| 필드 | 의미 |
|---|---|
len | beat 수 - 1 (len=3이면 4 beats) |
size | beat당 바이트 = 2^size (size=2면 4바이트) |
strb | write 시 바이트 레인 마스크 (비트당 1바이트) |
last | 마지막 beat 표시 (wlast/rlast) |
resp | 00=OKAY, 10=SLVERR, 11=DECERR |
c_4kc_alignc_wrap_lensize가 버스 폭을 초과 못 함 → c_sizerun_axi.bat → 1번(write path waveform)으로 열고 아래와 대조한다.

bready는 드라이버(b_thread)가 항상 1로 유지한다.
awready가 1 — DUT가 IDLE에서 대기awvalid와 awready가 동시에 1인 사이클이 있다 → 주소 수락wready가 1 — 드라이버가 AW/W를 분리한 이유wvalid가 먼저 올라가 기다린다 — valid는 ready 전에 안 내려감awlen + 1 — 직접 세어볼 것wlast=1wlast 다음에 bvalid, bresp=00write_state_reg: 0(IDLE) → 1(BURST) → 0req = axi4_seq_item::type_id::create("req"); // ① factory
start_item(req); // ② 시퀀서에 요청
if (!req.randomize() with { dir == AXI_WRITE; }) // ③ 제약 만족 랜덤
finish_item(req); // ④ 드라이버가 가져갈 때까지 블로킹
new 대신 type_id::create → factory 경유 (테스트에서 타입 교체 가능)finish_item은 드라이버가 item_done()을 부를 때까지 블로킹seq_item_port.get_next_item(req); // 드라이버가 당겨옴 (pull 모델)
seq_item_port(드라이버) ↔ seq_item_export(시퀀서)는 agent의 connect_phase에서 연결.
while (n_inflight >= cfg.max_outstanding) @(vif.master_cb); // 깊이 제한
aw_q.push_back(req); w_q.push_back(req); // 같은 순서로!
n_inflight++;
seq_item_port.item_done(); // ★ 버스 완료 전에 즉시 반환
item_thread는 버스 신호를 하나도 안 건드린다. 큐에 넣고 카운터만 관리.
// aw_thread
vif.master_cb.awaddr <= tr.addr;
vif.master_cb.awvalid <= 1'b1;
@(vif.master_cb);
while (!vif.master_cb.awready) @(vif.master_cb); // ready까지 유지
vif.master_cb.awvalid <= 1'b0;
master_cb(clocking block)로 구동 → 클럭 엣지 레이스 방지.
드라이버와 완전히 별개로 핀을 감시한다. mon_aw가 AW 핸드셰이크를 잡아
트랜잭션을 만들고, mon_w가 데이터를 채우고, mon_b가 완성해서 방송한다.
ap.write(tr); // 모니터
// env에서:
agent.mon.ap.connect(sb.ap_imp); // → scoreboard
agent.mon.ap.connect(cov.analysis_export); // → coverage
run_phase가 6개 영구 스레드를 띄운다. 버스트마다 만드는 게 아니라
시뮬 내내 돈다. 서로 큐와 카운터로만 소통.
fork
item_thread(); // 시퀀서 → 큐 (버스 안 건드림)
aw_thread(); w_thread(); b_thread(); // write 채널들
ar_thread(); r_thread(); // read 채널들
join
비유: item_thread는 주문 접수 창구, aw/w/b_thread는 주방.
창구는 티켓(큐)만 꽂고, 주방이 요리한다. 그래서 창구가 안 막힌다.
outstanding: out + standing = 밖에 나가 서 있는, 주소는 보냈는데 아직 응답 못 받은 상태
item_done()이 버스트 완료 전에 호출된다
→ 시퀀스가 곧바로 다음 걸 만들어 큐에 쌓음
→ 여러 버스트가 버스 위에 동시 존재
깊이 제한은 while (n_inflight >= cfg.max_outstanding) @(cb) 한 줄.
max_outstanding = 1 → 직렬 (기존과 동일)max_outstanding = 4 → 4개까지 겹침| 카운터 | 의미 |
|---|---|
n_inflight | 시퀀서에서 받았지만 완료 안 된 것 (큐 대기 포함) → 깊이 제한용 |
wr_outstanding / wr_peak | 버스에 실제로 떠 있는 것 (주소 수락~응답 전) → 계측/증명용 |
wr_peak가 v1=2, v2=4로 나오는 게 outstanding 지원 차이의 실측 증거.
채널마다 스레드를 두고 FIFO로 단계를 상관(correlate) 시킨다.
AW 핸드셰이크 → aw_q에 push
W beats → aw_q[0]에 채움, wlast에서 w_done_q로 이동
B 핸드셰이크 → w_done_q에서 pop, resp 붙여서 ap.write()
처음엔 AW 대기 → W beat 수집 → B 대기 순차 루프였다.
버스트 N의 W beat를 수집하는 사이에 DUT가 N+1의 주소를 받으면
그 핸드셰이크를 영영 못 본다. → 트랜잭션 유실, payload 오배치.
교훈: 모니터는 DUT만큼 동시성을 모델링해야 한다.
전제: in-order 응답 (두 DUT 모두 보장). ID별 out-of-order라면 per-ID FIFO 필요.
DUT가 올바르게 동작했다면 메모리가 어떤 상태여야 하는지 예측하는 모델.
predict / check 패턴:
write 관측 → ref_model.apply_write() (예측)
read 관측 → ref_model.expected_beat() → 실제와 비교 (검사)
① DUT와 독립적으로 만든다 (가장 중요)
beat_addr()로 정확한 FIXED/INCR/WRAP 주소를 계산한다.
DUT 코드(addr + (1<<size))를 베꼈다면 같은 WRAP 버그를 복사해 아무것도 못 잡는다.
② 바이트 단위 저장
protected bit [7:0] mem [bit [AXI_ADDR_WIDTH-1:0]];
wstrb로 일부 바이트만 써지는 걸 정확히 반영. 워드 단위면 부분 쓰기에서 가짜 미스매치.
③ Sparse (연관 배열) — 안 쓴 곳은 0
DUT가 시각 0에 메모리를 0으로 초기화하므로 모델도 안 쓴 바이트는 0으로 예측.
④ word_base 정렬 — DUT의 인덱싱을 흉내
word_base = a - (a % AXI_STRB_WIDTH); // 4바이트 워드 시작
DUT는 메모리를 워드 단위(addr >> 2) 로 인덱싱한다. 그래서 narrow transfer
(size < 버스폭)일 때 여러 beat가 같은 워드에 들어간다. 모델도 맞춰야 한다.
⑤ read 예측은 "정렬된 워드 전체" 반환
DUT가 32비트 워드를 통째로 반환하므로 모델도 그렇게. 그다음
스코어보드가 addressed lane만 골라서 비교 (나머지는 don't-care).
foreach (tr.data[i]) begin
a = tr.beat_addr(i); // 이 beat의 올바른 주소
word_base = a - (a % 4);
for (lane = 0; lane < 4; lane++)
if (tr.strb[i][lane]) // strobe 켜진 레인만
mem[word_base + lane] = tr.data[i][8*lane +: 8];
end
WRAP write @0x0A08 (윈도우 0x0A00~0x0A0F):
ref_model이 예측한 주소 : 0x0A08, 0x0A0C, 0x0A00, 0x0A04 ← 랩
DUT가 실제로 쓴 주소 : 0x0A08, 0x0A0C, 0x0A10, 0x0A14 ← 선형
모델은 0x0A00/0x0A04에 데이터가 있다고 예측 → DUT는 안 씀 → 미스매치!
beat_addr()가 자극과 모델 양쪽의 "올바른 주소" 단일 출처라는 게 핵심 설계.
uvm_analysis_imp #(axi4_seq_item, axi4_scoreboard) ap_imp;
function void write(axi4_seq_item tr); // 모니터가 방송할 때마다 호출
if (tr.dir == AXI_WRITE) ref_model.apply_write(tr); // 예측
else check_read(tr); // 검사
mask = tr.lane_mask(i); // 이 beat가 실제 주소지정한 레인
for (lane = 0; lane < 4; lane++) begin
if (!mask[lane]) continue; // 주소지정 안 된 레인은 don't-care
if (tr.data[i][8*lane +: 8] !== exp[8*lane +: 8]) `uvm_error(...)
end
narrow transfer에서 나머지 레인은 AXI상 don't-care라 다 비교하면 가짜 실패가 난다.
스코어보드는 "맞았나?", 커버리지는 "시험이나 해봤나?" — 둘 다 필요.
| coverpoint | 무엇 |
|---|---|
cp_dir | read / write |
cp_burst | FIXED / INCR / WRAP |
cp_size | 1 / 2 / 4 바이트 |
cp_len | 1 / 2-4 / 5-8 / 9-16 beats |
cp_resp | OKAY (나머지는 도달 불가) |
cp_region | 주소 상위 4비트 (4KB 영역 16개) |
cp_strb | FULL / PARTIAL / NONE |
cross: burst × size, burst × len, dir × burst
ignore_bins — 도달 불가를 근거와 함께 제외// DUT가 resp를 OKAY로 하드코딩 → 에러 응답 자체가 불가능
ignore_bins unreachable = {AXI_EXOKAY, AXI_SLVERR, AXI_DECERR};
// 1-beat WRAP은 AXI 규격상 불법 (WRAP은 2/4/8/16만)
ignore_bins wrap_single = binsof(cp_burst.wrap) && binsof(cp_len.single);
도달 불가 빈은 방치하지 말고 이유를 명시해야 한다. 안 그러면 "커버리지 98%"가
진짜 구멍인지 원래 불가능한 건지 아무도 모른다.
| 단계 | 커버리지 | 한 일 |
|---|---|---|
| 랜덤만 | 98.33% | 제약 랜덤의 한계 |
| ↓ | 리포트로 홀 특정 → x_burst_len 12개 중 10개 | |
ignore_bins | 90.91% | 불가능 빈 제외 (분모 12→11) |
| directed sweep | 100% | 모든 (burst × 길이) 조합 명시적으로 |
교훈: 제약 랜덤은 90%까지, 마지막 몇 %는 거의 항상 directed가 필요하다.
bind로 RTL·TB를 안 건드리고 체커를 주입한다 (실무 표준 패턴).
주요 검사:
2'b11 금지, size가 버스 폭 초과 금지| 질문 | WRAP 버그를 잡나? | |
|---|---|---|
| SVA | "버스가 규칙대로 동작하나?" | ❌ (핸드셰이크는 정상이었음) |
| Scoreboard | "데이터가 맞나?" | ✅ |
WRAP 버그는 데이터가 잘못된 위치에 저장되는 기능 버그이지 프로토콜 위반이 아니다.
둘은 보완재이지 대체재가 아니다.
axi_ram.v)WRITE_STATE_IDLE: s_axi_awready_next = 1'b1; // IDLE일 때만 주소 받음
주소를 받는 순간 엔진이 그 트랜잭션에 묶인다 → outstanding 불가.
그리고 burst != FIXED면 무조건 addr + (1<<size) → WRAP 미구현 (버그).
Axi_ram_v2.v)① 큐로 인터페이스와 엔진을 분리
assign s_axi_awready = !aw_full; // ★ 엔진 상태와 무관! 큐 자리만 있으면 받음
이 한 줄이 outstanding 지원의 핵심. 큐 3개(aw_q, ar_q, b_q).
b_q 덕분에 v1의 RESP 상태가 사라짐 (3-state → 2-state).
② WRAP 제대로 구현
total_bytes = (len + 1) << size; // 윈도우 크기
wrap_mask = total_bytes - 1; // 예: 16-1 = 0x000F
wrap_base = addr & ~wrap_mask; // 윈도우 시작
next_addr_calc = (inc & wrap_mask) | wrap_base; // ★ 오프셋만 남기고 복귀
WRAP 길이가 2/4/8/16으로 제한되는 이유가 여기 있다 —
윈도우가 2의 거듭제곱이라 마스크 한 번으로 끝난다.
③ credit-based flow control
wire w_can_start = !aw_empty & !b_full; // 출구 자리를 확보한 뒤 입구를 연다
버스트를 시작해놓고 응답 큐가 꽉 차서 멈추는 상황을 원천 차단 (데드락 회피 기본 패턴).
④ FIFO에 별도 카운터를 쓰는 이유
포인터만 쓰면 wr_ptr == rd_ptr일 때 full/empty 구분이 안 된다.
해법은 ① 포인터에 1비트 추가 ② 별도 카운터 — v2는 ②.
(CNT_W = $clog2(DEPTH+1): 0~4를 표현해야 하므로 포인터보다 1비트 더)
| 한계 | 내용 |
|---|---|
| 응답 코드 고정 | bresp/rresp OKAY 하드코딩 → 에러 보고 불가 |
| in-order만 | 실제 AXI는 ID별 out-of-order 허용, v2는 엄격한 FIFO |
| LOCK 무시 | 배타적 접근 미구현 |
| WRAP 길이 미검증 | 불법 길이가 오면 마스크가 깨짐 (입력이 합법이라 가정) |
| read/write 해저드 | 같은 주소에 대한 두 채널 간 순서 보장 없음 |
| v1 | v2 | |
|---|---|---|
axi4_wrap_test | 24 미스매치 🐛 | 통과 ✅ |
| peak outstanding | 2 | 4 |
v1의 2는 우연 — FSM이 BVALID를 올리는 같은 사이클에 AWREADY도 올려서
응답 대기 중 주소 하나를 더 받는다.
| # | 개념 | 파일 | 왜 중요 |
|---|---|---|---|
| 1 | 트랜잭션 흐름 전체 | Layer B 전부 | 모든 질문의 뿌리 |
| 2 | config_db + factory + phase | build_phase들 | UVM 3대 기둥 |
| 3 | TLM (seq_item_port / analysis_port) | driver, monitor, sb | 컴포넌트 간 통신 |
| 4 | driver 프로토콜 로직 | axi4_driver.svh | 유일한 "진짜 로직" |
| 5 | scoreboard predict/check | scoreboard + ref_model | "정답을 어떻게 아나" |
| 6 | objection | base_test run_phase | "시뮬이 왜 안 끝나요" |
| 7 | coverage 마감 + ignore_bins | coverage | "100%가 무슨 뜻" |
| 8 | SVA vs scoreboard | sva + scoreboard | "둘 다 왜 필요" |
면접관은 문법보다 검증적 사고를 본다. 각각 30초 안에 설명할 수 있어야 한다.
1. DUT의 WRAP 버그 + 대칭 검사의 함정
WRAP write를 WRAP read로 되읽으면 통과한다 — DUT가 양쪽에서 똑같이 틀리니
자기가 잘못 쓴 그 자리에서 그대로 읽어온다. 참조 모델도 양쪽 다 올바르니 일관됨.
WRAP으로 쓰고 INCR로 읽어야 데이터가 물리적으로 어디 있는지 드러난다.교훈: 쓴 방식 그대로 되읽는 검사는 주소 버그를 못 잡는다.
2. 내 모니터의 동시성 버그
파이프라이닝을 켜니 스코어보드가 48건 미스매치 → DUT를 지목. 범인은 순차 모니터.
8개 중 3개만 관측하고 있었다.교훈: 스코어보드 실패가 DUT가 범인이란 뜻은 아니다. 검증 환경도 검증 대상이다.
3. SVA가 조용히 죽어 있던 문제
XSim이 untyped property 인자를 경고만 내고 무시 → 어서션 전체가 vacuous.
실제 어서션을 뒤집어 5번 발화(관측된 AWVALID 5사이클과 일치)를 확인해 증명.교훈: 어서션이 통과하는 것과 어서션이 존재하는 것은 다르다.
aw_thread를 코드 덮고 써보기. 막히는 곳이 약점awvalid가 뜬 순간이 코드 어느 줄인지 짚기git log. 왜 그렇게 했는지, 무슨 버그를 잡았는지 다 있다.run_axi.bat 8번 vs 9번. 같은 테스트, 다른 결과| 일차 | 목표 |
|---|---|
| 1일 | Layer A + Part 2.1~2.2. 파형 체크리스트 8개 확인 |
| 2~3일 | Layer B + Part 2.3~2.4. 드라이버 백지 재현 |
| 4일 | Part 2.5~2.7 (monitor, ref model, scoreboard) |
| 5일 | Layer C + Part 2.8~2.9 (coverage, SVA) |
| 6일 | Part 2.10 (v1 vs v2) + 버그 3개 스토리 |
| 7일 | 전체 복습 + 예상 질문 소리 내어 답하기 |
new와 type_id::create의 차이는?ignore_bins는 언제 쓰나? 남용하면 어떤 위험이 있나?wstrb는 어떻게 정해지나?이 프로젝트의 진짜 가치는 "동작하는 UVM 코드"가 아니라
발견한 3개의 버그와 그 과정이다. 코드 100줄 외우는 것보다
Part 4의 세 문장을 자기 말로 설명하는 게 훨씬 강력하다.