DV 면접의 핵심. "레퍼런스 모델을 어떻게 만드나"는 거의 반드시 나오는 질문이고,
이 스코어보드가 그 답이다. 단순 메모리 모델이 아니라 캐시 전체(태그/valid/dirty/
data/PLRU + DRAM)를 미러링하는 골든 모델로, 매 요청마다 hit/miss·way·eviction·
fill burst를 전부 예측하고 관측과 비교한다.
UVM에서 스코어보드 = "DUT 출력이 맞는지 판정하는 심판". 방식은 두 가지:
여기서는 골든 캐시 모델을 들고, 4개 소스에서 트랜잭션을 받아 판정한다:

uvm_analysis_imp_decl (3~6, 46~49행)`uvm_analysis_imp_decl(_req)
`uvm_analysis_imp_decl(_rsp)
`uvm_analysis_imp_decl(_mem)
`uvm_analysis_imp_decl(_rst)
왜 필요? analysis export는 콜백 함수가 write() 하나뿐이다. 한 컴포넌트가 여러
소스를 받으면 이름이 겹친다. uvm_analysis_imp_decl(_req)를 선언하면 write_req()
라는 별도 이름의 콜백이 생긴다. 그래서 4개 소스 → write_req/write_rsp/write_mem/ write_rst 4개 콜백.
uvm_analysis_imp_req #(l1_cache_item, l1_cache_scoreboard) req_export; // → write_req()
uvm_analysis_imp_rsp #(l1_cache_item, ...) rsp_export; // → write_rsp()
uvm_analysis_imp_mem #(l1_cache_mem_item, ...) mem_export; // → write_mem()
uvm_analysis_imp_rst #(l1_cache_reset_item, ...) rst_export; // → write_rst()
포트별 연결은 env에서:
req_ap → req_export (수락된 모든 요청, 발행 순서)rsp_ap → rsp_export (read 응답)ap → mem_export (burst 단위)ap → rst_exporttag_t m_tag [SETS][WAYS]; // DUT 태그 배열의 거울
bit m_valid [SETS][WAYS];
bit m_dirty [SETS][WAYS];
bit [31:0] m_data [SETS][WAYS][WORDS_PER_LINE];
plru_t m_plru [SETS]; // PLRU 트리
bit [31:0] main_mem [int]; // 예측 DRAM (워드 주소 → 값)
pending_read_t rd_q [$]; // 발행됐지만 아직 응답 안 온 read + 기대값
exp_mem_t exp_mem_q[$]; // 일어나야 할 memory burst 큐
핵심: 이전 버전은 메인 메모리만 모델링해서 "이건 hit이었어야 했다"를 판단할 수
없었다. 지금은 DUT의 태그/valid/dirty/data 4-way 전부 + PLRU + DRAM을 미러링하므로:
주소 분할(get_set 등)과 PLRU 함수(plru_victim/plru_update)는 RTL과 같은 패키지
함수를 쓴다 → 모델과 DUT가 주소 해석에서 어긋날 수 없음. (단 이건 양날 — §10)
write_req — 예측 엔진 (122~210행) ★★★요청이 수락되는 순간 호출. RTL의 동작을 그대로 흉내 내며 모델을 갱신하고,
read면 기대값을 큐에 넣고, miss면 예상 burst를 큐에 넣는다. 순서가 중요하다:
1. set/tag/word/line_addr 계산
2. hit 탐색: 4개 way에서 valid && tag일치 검색
3-A. hit → hit_cnt++, victim_state=NONE
3-B. miss → miss_cnt++
- victim 선택: invalid way 우선, 없으면 plru_victim (RTL과 동일 로직!)
- victim_state 분류: INVALID / CLEAN / DIRTY
- dirty victim이면:
exp_mem_q에 eviction(rw=1, victim 주소, 4 beat) push
main_mem에 victim 데이터 반영 ← DRAM에 씀
did_evict=1, evict_cnt++
- fill:
exp_mem_q에 fetch(rw=0, 새 라인 주소) push
m_data에 DRAM(mem_value)에서 4워드 로드
m_tag/m_valid=1/m_dirty=0 설정
4. way_use[way]++
5. 접근 반영:
write → m_data에 byte 단위 병합(be), m_dirty=1
read → exp_data = m_data[st][way][wd] 스냅샷, rd_q에 push
6. m_plru 갱신 (hit/fill 모두)
7. cov_ap.write(item) ← 주석 달린 item을 커버리지로
write-allocate가 정확히 모델링됨: dirty victim 데이터를 DRAM에 쓰고(step 3-B),
새 라인을 그 DRAM에서 fetch(mem_value). 실제 캐시의 evict→fill 순서와 같다.
byte 단위 write 반영 (194~195행):
for (int b = 0; b < BE_WIDTH; b++)
if (item.be[b]) m_data[st][way][wd][b*8 +: 8] = item.wdata[b*8 +: 8];
be가 켜진 레인만 갱신 → partial write를 RTL과 똑같이 흉내. 안 그러면 read 비교에서
가짜 실패.
// write_req 안에서 (read일 때):
p.exp_data = m_data[st][way][wd]; // ← 지금(발행 시점) 값을 잡아둠
rd_q.push_back(p);
문제: DUT가 파이프라인이라 read 응답이 나가는 바로 그 사이클에 다음 요청이 수락
될 수 있다. 응답 시점(write_rsp)에 "현재 모델"과 비교하면, 모델은 이미 다음 요청으로
앞서 나가 있어 가짜 실패가 난다. (실제로 이것 때문에 런당 2건씩 헛 실패했었음)
해결: read의 기대값을 발행 시점(write_req)에 스냅샷해서 rd_q에 넣고,
응답이 올 때(write_rsp) 꺼내서 비교. 수락된 모든 요청(read+write)이 req_export에
발행 순서대로 오므로, 시뮬레이터가 모니터 스레드를 어떤 순서로 스케줄하든
프로그램 순서가 보존된다.
이게 "파이프라인 DUT의 스코어보드를 어떻게 짰나"의 정답. core의 2-포트 모니터
(req_ap/rsp_ap)와 한 세트로 동작.
write_rsp — read 검증 (215~245행)p = rd_q.pop_front(); // FIFO — 발행 순서대로
if (p.addr !== item.addr) // 순서 검증
→ SCB_ORDER 에러
if (p.skip) // poison된 라인 → 데이터 비교 건너뜀
skipped_cnt++; return;
if (item.rdata === p.exp_data) match_cnt++; // 데이터 비교
else → READ_DATA_BUG 에러
rd_q가 비었는데 응답이 오면 → SCB_SYNC (응답이 요청보다 많음 = DUT가 없는 응답을 냄)SCB_ORDER (out-of-order 응답)skip은 async reset poison 처리(§7)write_mem — 메모리 burst 검증, 순서까지 (250~303행)관측된 burst를 exp_mem_q의 예측과 순서대로 대조. 4중 체크:
1. exp_mem_q 비었는데 burst 관측 → MEM_UNEXPECTED (없어야 할 트래픽)
2. 방향(rw) 불일치 → MEM_DIRECTION (evict를 fetch로 등)
3. 주소 불일치 → MEM_ADDR
4. beat 수 != WORDS_PER_LINE → MEM_LEN (짧거나 긴 burst)
5. (skip이면 데이터 비교 생략)
6. beat별 데이터 불일치 → WRITE_BACK_BUG / FILL_DATA_BUG
이 덕에 없어야 할 / 빠진 / 주소 틀린 / 순서 바뀐 / 길이 틀린 / 데이터 틀린 burst가
전부 잡힌다. "빠진 burst"는 여기서 안 잡히고 check_phase가 잡음(§8).
write_rst — flush + poison (94~117행) ★ 난이도 높음리셋을 걸기 직전 reset 드라이버가 알려주면 호출. 모델을 DUT처럼 flush하는데,
모델이 알 수 없는 상태를 격리하는 게 핵심.
// in-flight write-back은 일부 beat만 DRAM에 갔을 수 있음 → 그 라인 DRAM은 불확실
foreach (exp_mem_q[i])
if (exp_mem_q[i].rw == 1'b1) poisoned[exp_mem_q[i].addr] = 1'b1;
rd_q.delete(); // 진행 중이던 read 추적 버림
exp_mem_q.delete(); // 예측 burst 버림
for (each set) m_valid=0, m_dirty=0, m_plru=0; // DUT 플롭 flush와 동일
poison의 의미: write-back burst가 절반만 나간 채 리셋되면 DRAM이 예측 불가능한
상태가 된다. 그 라인 주소만 poison 표시하고, 이후 그 라인의 데이터 비교만
건너뛴다(rd_q/exp_mem_q의 skip). 프로토콜·순서·burst 길이 체크는 전부 유지.
"모델이 모르는 건 모른다고 인정하고 범위를 좁혀 격리". 무리하게 다 맞추려다 가짜
통과를 만드는 것보다 정직하고 안전. 리셋당 보통 ≤1라인, 카운트를 리포트에 찍음.
m_data/main_mem은 안 지움 — valid=0이라 fresh fill 없이는 안 읽힘.
check_phase — 끝판 안전망 (308~317행) ★"조용히 통과"를 막는 최종 검사. 에러 0이 검증됨이 아니다의 실천.
total_reads==0 && total_writes==0 → SCB_NO_TRAFFIC (트래픽 전무 = 무의미한 통과)
rd_q.size() != 0 → SCB_DANGLING (응답 안 온 read = DUT가 응답 누락)
exp_mem_q.size() != 0 → SCB_MEM_MISSING (일어났어야 할 burst가 안 일어남)
mismatch_cnt != 0 → SCB_FAILED
run_phase가 아니라 check_phase에 둔 이유: 모든 트랜잭션이 처리된 뒤,
시뮬레이션 종료 직전에 도는 UVM 페이즈라 "남은 것"을 정확히 셀 수 있다.
function bit [31:0] mem_value(bit [31:0] word_addr);
return main_mem.exists(word_addr) ? main_mem[word_addr] : `L1_MEM_DEFAULT(word_addr);
endfunction
// `L1_MEM_DEFAULT(a) = 32'hDEAD_0000 | (a & 0xFFFF) ← 주소마다 다른 값
아무도 안 쓴 주소를 읽으면 DRAM이 주는 값. 상수(예: 0xDEADBEEF)로 하면 안 되는 이유:
cold fill의 4 beat가 전부 똑같아 보여서 워드 선택/순서 버그가 숨는다. 주소 의존이면
beat마다 값이 달라 그런 버그가 드러난다. 드라이버(l1_cache_mem_driver)도 같은 식을
써야 모델과 일치.
PLRU/주소 함수를 RTL과 공유한다. 모델이 plru_victim/get_set 등 RTL과 같은
패키지 함수를 쓴다. 장점은 주소 해석 불일치가 원천 봉쇄. 단점은 그 함수 자체에
버그가 있으면 모델과 DUT가 같이 틀려서 못 잡는다(whitebox reference model의 고전적
약점). 여기선 데이터/eviction 주소/순서는 독립적으로 검증하므로, 못 잡는 건 순수
"교체 정책 결정" 한 가지뿐. 진짜 독립성을 원하면 PLRU를 모델에 따로 구현해야 한다.
→ 이걸 말할 수 있으면 "reference model의 독립성 trade-off를 이해한다"로 읽힌다.
| 질문 | 답 |
|---|---|
| 스코어보드가 메모리만 모델링하면 안 되나? | hit/miss·way를 판단 못 함. "이건 hit이었어야" "이 evict는 없었어야"를 못 잡음 |
uvm_analysis_imp_decl는 왜? | 한 컴포넌트가 여러 analysis를 받을 때 write() 이름 충돌 방지, write_req 등 별도 콜백 생성 |
| 파이프라인 DUT에서 read 순서 문제 해결? | 기대값을 발행 시점에 스냅샷(rd_q), 응답 시점에 pop&비교. 발행 순서 보존 |
| memory burst의 어떤 오류를 잡나? | 없어야 할/빠진/주소틀린/순서바뀐/길이틀린/데이터틀린 burst 전부 |
| 빠진 burst는 어디서 잡히나? | write_mem이 아니라 check_phase의 exp_mem_q 잔여 검사 |
| check_phase가 왜 필요? | 응답 누락·예측 burst 미발생·무트래픽 등 "조용한 통과"를 종료 직전에 잡음 |
| 리셋 중 write-back이 끊기면? | 그 라인 poison → 데이터 비교만 skip, 프로토콜/순서 체크는 유지 |
| 왜 DRAM 기본값을 주소 의존으로? | 상수면 cold fill 4 beat가 동일 → 워드 선택/순서 버그를 숨김 |
| 이 골든 모델의 한계? | PLRU/주소 함수를 RTL과 공유 → 그 함수 자체 버그는 못 잡음 |
write_mem에서 "빠진 burst"를 못 잡는 이유와, 그럼 어디서 잡히나?exp_mem_q에 몇 개가, 어떤 순서로 push되나?0xDEADBEEF면 어떤 버그가 숨나?