AXI4 UVM (6) — ref_model & scoreboard

Seungyun Lee·2026년 7월 30일

AXI4_UVM_FULL

목록 보기
9/16

시리즈: 아이템 · 인터페이스 · 드라이버 · 모니터 · 관계 · ref_model/scoreboard


전체 그림 — 검증의 심판대

모니터가 관찰한 트랜잭션이 최종적으로 "맞는지 틀린지" 판정되는 곳이다.

Monitor가 관찰한 트랜잭션
        │ ap.write(tr)
        ↓
   ┌─────────────────────────────────────┐
   │          Scoreboard (심판)           │
   │                                     │
   │  WRITE → ref_model.apply_write()    │  "이렇게 됐어야 해"
   │  READ  → check_read()               │  "실제로 맞나 비교"
   │              │                      │
   │              ↓                      │
   │  ┌────────────────────────┐         │
   │  │  Reference Model        │         │
   │  │  (골든 모델 = 정답지)    │         │
   │  │  byte 단위 sparse memory │         │
   │  └────────────────────────┘         │
   └─────────────────────────────────────┘

핵심 분업:

  • Reference Model = "메모리가 이렇게 됐어야 한다"를 계산하는 정답지
  • Scoreboard = 정답지와 DUT 실제 결과를 비교하는 심판

Part 1: axi4_ref_model — 골든 메모리 모델

설계 철학 (주석이 핵심)

// 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() 같은 계산 규칙을 코드에 박아둔 것이다.

정리하면:

  • 정답의 은 관찰한 쓰기에서 온다 (0xAB를 쓰는 걸 보면 0xAB를 기억)
  • 정답의 위치(주소)만 스펙을 따른다 (beat_addr가 WRAP/INCR를 스펙대로 계산)

클래스 선언 & 메모리 구조

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_component가 아니라 uvm_object인가

uvm_object 상속:
  → 계층 구조(hierarchy)에 안 들어감
  → phase(build/run/report)가 없음
  → 그냥 "데이터를 담는 객체"

이유:
  ref_model은 스코어보드가 소유하고 호출하는 도구
  독립적으로 phase를 돌 필요 없음 → 가벼운 uvm_object가 적합

mem — Associative Array (연관 배열)

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)

함수 3개만 이해하면 끝

1. read_byte — "이 주소 값 뭐야?"

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와 일치

2. apply_write — "쓰기를 정답지에 반영" (핵심)

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이 변수라 에러
           +: 는 시작점이 변수여도 됨 (폭은 상수)

3. expected_beat — "읽으면 뭐가 나와야 해?"

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 실제 값과 비교

Part 2: axi4_scoreboard — 심판

역할

"DUT가 준 답"과 "정답지(ref_model)"를 비교해서
맞았는지 틀렸는지 판정하는 채점관

클래스 선언 & analysis port

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;

uvm_analysis_imp — 모니터의 반대편

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이 아니면 버그!

new() & build_phase()

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에서

write() — 진입점

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로 비교 (채점)

순서가 중요 — write 먼저, read 나중

시퀀스: 0x1000에 쓰고 → 0x1000 읽기
  1. write(0x1000) → apply_write → 모델 갱신
  2. write(0x1000) → check_read → 모델과 비교

→ 쓰기가 먼저 반영돼야 읽기 비교가 정확
→ 모니터가 in-order로 보내는 게 전제

check_read() — 실제 비교 (핵심)

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에서 실제로 볼 바이트가 어디야?"

mask로 유효 레인만 비교 (매우 중요)

// 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는 그 주소에 다른 값을 넣음
  → 버그 위치 특정 가능

report_phase() — 최종 성적표

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로 나왔던 것

Part 3: 전체 데이터 흐름

Write 경로 & Read 경로 (버그 검출)

왜 이 구조가 WRAP 버그를 잡았나

1. ref_model이 beat_addr()로 "올바른" WRAP 주소 사용
   → DUT의 "틀린" 선형 주소와 다름

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

전체 판정 흐름도


한눈에 정리

Reference ModelScoreboard
역할정답지 (메모리 예측)심판 (비교/판정)
베이스uvm_object (가벼움)uvm_scoreboard
메모리sparse byte array없음 (모델에 위임)
핵심 함수apply_write / expected_beatwrite / 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로 두 주소 계산이 갈라지게
   해서 미스매치를 노출.
profile
Design Verification engineer

0개의 댓글