시리즈: 아이템 · 인터페이스 · 드라이버 · 모니터 · 관계 · ref_model/scoreboard
모니터가 관찰한 트랜잭션이 최종적으로 "맞는지 틀린지" 판정되는 곳이다.
Monitor가 관찰한 트랜잭션
│ ap.write(tr)
↓
┌─────────────────────────────────────┐
│ Scoreboard (심판) │
│ │
│ WRITE → ref_model.apply_write() │ "이렇게 됐어야 해"
│ READ → check_read() │ "실제로 맞나 비교"
│ │ │
│ ↓ │
│ ┌────────────────────────┐ │
│ │ Reference Model │ │
│ │ (골든 모델 = 정답지) │ │
│ │ byte 단위 sparse memory │ │
│ └────────────────────────┘ │
└─────────────────────────────────────┘
핵심 분업:
// Deliberately independent of the DUT's implementation
// The DUT increments the address linearly even for WRAP bursts, so
// a WRAP write/read pair will disagree with this model — that is a
// real DUT bug, not a modelling error.
이 주석이 "골든 모델 독립 구현" 원칙의 실제 구현이다.
DUT 코드를 보고 만들면:
DUT가 WRAP을 INCR처럼 처리 → 골든 모델도 똑같이 처리
→ 둘 다 틀렸는데 일치 → PASS → 버그 놓침 ❌
스펙만 보고 독립 구현:
골든 모델은 beat_addr()로 "올바른" WRAP 주소 계산
DUT는 "틀린" 선형 증가
→ 둘이 불일치 → 버그 노출 ✅
→ 이 파일이 WRAP 버그를 잡을 수 있었던 근본 이유
❌ 오해: ref_model이 스펙 문서를 읽어서 미리 정답을 다 적어둔다
✅ 진실: ref_model은 모니터가 관찰한 "실제 트랜잭션"을 받아서
그때그때 계산한다
"스펙 문서로 보고 만든다"는 건 코드를 짤 때 얘기다. 실행 중에 문서를 읽는 게 아니라, 개발자가 스펙대로 beat_addr() 같은 계산 규칙을 코드에 박아둔 것이다.
정리하면:
class axi4_ref_model extends uvm_object;
`uvm_object_utils(axi4_ref_model)
protected bit [7:0] mem [bit [AXI_ADDR_WIDTH-1:0]];
uvm_object 상속:
→ 계층 구조(hierarchy)에 안 들어감
→ phase(build/run/report)가 없음
→ 그냥 "데이터를 담는 객체"
이유:
ref_model은 스코어보드가 소유하고 호출하는 도구
독립적으로 phase를 돌 필요 없음 → 가벼운 uvm_object가 적합
protected bit [7:0] mem [bit [AXI_ADDR_WIDTH-1:0]];
// ↑ 값: 1바이트 ↑ 키: 주소
이게 "byte-granular sparse memory"다.
byte-granular (바이트 단위):
값이 bit[7:0] = 1바이트 → 주소 하나당 1바이트 저장
→ write strobe를 정확히 모델링 가능 (특정 바이트만 쓰는 걸 그대로 반영)
sparse (희소):
associative array = 실제로 쓴 주소만 저장
→ 64KB 전체를 배열로 잡지 않음 (메모리 절약)
→ mem[0x1000] 쓰면 그 주소만 생김
일반 배열 vs 연관 배열:
bit [7:0] mem [65536]; ← 64KB 전부 할당 (안 써도)
bit [7:0] mem [bit[15:0]]; ← 쓴 주소만 (sparse)
function bit [7:0] read_byte(bit [AXI_ADDR_WIDTH-1:0] a);
return mem.exists(a) ? mem[a] : 8'h00;
endfunction
mem.exists(a): 이 주소에 값이 있나?
있으면 → mem[a] 반환
없으면 → 8'h00 반환
왜 없으면 0?
DUT가 시작 시 메모리를 0으로 초기화 → 안 쓴 주소는 0이어야 함
→ 모델도 안 쓴 주소를 0으로 예측 → DUT와 일치
DUT가 쓰기를 하면, 정답지에도 똑같이 써놓는 함수.
function void apply_write(axi4_seq_item tr);
bit [AXI_ADDR_WIDTH-1:0] a, word_base;
foreach (tr.data[i]) begin
a = tr.beat_addr(i);
word_base = a - (a % AXI_STRB_WIDTH);
for (int lane = 0; lane < AXI_STRB_WIDTH; lane++)
if (tr.strb[i][lane])
mem[word_base + lane] = tr.data[i][8*lane +: 8];
end
endfunction
구체적 숫자로 따라가보면:
상황: 0x1000에 데이터 0xDDCCBBAA 쓰기, strb = 4'b1010
strb = 1010 의미:
bit0=0 → 바이트0 안 씀
bit1=1 → 바이트1 씀
bit2=0 → 바이트2 안 씀
bit3=1 → 바이트3 씀
데이터 0xDDCCBBAA를 바이트로 쪼개면:
바이트0=0xAA, 바이트1=0xBB, 바이트2=0xCC, 바이트3=0xDD
strb에 따라 1번, 3번만 저장:
mem[0x1001] = 0xBB (바이트1)
mem[0x1003] = 0xDD (바이트3)
(0x1000, 0x1002는 안 씀 → 사전에 안 생김)
이게 "write strobe를 정확히 반영"한다는 뜻이다. DUT가 특정 바이트만 썼으면, 정답지도 그 바이트만 쓴다.
beat_addr(i)가 버그를 잡는 지점a = tr.beat_addr(i); ← 여기가 핵심!
beat_addr는 seq_item의 함수로 WRAP/INCR/FIXED 주소를 "올바르게" 계산
DUT는 WRAP을 틀리게 계산하는데
ref_model은 여기서 올바르게 계산
→ 나중에 둘이 안 맞아서 버그 노출
word_base = a - (a % AXI_STRB_WIDTH) — 버스 폭 정렬주소 a를 버스 폭(4바이트) 배수로 내림 = "a가 속한 워드의 시작 주소"
a = 0x1006:
0x1006 % 4 = 2
word_base = 0x1006 - 2 = 0x1004
DUT는 메모리를 워드(4바이트) 단위로 다룸 (mem[addr>>2])
→ ref_model도 워드 시작을 알아야 바이트 레인을 정확히 매핑
[8*lane +: 8] 문법+: = "이 위치부터 N비트" (part-select)
data[8*lane +: 8] = data[8*lane]부터 8비트
lane=0: data[7:0] ← 바이트 0
lane=1: data[15:8] ← 바이트 1
lane=2: data[23:16] ← 바이트 2
lane=3: data[31:24] ← 바이트 3
왜 이 문법? data[8*lane+7 : 8*lane]처럼 쓰면 lane이 변수라 에러
+: 는 시작점이 변수여도 됨 (폭은 상수)
apply_write의 역방향. 읽기 검증 시 "정답"을 알려준다.
function bit [AXI_DATA_WIDTH-1:0] expected_beat(axi4_seq_item tr, int i);
bit [AXI_ADDR_WIDTH-1:0] a, word_base;
a = tr.beat_addr(i);
word_base = a - (a % AXI_STRB_WIDTH);
for (int lane = 0; lane < AXI_STRB_WIDTH; lane++)
expected_beat[8*lane +: 8] = read_byte(word_base + lane);
endfunction
apply_write: data → mem (쓰기)
expected_beat: mem → 예상 data (읽기)
한 beat에 대해:
1. beat_addr(i)로 올바른 주소 계산
2. word_base로 정렬
3. 4개 레인의 바이트를 mem에서 읽어 32비트 워드 조립
왜 전체 워드를 조립하나:
DUT는 읽기 시 32비트 워드 전체를 반환 (narrow transfer라도 워드 전체)
→ 모델도 워드 전체를 조립
→ 그중 유효한 레인만 스코어보드가 mask로 비교

쓸 때: apply_write로 mem 채움
읽을 때: expected_beat로 "정답" 만들어서 스코어보드에 줌
→ 스코어보드가 DUT 실제 값과 비교
"DUT가 준 답"과 "정답지(ref_model)"를 비교해서
맞았는지 틀렸는지 판정하는 채점관
class axi4_scoreboard extends uvm_scoreboard;
`uvm_component_utils(axi4_scoreboard)
uvm_analysis_imp #(axi4_seq_item, axi4_scoreboard) ap_imp;
axi4_ref_model ref_model;
int unsigned n_writes, n_reads, n_beats, n_mismatch;
Monitor: uvm_analysis_port #(...) ap; ← 보내는 쪽
Scoreboard: uvm_analysis_imp #(...) ap_imp; ← 받는 쪽
imp = implementation → analysis port로 들어온 데이터를 받는 포트
#(axi4_seq_item, axi4_scoreboard) 파라미터uvm_analysis_imp #(T, IMP)
T = axi4_seq_item ← 받을 데이터 타입
IMP = axi4_scoreboard ← write() 함수를 가진 클래스 (나 자신!)
imp는 데이터를 받으면 IMP의 write() 함수를 자동 호출
→ 그래서 스코어보드에 write() 함수를 반드시 구현해야 함
n_writes : 쓰기 버스트 개수
n_reads : 읽기 버스트 개수
n_beats : 검사한 beat 개수
n_mismatch : 바이트 미스매치 개수 ← 이게 0이 아니면 버그!
function new(string name, uvm_component parent);
super.new(name, parent);
ap_imp = new("ap_imp", this); // analysis imp 생성
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ref_model = axi4_ref_model::type_id::create("ref_model");
endfunction
ap_imp = new(...) → new()에서 (TLM 포트라 Factory 아님)
ref_model = create(...) → build_phase에서 (Factory로, override 가능)
포트는 계층 연결에 필요 → 생성자에서 즉시
ref_model은 factory override 대상 → build_phase에서
function void write(axi4_seq_item tr);
if (tr.dir == AXI_WRITE) begin
ref_model.apply_write(tr); // 모델 업데이트
n_writes++;
end
else begin
check_read(tr); // 비교!
n_reads++;
end
endfunction
모니터가 ap.write(tr) 하면 → 이 함수가 자동 호출됨
WRITE 버스트: "메모리가 이렇게 됐어야 해" → 모델에 반영 (정답지 갱신)
READ 버스트: "실제로 맞나?" → check_read로 비교 (채점)
시퀀스: 0x1000에 쓰고 → 0x1000 읽기
1. write(0x1000) → apply_write → 모델 갱신
2. write(0x1000) → check_read → 모델과 비교
→ 쓰기가 먼저 반영돼야 읽기 비교가 정확
→ 모니터가 in-order로 보내는 게 전제
protected function void check_read(axi4_seq_item tr);
bit [AXI_DATA_WIDTH-1:0] exp;
bit [AXI_STRB_WIDTH-1:0] mask;
foreach (tr.data[i]) begin
exp = ref_model.expected_beat(tr, i); // 예상값
mask = tr.lane_mask(i); // 유효 레인
n_beats++;
for (int lane = 0; lane < AXI_STRB_WIDTH; lane++) begin
if (!mask[lane]) continue; // lane not addressed
if (tr.data[i][8*lane +: 8] !== exp[8*lane +: 8]) begin
n_mismatch++;
`uvm_error("SB_MISMATCH", $sformatf(
"READ mismatch @beat %0d lane %0d: addr=0x%04h burst=%s size=%0d len=%0d | expected 0x%02h, got 0x%02h",
i, lane, tr.beat_addr(i), tr.burst.name(), tr.size, tr.len,
exp[8*lane +: 8], tr.data[i][8*lane +: 8]))
end
end
end
endfunction
exp = ref_model.expected_beat(...) → 정답지에게 "이 주소 읽으면 뭐가 나와야 해?"
mask = tr.lane_mask(i) → "이 beat에서 실제로 볼 바이트가 어디야?"
// Only the byte lanes that the beat actually addresses are compared:
// for a narrow transfer the other lanes are don't-care in AXI, so
// checking them would produce false failures on a different slave.
narrow transfer(부분 전송)에서:
버스는 32비트인데 실제로는 일부 레인만 유효
나머지 레인은 AXI상 don't-care (아무 값이나 와도 됨)
모든 레인을 비교하면?
→ don't-care 레인의 쓰레기 값 때문에 false failure
→ mask[lane]이 1인 레인만 비교 → 유효한 데이터만 검사 → false failure 방지
!== 4-state 비교!== : 4-state 비교 (X/Z까지 정확히)
왜 != 아니고 !== ?
DUT가 X를 반환하면:
!= → 결과 X (불확실) → if가 애매하게 동작
!== → 명확히 "다름" → 미스매치로 확실히 잡음
→ seq_item do_compare의 === 와 같은 원리, X 전파 버그를 놓치지 않음
미스매치 시 출력:
@beat i, lane → 어느 beat, 어느 바이트에서
addr=beat_addr(i) → 정확한 주소 (WRAP이면 계산된 주소!)
burst=WRAP → 어떤 버스트 타입에서
size, len → 전송 크기/길이
expected vs got → 기대값 vs 실제값
→ WRAP 버그 발생 시:
addr가 "올바른 WRAP 주소"로 찍히고, DUT는 그 주소에 다른 값을 넣음
→ 버그 위치 특정 가능
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("SB", $sformatf(
"scoreboard: %0d write bursts, %0d read bursts, %0d beats checked, %0d byte mismatches",
n_writes, n_reads, n_beats, n_mismatch), UVM_LOW)
endfunction
시뮬레이션 끝에 출력:
scoreboard: 42 write bursts, 40 read bursts, 269 beats checked, 0 byte mismatches
n_mismatch = 0 → 모든 읽기가 예상과 일치 ✅
n_mismatch > 0 → 버그! (uvm_error가 이미 났을 것)
이력서의 "24 byte-level mismatches"가 바로 이 n_mismatch
→ WRAP 버그 있을 때 이 숫자가 24로 나왔던 것

1. ref_model이 beat_addr()로 "올바른" WRAP 주소 사용
→ DUT의 "틀린" 선형 주소와 다름
2. 비대칭(WRAP write + INCR read):
ref_model: WRAP write를 올바른 주소에 반영
DUT: WRAP write를 틀린 주소에 씀
INCR read: 양쪽 다 올바른 주소로 읽음
→ DUT가 틀린 곳에 썼으니 그 주소엔 원하는 값 없음
→ 미스매치 노출!

| Reference Model | Scoreboard | |
|---|---|---|
| 역할 | 정답지 (메모리 예측) | 심판 (비교/판정) |
| 베이스 | uvm_object (가벼움) | uvm_scoreboard |
| 메모리 | sparse byte array | 없음 (모델에 위임) |
| 핵심 함수 | apply_write / expected_beat | write / check_read |
| 독립성 | DUT 안 보고 스펙만 | — |
| 버그 검출 | beat_addr()로 올바른 주소 | mask로 유효 레인만 비교 |
핵심:
ref_model = "관찰한 쓰기 값"을 "스펙대로의 주소"에 기억해뒀다가
읽기 검증 때 정답으로 제시하는 장부
(값은 관찰에서, 주소 규칙은 스펙에서)
scoreboard = DUT 답을 정답지와 비교하는 채점관
자기가 직접 메모리 계산 안 함 → ref_model에게 물어보고 비교만
Q. 왜 골든 모델을 byte 단위로 만들었나?
A. write strobe를 정확히 모델링하기 위해. AXI는 바이트 레인 단위로
선택적 쓰기가 가능한데, 워드 단위 모델이면 이걸 표현 못 함.
Q. sparse memory를 쓴 이유는?
A. 64KB 전체를 배열로 잡으면 낭비. 연관 배열로 쓴 주소만 저장하고,
안 쓴 주소는 DUT 초기값 0x00으로 예측.
Q. narrow transfer에서 왜 mask로 걸러 비교하나?
A. narrow transfer는 일부 레인만 유효하고 나머지는 AXI상 don't-care.
전부 비교하면 don't-care 레인 때문에 false failure가 남.
Q. ref_model은 정답을 어디서 아나?
A. 미리 아는 게 아님. 모니터가 관찰한 쓰기 트랜잭션이 지나갈 때
그 값을 저장. 주소만 스펙(beat_addr)대로 계산.
Q. 이 구조가 어떻게 WRAP 버그를 잡았나?
A. ref_model이 beat_addr()로 올바른 WRAP 주소를 쓰는데 DUT는 선형
증가시킴. 비대칭 WRAP-write/INCR-read로 두 주소 계산이 갈라지게
해서 미스매치를 노출.