<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Texas_DV_engineer</title>
        <link>https://velog.io/</link>
        <description>Design Verification engineer</description>
        <lastBuildDate>Thu, 27 Aug 2026 22:12:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Texas_DV_engineer</title>
            <url>https://velog.velcdn.com/images/houston_guy2/profile/d90acedc-a60f-469f-8221-09efa3e93415/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Texas_DV_engineer. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/houston_guy2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[VLSI Lec 2]]></title>
            <link>https://velog.io/@houston_guy2/VLSI-Lec-2</link>
            <guid>https://velog.io/@houston_guy2/VLSI-Lec-2</guid>
            <pubDate>Thu, 27 Aug 2026 22:12:50 GMT</pubDate>
            <description><![CDATA[<h1 id="mosfet-동작-원리-정리-vlsi-기초">MOSFET 동작 원리 정리 (VLSI 기초)</h1>
<blockquote>
<p>NMOS/PMOS 구조부터 차단 · 선형 · 포화 영역과 핀치오프까지</p>
</blockquote>
<hr>
<h2 id="1-mosfet의-구조">1. MOSFET의 구조</h2>
<p>MOSFET = <strong>M</strong>etal–<strong>O</strong>xide–<strong>S</strong>emiconductor <strong>F</strong>ield <strong>E</strong>ffect <strong>T</strong>ransistor.
금속(요즘은 폴리실리콘) 게이트가 산화막을 사이에 두고 반도체 위에 얹혀 있는 구조.</p>
<h3 id="nmos-단면도">NMOS 단면도</h3>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/0ec76b11-81ff-482d-8ed9-009be48dd9fd/image.png" alt=""></p>
<h3 id="꼭-챙겨야-할-두-가지">꼭 챙겨야 할 두 가지</h3>
<p><strong>① 단자는 4개다.</strong>
G, S, D 말고 <strong>바디(B, bulk/substrate)</strong> 가 있다. 수업에서 보통 생략하는데,
NMOS는 바디를 GND에, PMOS는 VDD에 묶어두기 때문에 안 그려도 되는 것뿐이다.
바디 전압이 소스와 달라지면 <code>Vt</code>가 변한다 (<strong>body effect</strong>, → 10장).</p>
<p><strong>② 게이트는 채널과 전기적으로 완전히 절연되어 있다.</strong>
산화막(SiO₂)이 사이에 있으므로 DC에서 <strong>게이트 전류 = 0</strong>.
이것이 BJT 대비 MOSFET의 결정적 장점이고, 게이트는 사실상 <strong>커패시터</strong>로 동작한다.</p>
<p>게이트–채널 사이의 단위면적당 커패시턴스:</p>
<p>$$C_{ox} = \frac{\varepsilon_{ox}}{t_{ox}}$$</p>
<ul>
<li>$\varepsilon_{ox}$ : 산화막 유전율</li>
<li>$t_{ox}$ : 산화막 두께 (얇을수록 $C_{ox}$ ↑ → 구동력 ↑)</li>
</ul>
<hr>
<h2 id="2-소스와-드레인은-어떻게-구분하나">2. 소스와 드레인은 어떻게 구분하나</h2>
<p>구조적으로 두 n+ 영역은 <strong>완전히 대칭</strong>이다.
어느 쪽이 소스인지는 공정이 아니라 <strong>전압이 결정</strong>한다.</p>
<p>기준은 &quot;캐리어가 나오는 쪽(<strong>source</strong>)&quot;.</p>
<table>
<thead>
<tr>
<th>소자</th>
<th>캐리어</th>
<th>흐르는 방향</th>
<th>소스는</th>
</tr>
</thead>
<tbody><tr>
<td>NMOS</td>
<td>전자</td>
<td>낮은 전위 → 높은 전위</td>
<td><strong>전압이 낮은 쪽</strong> (GND 쪽)</td>
</tr>
<tr>
<td>PMOS</td>
<td>정공</td>
<td>높은 전위 → 낮은 전위</td>
<td><strong>전압이 높은 쪽</strong> (VDD 쪽)</td>
</tr>
</tbody></table>
<h3 id="자주-나오는-함정">자주 나오는 함정</h3>
<p>$$V_{gs} = V_g - V_s = V_g$$</p>
<p>이 단순화는 <strong>소스가 GND(0 V)에 물려 있을 때만</strong> 성립한다.</p>
<ul>
<li>CMOS pull-down network의 <strong>맨 아래</strong> 트랜지스터 → 소스가 GND → 성립</li>
<li>NAND 게이트에서 <strong>위에 쌓인</strong> NMOS → 소스가 떠 있음 → $V_s \neq 0$ → 성립 안 함</li>
</ul>
<hr>
<h2 id="3-v_gs가-하는-일--채널-만들기">3. $$V_{gs}$$가 하는 일 — 채널 만들기</h2>
<p>게이트에 전압을 걸면 p형 기판 표면이 세 단계로 변한다.
이것이 문턱 전압 $V_t$의 정체다.</p>
<h3 id="a-v_gs--0--축적-accumulation">(a) <code>$$V_{gs}$$ &lt; 0</code> — 축적 (accumulation)</h3>
<p>게이트가 음전하 → p형의 <strong>다수 캐리어인 정공</strong>이 표면으로 끌려옴 → 표면이 더 강한 p형.</p>
<p>결과: <code>n+ / p / p+표면 / n+</code> 구조 = <strong>back-to-back PN 접합</strong>.
어느 방향으로 전압을 걸어도 둘 중 하나는 역바이어스.</p>
<p>→ <strong>I = 0</strong></p>
<h3 id="b-0--v_gs--c--공핍-depletion">(b) <code>0 &lt; $$V_{gs}$$ &lt; c</code> — 공핍 (depletion)</h3>
<p>게이트 양전하가 정공을 밀어냄 → 표면에 공핍층 형성
(움직이지 않는 acceptor 음이온만 남음).</p>
<p><strong>아직 전자가 없어서 여전히 채널이 없다.</strong></p>
<p>→ <strong>I ≈ 0</strong></p>
<h3 id="c-v_gs--v_t--반전-inversion">(c) <code>$$V_{gs}$$ &gt; $$V_t$$</code> — 반전 (inversion)</h3>
<p>게이트 전압을 더 올리면 <strong>소수 캐리어인 전자</strong>가 표면으로 끌려와 모인다.
표면이 p형에서 <strong>n형으로 반전(inverted)</strong>.</p>
<p>이제 <code>n+ 소스 — n채널 — n+ 드레인</code>이 하나로 연결된다.</p>
<p>→ <strong>전류 통로 생성</strong></p>
<blockquote>
<p><strong>$V_t$의 엄밀한 정의</strong>
표면의 전자 농도가 기판의 정공 농도와 같아지는 순간 (strong inversion, $\psi_s = 2\phi_F$).
최신 공정: $V_{t0} \approx 0.3 \sim 0.5\ \mathrm{V}$
교과서 예제: $0.5 \sim 0.7\ \mathrm{V}$</p>
</blockquote>
<h3 id="정리">정리</h3>
<p>노트에 &quot;<code>Vgs &lt; 0</code> → <code>Vgs &lt; Vt</code> → <code>I = 0</code>&quot;이라고 쓴 것은 <strong>맞다</strong>.
다만 조건의 본질은 <code>Vgs &lt; 0</code>이 아니라 <strong><code>Vgs &lt; Vt</code></strong> 다.
$V_{gs}$가 <strong>양수여도</strong> $V_t$보다 작으면 (b) 상태라 여전히 채널이 없다.</p>
<hr>
<h2 id="4-vds가-하는-일--캐리어-흘리기">4. Vds가 하는 일 — 캐리어 흘리기</h2>
<p>채널이 만들어진 상태에서 $V_{ds} &gt; 0$을 걸면 전자가 소스 → 드레인으로 drift한다.
$V_{ds}$ 크기에 따라 <strong>채널 모양</strong>이 달라지고, 그것이 세 가지 동작 영역이 된다.</p>
<h3 id="세-영역의-채널-모양">세 영역의 채널 모양</h3>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/728ec01a-f890-48b3-9687-78840785160b/image.png" alt=""></p>
<h3 id="왜-드레인-쪽-채널이-얇아지나">왜 드레인 쪽 채널이 얇아지나</h3>
<p><strong>핵심 아이디어 하나만 잡으면 다 풀린다.</strong></p>
<blockquote>
<p>채널을 유지하는 힘은 그 지점에서의 <strong>&quot;게이트 − 채널&quot; 전압차</strong> 다.</p>
</blockquote>
<p>소스에서 드레인 방향 위치를 $y$, 그 지점의 채널 전위를 $V(y)$라 하면:</p>
<table>
<thead>
<tr>
<th>위치</th>
<th>채널 전위</th>
<th>게이트–채널 전압</th>
</tr>
</thead>
<tbody><tr>
<td>소스 끝 ($y=0$)</td>
<td>$0$</td>
<td>$V_{gs}$ ← 가장 강함</td>
</tr>
<tr>
<td>드레인 끝 ($y=L$)</td>
<td>$V_{ds}$</td>
<td>$V_{gs} - V_{ds} = V_{gd}$ ← 가장 약함</td>
</tr>
</tbody></table>
<p>채널 전하 밀도:</p>
<p>$$Q(y) = -C_{ox}\big(V_{gs} - V(y) - V_t\big)$$</p>
<p>드레인 쪽으로 갈수록 전하가 줄어들어 채널이 얇아진다 → <strong>삼각형 모양</strong>.</p>
<h3 id="선형-영역-linear--triode--ohmic">선형 영역 (linear / triode / ohmic)</h3>
<p>위 전하 분포를 채널 전체에 대해 적분하면:</p>
<p>$$I_{ds} = \mu_n C_{ox}\frac{W}{L}\left[(V_{gs}-V_t)V_{ds} - \frac{V_{ds}^2}{2}\right]$$</p>
<p>$V_{ds} \ll (V_{gs}-V_t)$ 이면 $V_{ds}^2$ 항이 무시되어:</p>
<p>$$I_{ds} \approx \mu_n C_{ox}\frac{W}{L}(V_{gs}-V_t),V_{ds}$$</p>
<p>즉 $I \propto V_{ds}$ → <strong>옴의 법칙을 따르는 저항</strong>처럼 보인다.</p>
<p>등가 저항:</p>
<p>$$R_{on} = \frac{1}{\mu_n C_{ox}\dfrac{W}{L}(V_{gs}-V_t)}$$</p>
<p>게이트 전압으로 저항값을 조절하는 <strong>전압제어 가변저항</strong>인 셈.
→ Transmission gate, pass transistor가 바로 이 영역을 사용한다.</p>
<h3 id="포화-영역-saturation--핀치오프">포화 영역 (saturation) — 핀치오프</h3>
<p>드레인 쪽 게이트–채널 전압 $V_{gd} = V_{gs} - V_{ds}$가 계속 줄어들다가,
어느 순간 $V_{gd} = V_t$가 된다. 그 지점에서 채널 전하가 정확히 0 → <strong>핀치오프(pinch-off)</strong>.</p>
<p>$$V_{gs} - V_{ds} = V_t \quad\Longrightarrow\quad \boxed{V_{ds,sat} = V_{gs} - V_t}$$</p>
<p>이 $V_{gs} - V_t$를 <strong>overdrive voltage</strong> $V_{ov}$ 또는 $V_{eff}$라고 부른다.
앞으로 계속 나오는 값이니 이름을 외워둘 것.</p>
<p>이 값을 선형 식에 대입하면 포화 전류:</p>
<p>$$I_{ds} = \frac{\mu_n C_{ox}}{2}\frac{W}{L}(V_{gs}-V_t)^2$$</p>
<p><strong>여기에 $V_{ds}$가 없다.</strong> → &quot;does not follow Ohm&#39;s law&quot;가 이 뜻.</p>
<hr>
<h2 id="5-iv-특성-곡선">5. I–V 특성 곡선</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/1e69276f-f7bf-4bd3-a787-7623b7132044/image.png" alt=""></p>
<ul>
<li>곡선 하나하나가 특정 $V_{gs}$에 대응</li>
<li>점선(핀치오프 궤적) <strong>왼쪽 = 선형 영역</strong>, <strong>오른쪽 = 포화 영역</strong></li>
<li>$V_{gs}$가 커질수록 포화 전류가 <strong>제곱으로</strong> 커진다</li>
</ul>
<hr>
<h2 id="6-핀치오프인데-왜-전류가-0이-아닌가">6. 핀치오프인데 왜 전류가 0이 아닌가</h2>
<blockquote>
<p>&quot;채널이 끊겼는데 왜 전류가 흐르지?&quot; — 가장 많이 막히는 지점</p>
</blockquote>
<h3 id="1-채널이-완전히-끊긴-게-아니다">(1) 채널이 완전히 끊긴 게 아니다</h3>
<p>반전층(strong inversion 수준의 전하)이 사라진 것이지,
그 자리가 절연체가 된 게 아니다. 그 구간은 좁은 <strong>공핍 영역</strong>이 된다.</p>
<h3 id="2-그-공핍-영역에-매우-강한-횡전계가-걸린다">(2) 그 공핍 영역에 매우 강한 횡전계가 걸린다</h3>
<ul>
<li>소스 ~ 핀치오프 지점: 항상 $V_{ds,sat} = V_{gs}-V_t$ 가 걸림 (<strong>고정</strong>)</li>
<li>초과분 $(V_{ds} - V_{ds,sat})$: 전부 이 <strong>좁은</strong> 공핍 구간에 몰림</li>
<li>폭이 좁으니 전계는 어마어마함</li>
</ul>
<p>채널을 타고 온 전자가 핀치오프 지점에 도착하면 이 전계에 <strong>즉시 쓸려서(swept)</strong> 드레인으로 빨려 들어간다.</p>
<h3 id="3-전류를-제한하는-건-채널-쪽이다">(3) 전류를 제한하는 건 채널 쪽이다</h3>
<p>채널이 공급할 수 있는 만큼만 흐르고,
채널에 걸리는 전압은 $V_{ds}$를 아무리 올려도 $V_{ds,sat}$로 고정.</p>
<p>→ <strong>전류 포화</strong></p>
<blockquote>
<p><strong>비유</strong>
수도관 끝에서 물이 폭포로 떨어지는 상황.
폭포 높이($V_{ds}$)를 아무리 높여도 관을 통과하는 유량은 <strong>관</strong>이 정하지, 낙차가 정하지 않는다.</p>
</blockquote>
<h3 id="실제로는-완전히-평평하지-않다--channel-length-modulation">실제로는 완전히 평평하지 않다 — Channel Length Modulation</h3>
<p>$V_{ds}$를 더 올리면 핀치오프 지점이 소스 쪽으로 조금씩 이동
→ 유효 채널 길이 $L_{eff}$ 감소 → 전류 소폭 증가.</p>
<p>$$I_{ds} = \frac{\mu_n C_{ox}}{2}\frac{W}{L}(V_{gs}-V_t)^2,(1+\lambda V_{ds})$$</p>
<p>출력 저항:</p>
<p>$$r_o \approx \frac{1}{\lambda I_{ds}}$$</p>
<p>아날로그 증폭기 이득을 결정하는 핵심 파라미터. 디지털에서는 대개 무시한다.</p>
<hr>
<h2 id="7-세-영역-정리">7. 세 영역 정리</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/17c03a49-e8cf-4023-bc5e-3261139c4854/image.png" alt=""></p>
<blockquote>
<p><strong>암기용 한 줄</strong>
<strong>$V_{gs}$가 켜는지 결정하고, $V_{ds}$가 어떻게 켜지는지 결정한다.</strong></p>
</blockquote>
<hr>
<h2 id="8-pmos">8. PMOS</h2>
<p>모든 극성이 반대. n-well 안에 p+ 소스/드레인을 만들고, 캐리어는 <strong>정공</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/6decc2a6-87d0-43e5-8c53-c6b9278dd75c/image.png" alt=""></p>
<h3 id="부등호-뒤집히는-게-헷갈릴-때">부등호 뒤집히는 게 헷갈릴 때</h3>
<p><strong>절댓값으로 생각하면 NMOS와 완전히 같은 문장이 된다.</strong></p>
<ul>
<li>PMOS도 $|V_{gs}| &gt; |V_{tp}|$ 면 켜진다</li>
<li>PMOS도 $|V_{ds}| \ge |V_{gs}| - |V_{tp}|$ 면 포화다</li>
</ul>
<h3 id="레이아웃에-직결되는-사실">레이아웃에 직결되는 사실</h3>
<p>$\mu_p &lt; \mu_n$ 이므로 같은 $W/L$이면 PMOS가 <strong>2~3배 약하다</strong>.
CMOS 인버터의 상승/하강 시간을 맞추려면 PMOS의 $W$를 <strong>2~3배</strong>로 키운다.</p>
<p>→ 인버터 그림에서 PMOS가 항상 뚱뚱하게 그려지는 이유.</p>
<hr>
<h2 id="9-왜-cmos는-nmos를-아래-pmos를-위에-두는가">9. 왜 CMOS는 NMOS를 아래, PMOS를 위에 두는가</h2>
<p>앞 내용의 직접적인 귀결.</p>
<p><strong>NMOS로 1(VDD)을 전달한다고 해보자.</strong></p>
<p>게이트는 VDD로 고정인데, 출력이 올라가면 <strong>소스 전압도 같이 올라간다</strong>.</p>
<p>$$V_{gs} = V_{DD} - V_{out}$$</p>
<p>$V_{out}$이 오를수록 $V_{gs}$가 줄어들고,
$V_{out} = V_{DD} - V_{tn}$ 이 되는 순간 $V_{gs} = V_{tn}$ → <strong>트랜지스터가 꺼진다.</strong></p>
<p>출력이 VDD까지 못 간다. (&quot;threshold drop&quot;)</p>
<table>
<thead>
<tr>
<th>소자</th>
<th>강한 신호</th>
<th>약한 신호</th>
<th>배치</th>
</tr>
</thead>
<tbody><tr>
<td><strong>NMOS</strong></td>
<td>strong <strong>0</strong></td>
<td>weak 1</td>
<td><strong>pull-down network</strong> (아래)</td>
</tr>
<tr>
<td><strong>PMOS</strong></td>
<td>strong <strong>1</strong></td>
<td>weak 0</td>
<td><strong>pull-up network</strong> (위)</td>
</tr>
</tbody></table>
<hr>
<h2 id="10-2차-효과-곧-나올-것들">10. 2차 효과 (곧 나올 것들)</h2>
<p>이름만 미리 알아두면 좋은 것들.</p>
<h3 id="subthreshold-conduction-문턱-이하-전도">Subthreshold conduction (문턱 이하 전도)</h3>
<p>$V_{gs} &lt; V_t$ 에서도 전류가 완전히 0은 아니다.
$V_{gs}$에 <strong>지수적으로</strong> 의존하는 누설 전류가 흐른다.</p>
<p>$$I_{ds} \propto e^{,V_{gs}/(nV_T)}$$</p>
<p>→ 저전력 설계에서 결정적. 대기 전력(standby power)의 주범.</p>
<h3 id="body-effect-기판-효과">Body effect (기판 효과)</h3>
<p>$V_{sb} &gt; 0$ 이면 문턱값이 올라간다.</p>
<p>$$V_t = V_{t0} + \gamma\left(\sqrt{2\phi_F + V_{sb}} - \sqrt{2\phi_F}\right)$$</p>
<ul>
<li>$\gamma$ : body effect coefficient</li>
<li>$\phi_F$ : Fermi potential</li>
</ul>
<p>→ NAND처럼 트랜지스터가 <strong>직렬로 쌓일 때</strong> 위쪽 트랜지스터가 느려지는 원인.</p>
<h3 id="velocity-saturation-속도-포화">Velocity saturation (속도 포화)</h3>
<p>최신 short-channel 공정에서는 전계가 너무 강해 캐리어 속도가 포화한다.</p>
<p>$$I_{ds} \propto (V_{gs}-V_t)^\alpha \quad,\quad \alpha \approx 1 \sim 1.3$$</p>
<p>→ <strong>제곱 법칙이 깨진다.</strong>
교과서의 제곱식은 <strong>long-channel 근사</strong>라는 점을 기억할 것.</p>
<hr>
<h2 id="11-핵심-공식-요약">11. 핵심 공식 요약</h2>
<h3 id="nmos">NMOS</h3>
<pre><code>동작 조건
  차단   : Vgs &lt; Vt
  선형   : Vgs &gt; Vt  AND  Vds &lt; Vgs − Vt
  포화   : Vgs &gt; Vt  AND  Vds ≥ Vgs − Vt

경계
  Vds,sat = Vgs − Vt  =  Vov  (overdrive voltage)</code></pre><p>$$\text{선형: } I_{ds} = \mu_n C_{ox}\frac{W}{L}\left[(V_{gs}-V_t)V_{ds} - \frac{V_{ds}^2}{2}\right]$$</p>
<p>$$\text{포화: } I_{ds} = \frac{\mu_n C_{ox}}{2}\frac{W}{L}(V_{gs}-V_t)^2,(1+\lambda V_{ds})$$</p>
<p>$$R_{on} = \frac{1}{\mu_n C_{ox}\frac{W}{L}(V_{gs}-V_t)} \qquad r_o \approx \frac{1}{\lambda I_{ds}}$$</p>
<p>$$C_{ox} = \frac{\varepsilon_{ox}}{t_{ox}} \qquad V_t = V_{t0} + \gamma\left(\sqrt{2\phi_F + V_{sb}} - \sqrt{2\phi_F}\right)$$</p>
<h3 id="자주-쓰는-파생-관계">자주 쓰는 파생 관계</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>식</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>전달컨덕턴스</td>
<td>$g_m = \dfrac{2I_{ds}}{V_{gs}-V_t}$</td>
<td>포화 영역, 아날로그</td>
</tr>
<tr>
<td>게이트 커패시턴스</td>
<td>$C_g = C_{ox},W L$</td>
<td>부하 계산</td>
</tr>
<tr>
<td>게이트 지연</td>
<td>$t_{pd} \propto R_{on} C_L$</td>
<td>디지털</td>
</tr>
</tbody></table>
<hr>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/5f0996b7-7f9d-41b0-be69-573a1479c3e8/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Lec 2 Performance]]></title>
            <link>https://velog.io/@houston_guy2/Lec-2-Performance</link>
            <guid>https://velog.io/@houston_guy2/Lec-2-Performance</guid>
            <pubDate>Wed, 26 Aug 2026 16:16:29 GMT</pubDate>
            <description><![CDATA[<h2 id="response-time-and-throughput">Response Time and Throughput</h2>
<ul>
<li>Response time (also called execution
time)<ul>
<li>How long it takes to do a task</li>
</ul>
</li>
<li>Throughput<ul>
<li>Total work done per unit time</li>
<li>e.g., tasks/transactions/… per hour</li>
</ul>
</li>
</ul>
<h2 id="realative-performance">Realative Performance</h2>
<p>$$
\frac {Performance_x} {Performance_y} = \frac {Execution\ time_y} {Execution\ time_x} = n
$$
x,y 분자 분모 뒤바뀌는거 주의
x가 n만큼 y보다 빠르다 or 느리다</p>
<h2 id="measuring-execution-time">Measuring Execution Time</h2>
<ul>
<li><p>Elapsed time ( total excution time, wall time)</p>
<ul>
<li>Total response time, Including all aspects<ul>
<li>$ time ls: measure time under the file, ls(list files in derectory)</li>
</ul>
</li>
</ul>
</li>
<li><p>CPU Time</p>
<ul>
<li>Time spent processing a given job</li>
</ul>
</li>
</ul>
<h2 id="cpu-time">CPU Time</h2>
<p>$$
CPU_{Time} = \frac {CPU\ Clock\ Cycles} {Clock\ Rate} = CPU\ Clock\ Cycle \times CCT
$$</p>
<p>거속시 관계처럼 유연하게 생각하기 
$$
Time = \frac{CC} {Rate} = CC \times CCT
$$</p>
<p>Perfomance improved by</p>
<ul>
<li>reducing number of clock cycle</li>
<li>increasing clock rate</li>
</ul>
<h3 id="example">Example</h3>
<p>Computer A: 2GHz clock, 10s CPU time
• Designing Computer B
– Aim for 6s CPU time
– Can do faster clock, but causes 1.2 × clock cycles</p>
<p>• How fast must Computer B clock be?</p>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/ef1a34e2-01d2-40fe-8ec8-e1be3498b161/image.png" alt=""></p>
<h2 id="cpi-cycle-per-insturction">CPI (Cycle per Insturction)</h2>
<p>Instruction Count for a program</p>
<ul>
<li>Determined by program, ISA and compiler</li>
</ul>
<p>$$
Clock Cycle = Instruction\ Count \times Cycle\ per\ Instruction
$$</p>
<p>$$
CPU_{Time} = \frac {IC \times CPI} {Clock\ Rate} = IC \times CPI \times CCT
$$</p>
<h3 id="isa-instruction-set-architecture">ISA (instruction set Architecture)</h3>
<ul>
<li><p>defines types of instuctions</p>
</li>
<li><p>defines addressing mode</p>
<p>  ALU: ADD R1, R2, R3 -&gt; Register addressing mode
  BNE R1, R2, L1
  LD R, 8(R2) -&gt; Indirect addressing mode</p>
</li>
</ul>
<p>bracket means no data -&gt; need to access memory</p>
<h4 id="risc-reduced-inst-set-computing">RISC (Reduced Inst Set Computing)</h4>
<ul>
<li>ADD R1 R2 R3: add R2, R3</li>
</ul>
<h4 id="cisc-complex-inst-set-computing">CISC (Complex Inst Set Computing)</h4>
<ul>
<li>ADD R1, (R2), (R3): bring R2, R3 data from memory and add -&gt; store to R1</li>
<li>include multiple instructions</li>
<li>inefficient</li>
</ul>
<pre><code class="language-c"> j = b(i) + c(i)

 RISC - MIPS, ARM
 LD R4, (R2)
 LD R5, (R3)
 ADD R1, R4, R5

 CISC
 ADD R1, (R2), (R3)</code></pre>
<p>Average cycles per instruction</p>
<ul>
<li>Determined by CPU hardware</li>
<li>If different instructions have different CPI</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[l1_cache_scoreboard.sv]]></title>
            <link>https://velog.io/@houston_guy2/l1cachescoreboard.sv</link>
            <guid>https://velog.io/@houston_guy2/l1cachescoreboard.sv</guid>
            <pubDate>Sun, 09 Aug 2026 06:04:35 GMT</pubDate>
            <description><![CDATA[<p><strong>DV 면접의 핵심.</strong> &quot;레퍼런스 모델을 어떻게 만드나&quot;는 거의 반드시 나오는 질문이고,
이 스코어보드가 그 답이다. 단순 메모리 모델이 아니라 <strong>캐시 전체(태그/valid/dirty/
data/PLRU + DRAM)를 미러링하는 골든 모델</strong>로, 매 요청마다 hit/miss·way·eviction·
fill burst를 전부 예측하고 관측과 비교한다.</p>
<hr>
<h2 id="0-스코어보드란--이-파일의-역할">0. 스코어보드란 / 이 파일의 역할</h2>
<p>UVM에서 스코어보드 = <strong>&quot;DUT 출력이 맞는지 판정하는 심판&quot;</strong>. 방식은 두 가지:</p>
<ul>
<li><strong>예측형(reference model)</strong>: 입력을 받아 &quot;정답&quot;을 스스로 계산 → DUT 출력과 비교. ← 이 프로젝트</li>
<li><strong>비교형</strong>: 두 DUT/두 경로 출력을 나란히 비교</li>
</ul>
<p>여기서는 <strong>골든 캐시 모델</strong>을 들고, 4개 소스에서 트랜잭션을 받아 판정한다:</p>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/0acd2572-357a-48c7-903f-7ac1844c3908/image.png" alt=""></p>
<hr>
<h2 id="1-4개-입력-포트--uvm_analysis_imp_decl-36-4649행">1. 4개 입력 포트 — <code>uvm_analysis_imp_decl</code> (3<del>6, 46</del>49행)</h2>
<pre><code class="language-systemverilog">`uvm_analysis_imp_decl(_req)
`uvm_analysis_imp_decl(_rsp)
`uvm_analysis_imp_decl(_mem)
`uvm_analysis_imp_decl(_rst)</code></pre>
<p><strong>왜 필요?</strong> analysis export는 콜백 함수가 <code>write()</code> 하나뿐이다. 한 컴포넌트가 여러
소스를 받으면 이름이 겹친다. <code>uvm_analysis_imp_decl(_req)</code>를 선언하면 <code>write_req()</code>
라는 <strong>별도 이름의 콜백</strong>이 생긴다. 그래서 4개 소스 → <code>write_req/write_rsp/write_mem/
write_rst</code> 4개 콜백.</p>
<pre><code class="language-systemverilog">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()</code></pre>
<p>포트별 연결은 <a href="../tb/env/l1_cache_env.sv">env</a>에서:</p>
<ul>
<li>CPU 모니터 <code>req_ap</code> → <code>req_export</code> (수락된 모든 요청, 발행 순서)</li>
<li>CPU 모니터 <code>rsp_ap</code> → <code>rsp_export</code> (read 응답)</li>
<li>메모리 모니터 <code>ap</code> → <code>mem_export</code> (burst 단위)</li>
<li>reset 드라이버 <code>ap</code> → <code>rst_export</code></li>
</ul>
<hr>
<h2 id="2-골든-모델--캐시-전체-5465행-★">2. 골든 모델 = 캐시 전체 (54~65행) ★</h2>
<pre><code class="language-systemverilog">tag_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 큐</code></pre>
<p><strong>핵심</strong>: 이전 버전은 <strong>메인 메모리만</strong> 모델링해서 &quot;이건 hit이었어야 했다&quot;를 판단할 수
없었다. 지금은 DUT의 <strong>태그/valid/dirty/data 4-way 전부 + PLRU + DRAM</strong>을 미러링하므로:</p>
<ul>
<li>hit/miss <strong>그리고 어느 way인지</strong> 예측</li>
<li>read가 반환할 <strong>정확한 데이터</strong>(byte 단위) 예측</li>
<li>dirty victim이면 <strong>어느 주소로 어떤 4 beat를</strong> 쓸지 예측</li>
<li>뒤따르는 <strong>fill burst</strong>와 그 데이터 예측</li>
</ul>
<p>주소 분할(<code>get_set</code> 등)과 PLRU 함수(<code>plru_victim</code>/<code>plru_update</code>)는 RTL과 <strong>같은 패키지
함수</strong>를 쓴다 → 모델과 DUT가 주소 해석에서 어긋날 수 없음. (단 이건 양날 — §10)</p>
<hr>
<h2 id="3-write_req--예측-엔진-122210행-★★★">3. <code>write_req</code> — 예측 엔진 (122~210행) ★★★</h2>
<p>요청이 수락되는 순간 호출. <strong>RTL의 동작을 그대로 흉내 내며</strong> 모델을 갱신하고,
read면 기대값을 큐에 넣고, miss면 예상 burst를 큐에 넣는다. 순서가 중요하다:</p>
<pre><code>1. set/tag/word/line_addr 계산
2. hit 탐색: 4개 way에서 valid &amp;&amp; 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을 커버리지로</code></pre><p><strong>write-allocate가 정확히 모델링됨</strong>: dirty victim 데이터를 DRAM에 쓰고(step 3-B),
새 라인을 그 DRAM에서 fetch(mem_value). 실제 캐시의 evict→fill 순서와 같다.</p>
<p><strong>byte 단위 write 반영</strong> (194~195행):</p>
<pre><code class="language-systemverilog">for (int b = 0; b &lt; BE_WIDTH; b++)
    if (item.be[b]) m_data[st][way][wd][b*8 +: 8] = item.wdata[b*8 +: 8];</code></pre>
<p>be가 켜진 레인만 갱신 → partial write를 RTL과 똑같이 흉내. 안 그러면 read 비교에서
가짜 실패.</p>
<hr>
<h2 id="4-순서-문제와-발행-시점-스냅샷-2328-200201행-★★★-면접-최다-질문">4. 순서 문제와 &quot;발행 시점 스냅샷&quot; (23<del>28, 200</del>201행) ★★★ 면접 최다 질문</h2>
<pre><code class="language-systemverilog">// write_req 안에서 (read일 때):
p.exp_data = m_data[st][way][wd];   // ← 지금(발행 시점) 값을 잡아둠
rd_q.push_back(p);</code></pre>
<p><strong>문제</strong>: DUT가 파이프라인이라 <strong>read 응답이 나가는 바로 그 사이클에 다음 요청이 수락</strong>
될 수 있다. 응답 시점(<code>write_rsp</code>)에 &quot;현재 모델&quot;과 비교하면, 모델은 이미 다음 요청으로
앞서 나가 있어 <strong>가짜 실패</strong>가 난다. (실제로 이것 때문에 런당 2건씩 헛 실패했었음)</p>
<p><strong>해결</strong>: read의 기대값을 <strong>발행 시점(<code>write_req</code>)에 스냅샷</strong>해서 <code>rd_q</code>에 넣고,
응답이 올 때(<code>write_rsp</code>) 꺼내서 비교. 수락된 모든 요청(read+write)이 <code>req_export</code>에
<strong>발행 순서대로</strong> 오므로, 시뮬레이터가 모니터 스레드를 어떤 순서로 스케줄하든
프로그램 순서가 보존된다.</p>
<blockquote>
<p>이게 &quot;파이프라인 DUT의 스코어보드를 어떻게 짰나&quot;의 정답. core의 2-포트 모니터
(<code>req_ap</code>/<code>rsp_ap</code>)와 한 세트로 동작.</p>
</blockquote>
<hr>
<h2 id="5-write_rsp--read-검증-215245행">5. <code>write_rsp</code> — read 검증 (215~245행)</h2>
<pre><code class="language-systemverilog">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 에러</code></pre>
<ul>
<li><code>rd_q</code>가 비었는데 응답이 오면 → <code>SCB_SYNC</code> (응답이 요청보다 많음 = DUT가 없는 응답을 냄)</li>
<li>주소 순서 불일치 → <code>SCB_ORDER</code> (out-of-order 응답)</li>
<li><code>skip</code>은 async reset poison 처리(§7)</li>
</ul>
<hr>
<h2 id="6-write_mem--메모리-burst-검증-순서까지-250303행">6. <code>write_mem</code> — 메모리 burst 검증, 순서까지 (250~303행)</h2>
<p>관측된 burst를 <code>exp_mem_q</code>의 예측과 <strong>순서대로</strong> 대조. 4중 체크:</p>
<pre><code>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</code></pre><p>이 덕에 <strong>없어야 할 / 빠진 / 주소 틀린 / 순서 바뀐 / 길이 틀린 / 데이터 틀린</strong> burst가
전부 잡힌다. &quot;빠진 burst&quot;는 여기서 안 잡히고 <code>check_phase</code>가 잡음(§8).</p>
<hr>
<h2 id="7-write_rst--flush--poison-94117행-★-난이도-높음">7. <code>write_rst</code> — flush + poison (94~117행) ★ 난이도 높음</h2>
<p>리셋을 걸기 직전 reset 드라이버가 알려주면 호출. 모델을 DUT처럼 flush하는데,
<strong>모델이 알 수 없는 상태</strong>를 격리하는 게 핵심.</p>
<pre><code class="language-systemverilog">// in-flight write-back은 일부 beat만 DRAM에 갔을 수 있음 → 그 라인 DRAM은 불확실
foreach (exp_mem_q[i])
    if (exp_mem_q[i].rw == 1&#39;b1) poisoned[exp_mem_q[i].addr] = 1&#39;b1;

rd_q.delete();        // 진행 중이던 read 추적 버림
exp_mem_q.delete();   // 예측 burst 버림

for (each set) m_valid=0, m_dirty=0, m_plru=0;   // DUT 플롭 flush와 동일</code></pre>
<p><strong>poison의 의미</strong>: write-back burst가 절반만 나간 채 리셋되면 DRAM이 예측 불가능한
상태가 된다. 그 <strong>라인 주소만 poison</strong> 표시하고, 이후 그 라인의 <strong>데이터 비교만
건너뛴다</strong>(rd_q/exp_mem_q의 <code>skip</code>). 프로토콜·순서·burst 길이 체크는 전부 유지.</p>
<blockquote>
<p>&quot;모델이 모르는 건 모른다고 인정하고 범위를 좁혀 격리&quot;. 무리하게 다 맞추려다 가짜
통과를 만드는 것보다 정직하고 안전. 리셋당 보통 ≤1라인, 카운트를 리포트에 찍음.
<code>m_data</code>/<code>main_mem</code>은 안 지움 — valid=0이라 fresh fill 없이는 안 읽힘.</p>
</blockquote>
<hr>
<h2 id="8-check_phase--끝판-안전망-308317행-★">8. <code>check_phase</code> — 끝판 안전망 (308~317행) ★</h2>
<p>&quot;조용히 통과&quot;를 막는 최종 검사. <strong>에러 0이 검증됨이 아니다</strong>의 실천.</p>
<pre><code class="language-systemverilog">total_reads==0 &amp;&amp; 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</code></pre>
<p><code>run_phase</code>가 아니라 <code>check_phase</code>에 둔 이유: 모든 트랜잭션이 처리된 뒤,
시뮬레이션 종료 직전에 도는 UVM 페이즈라 &quot;남은 것&quot;을 정확히 셀 수 있다.</p>
<hr>
<h2 id="9-영리한-디테일--주소-의존-dram-기본값-8389행">9. 영리한 디테일 — 주소 의존 DRAM 기본값 (83~89행)</h2>
<pre><code class="language-systemverilog">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&#39;hDEAD_0000 | (a &amp; 0xFFFF)  ← 주소마다 다른 값</code></pre>
<p>아무도 안 쓴 주소를 읽으면 DRAM이 주는 값. <strong>상수(예: 0xDEADBEEF)로 하면 안 되는 이유</strong>:
cold fill의 4 beat가 전부 똑같아 보여서 <strong>워드 선택/순서 버그가 숨는다</strong>. 주소 의존이면
beat마다 값이 달라 그런 버그가 드러난다. 드라이버(<code>l1_cache_mem_driver</code>)도 같은 식을
써야 모델과 일치.</p>
<hr>
<h2 id="10-솔직한-한계-면접에서-물으면-가점">10. 솔직한 한계 (면접에서 물으면 가점)</h2>
<p><strong>PLRU/주소 함수를 RTL과 공유한다.</strong> 모델이 <code>plru_victim</code>/<code>get_set</code> 등 <strong>RTL과 같은
패키지 함수</strong>를 쓴다. 장점은 주소 해석 불일치가 원천 봉쇄. 단점은 <strong>그 함수 자체에
버그가 있으면 모델과 DUT가 같이 틀려서 못 잡는다</strong>(whitebox reference model의 고전적
약점). 여기선 데이터/eviction 주소/순서는 독립적으로 검증하므로, 못 잡는 건 순수
&quot;교체 정책 결정&quot; 한 가지뿐. 진짜 독립성을 원하면 PLRU를 모델에 따로 구현해야 한다.</p>
<p>→ 이걸 말할 수 있으면 &quot;reference model의 독립성 trade-off를 이해한다&quot;로 읽힌다.</p>
<hr>
<h2 id="11-면접-예상-질문">11. 면접 예상 질문</h2>
<table>
<thead>
<tr>
<th>질문</th>
<th>답</th>
</tr>
</thead>
<tbody><tr>
<td>스코어보드가 메모리만 모델링하면 안 되나?</td>
<td>hit/miss·way를 판단 못 함. &quot;이건 hit이었어야&quot; &quot;이 evict는 없었어야&quot;를 못 잡음</td>
</tr>
<tr>
<td><code>uvm_analysis_imp_decl</code>는 왜?</td>
<td>한 컴포넌트가 여러 analysis를 받을 때 write() 이름 충돌 방지, write_req 등 별도 콜백 생성</td>
</tr>
<tr>
<td>파이프라인 DUT에서 read 순서 문제 해결?</td>
<td>기대값을 발행 시점에 스냅샷(rd_q), 응답 시점에 pop&amp;비교. 발행 순서 보존</td>
</tr>
<tr>
<td>memory burst의 어떤 오류를 잡나?</td>
<td>없어야 할/빠진/주소틀린/순서바뀐/길이틀린/데이터틀린 burst 전부</td>
</tr>
<tr>
<td>빠진 burst는 어디서 잡히나?</td>
<td>write_mem이 아니라 check_phase의 exp_mem_q 잔여 검사</td>
</tr>
<tr>
<td>check_phase가 왜 필요?</td>
<td>응답 누락·예측 burst 미발생·무트래픽 등 &quot;조용한 통과&quot;를 종료 직전에 잡음</td>
</tr>
<tr>
<td>리셋 중 write-back이 끊기면?</td>
<td>그 라인 poison → 데이터 비교만 skip, 프로토콜/순서 체크는 유지</td>
</tr>
<tr>
<td>왜 DRAM 기본값을 주소 의존으로?</td>
<td>상수면 cold fill 4 beat가 동일 → 워드 선택/순서 버그를 숨김</td>
</tr>
<tr>
<td>이 골든 모델의 한계?</td>
<td>PLRU/주소 함수를 RTL과 공유 → 그 함수 자체 버그는 못 잡음</td>
</tr>
</tbody></table>
<hr>
<h2 id="12-셀프-체크">12. 셀프 체크</h2>
<ol>
<li>read 기대값을 응답 시점이 아니라 발행 시점에 스냅샷하는 이유는?</li>
<li><code>write_mem</code>에서 &quot;빠진 burst&quot;를 못 잡는 이유와, 그럼 어디서 잡히나?</li>
<li>dirty miss일 때 <code>exp_mem_q</code>에 몇 개가, 어떤 순서로 push되나?</li>
<li>async reset에서 poison하는 대상과, poison되면 무슨 검사가 꺼지나?</li>
<li>DRAM 기본값이 상수 <code>0xDEADBEEF</code>면 어떤 버그가 숨나?</li>
<li>이 스코어보드가 원리상 못 잡는 버그 하나는?</li>
</ol>
<details>
<summary>답</summary>

<ol>
<li>파이프라인이라 응답 사이클에 다음 요청이 수락됨 → 응답 시점 모델은 이미 앞서 나가
가짜 실패. 발행 순서대로 스냅샷하면 프로그램 순서 보존.</li>
<li>write_mem은 &quot;온 burst&quot;만 검사. 안 온 건 볼 수 없음 → check_phase가 exp_mem_q 잔여로 잡음.</li>
<li>2개: eviction(rw=1) 먼저, fetch(rw=0) 나중. (clean miss면 fetch 1개만)</li>
<li>in-flight write-back의 라인 주소. poison되면 그 라인의 <strong>데이터 비교</strong>만 skip,
방향/주소/길이/순서 체크는 유지.</li>
<li>cold fill의 4 beat가 다 같은 값 → 워드 순서가 뒤바뀌거나 잘못된 워드를 반환해도
구별 안 됨(워드 선택 버그).</li>
<li>PLRU/주소 함수를 RTL과 공유하므로, 그 함수 자체의 버그(예: 잘못된 victim 계산)는
모델·DUT가 같이 틀려 못 잡음.</details>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[rtl/l1_cache_core.sv]]></title>
            <link>https://velog.io/@houston_guy2/rtll1cachecore.sv-v7tf05oo</link>
            <guid>https://velog.io/@houston_guy2/rtll1cachecore.sv-v7tf05oo</guid>
            <pubDate>Sun, 09 Aug 2026 05:56:05 GMT</pubDate>
            <description><![CDATA[<p>이 프로젝트의 <strong>심장</strong>. 4-way 캐시 컨트롤러 = 2단 파이프라인 + 6상태 FSM +
forwarding. 면접에서 가장 깊게 파고드는 파일이라, 블록별로 &quot;무엇을·왜&quot;를 다 잡는다.</p>
<hr>
<h2 id="0-큰-그림--이-모듈의-구성">0. 큰 그림 — 이 모듈의 구성</h2>
<p>컨트롤러는 크게 <strong>7개 블록</strong>으로 나뉜다. 순서대로 읽으면 데이터 흐름이 보인다.</p>
<table>
<thead>
<tr>
<th>#</th>
<th>블록</th>
<th>종류</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>S1→S2 파이프라인 레지스터</td>
<td><code>always_ff</code></td>
<td>요청을 한 사이클 붙잡아 S2로 넘김</td>
</tr>
<tr>
<td>2</td>
<td>라인 상태 (valid/dirty/PLRU)</td>
<td>플롭 배열</td>
<td>캐시 메타데이터, 리셋 1사이클 flush</td>
</tr>
<tr>
<td>3</td>
<td>배열 read 주소 mux</td>
<td><code>always_comb</code></td>
<td>S1/S2/victim 중 어느 주소를 SRAM에 줄지</td>
</tr>
<tr>
<td>4</td>
<td>forwarding</td>
<td><code>always_ff</code>+<code>always_comb</code></td>
<td>read-before-write hazard 우회</td>
</tr>
<tr>
<td>5</td>
<td>hit 판정</td>
<td><code>always_comb</code></td>
<td>4-way 병렬 태그 비교</td>
</tr>
<tr>
<td>6</td>
<td>victim 선택</td>
<td><code>always_comb</code></td>
<td>교체할 way 결정 (invalid 우선, 아니면 PLRU)</td>
</tr>
<tr>
<td>7</td>
<td>FSM</td>
<td><code>always_ff</code>+<code>always_comb</code></td>
<td>miss 처리(evict/fill) 시퀀싱</td>
</tr>
</tbody></table>
<p>핵심 원칙: <strong>hit는 파이프라인이 조합적으로 처리</strong>(FSM은 ST_IDLE에 머무름),
<strong>miss만 FSM이 여러 사이클에 걸쳐 처리</strong>(그동안 stall).</p>
<hr>
<h2 id="1-s1-→-s2-파이프라인-레지스터-6696행">1. S1 → S2 파이프라인 레지스터 (66~96행)</h2>
<pre><code class="language-c">always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin ... 리셋 ... end
    else if (!pipeline_stall) begin       // ← stall이면 값 유지(홀드)
        s2_valid &lt;= cpu_req_valid;
        s2_rw    &lt;= cpu_req_rw;
        s2_addr  &lt;= cpu_req_addr;
        s2_be    &lt;= cpu_req_be;
        s2_wdata &lt;= cpu_req_wdata;
    end
end</code></pre>
<ul>
<li><strong>S1</strong>(입력 핀) → <strong>S2</strong>(레지스터)로 요청을 1사이클 지연. 이 레지스터가 파이프라인 단 경계.</li>
<li><code>!pipeline_stall</code> 일 때만 갱신 = <strong>stall이면 S2가 그 요청을 붙잡고 있음</strong> (miss 처리 동안 요청이 날아가지 않게).</li>
<li><code>s1_*</code> 는 입력 주소에서 조합적으로 뽑은 필드, <code>s2_*</code> 는 레지스터된 주소에서 뽑은 필드. (<code>get_set</code>/<code>get_word</code>/<code>get_tag</code>는 <a href="../rtl/l1_cache_pkg.sv">l1_cache_pkg.sv</a>)</li>
</ul>
<hr>
<h2 id="2-라인-상태--플롭에-둔-이유-98108행">2. 라인 상태 — 플롭에 둔 이유 (98~108행)</h2>
<pre><code class="language-c">logic [WAYS-1:0] valid_bits [SETS];   // set당 4비트 (way별 valid)
logic [WAYS-1:0] dirty_bits [SETS];
plru_t           plru_bits  [SETS];   // set당 3비트 PLRU</code></pre>
<ul>
<li>valid/dirty/PLRU를 <strong>SRAM이 아니라 플롭</strong>에 뒀다.</li>
<li>이유: <strong>리셋 한 사이클에 캐시 전체가 flush</strong>된다 (FSM 리셋 블록 280~284행에서 전 set을 0으로).
SRAM에 두면 전원 인가 후 전 라인을 순회 무효화하는 상태가 필요하고, 그 전까진
태그가 X라 hit 판정에 합성 불가 <code>===</code>를 써야 한다.</li>
<li>용량: 64 set × 4 way × 2bit(valid+dirty) + 64 × 3(plru) = 감당 가능한 플롭 수.</li>
</ul>
<hr>
<h2 id="3-배열-read-주소-mux-136149행-★">3. 배열 read 주소 mux (136~149행) ★</h2>
<p>동기 SRAM이라 <strong>N에 준 주소가 N+1에 데이터</strong>로 나온다. 그래서 &quot;지금 어느 주소를
줘야 다음 사이클 S2가 원하는 데이터가 오는가&quot;를 3경우로 나눈다.</p>
<pre><code class="language-c">always_comb begin
    if (!pipeline_stall &amp;&amp; cpu_req_valid) begin        // ① 정상
        tag_raddr  = s1_set;
        data_raddr = make_data_addr(s1_set, s1_word);
    end
    else if (state == ST_WB_READ) begin                // ② victim 읽어내는 중
        data_raddr = make_data_addr(s2_set, wb_cnt[1:0]);
    end
    else begin                                          // ③ stall/idle
        tag_raddr  = s2_set;
        data_raddr = make_data_addr(s2_set, s2_word);
    end
end</code></pre>
<table>
<thead>
<tr>
<th>경우</th>
<th>언제</th>
<th>주소</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>① 정상</td>
<td>진행 중 &amp; 유효 요청</td>
<td>S1의 {set,word}</td>
<td>다음 사이클 S2에 이 요청 데이터가 도착</td>
</tr>
<tr>
<td>② WB_READ</td>
<td>dirty victim 읽어낼 때</td>
<td>S2 set + <code>wb_cnt</code> word</td>
<td>victim 라인을 워드별로 순회</td>
</tr>
<tr>
<td>③ stall/idle</td>
<td>그 외</td>
<td>S2의 {set,word}</td>
<td>파이프라인 홀드 중이니 S2 유지. idle 시 X 인덱싱 방지</td>
</tr>
</tbody></table>
<p><strong>왜 ③에서 S2로 폴백?</strong> stall 중엔 S1에 새 주소가 와도 무시해야 하고, 요청이 없을 땐
드라이버가 버스에 X를 실으므로 그 X로 배열을 인덱싱하면 안 된다.</p>
<hr>
<h2 id="4-forwarding--read-before-write-우회-166210행-★★">4. Forwarding — read-before-write 우회 (166~210행) ★★</h2>
<p>동기 SRAM은 &quot;쓰기 직전 값&quot;을 반환한다(<a href="STUDY_sram.md">STUDY_sram.md</a> §4).
그래서 <strong>직전 사이클에 쓴 위치를 지금 읽으면 stale</strong>. 1단 포워딩으로 우회.</p>
<pre><code class="language-c">// 직전 사이클의 write/read 정보를 플롭에 저장
prev_data_we, prev_data_wbe, prev_data_waddr, prev_data_wdata, prev_data_raddr &lt;= ...

// 바이트 단위로 우회
safe_data_rdata[w][b*8 +: 8] =
    (prev_data_we[w] &amp;&amp; (prev_data_waddr == prev_data_raddr) &amp;&amp; prev_data_wbe[b])
    ? prev_data_wdata[b*8 +: 8]     // 직전에 쓴 값
    : data_rdata[w][b*8 +: 8];      // 배열이 준 값</code></pre>
<p><strong>포인트 3개</strong> (면접에서 여기 파고듦):</p>
<ol>
<li><p><strong>비교 대상이 <code>s2</code>가 아니라 <code>prev_*_raddr</code></strong> — 직전 사이클에 <strong>실제로 인가한
read 주소</strong>. <code>ST_WB_READ</code>에선 read 주소가 라인을 순회(§3 ②)하므로 <code>s2_word</code>가 아님.
그래서 &quot;직전에 준 read 주소&quot;를 플롭으로 잡아 비교.</p>
</li>
<li><p><strong>바이트 단위</strong> — partial write(<code>be != 4&#39;b1111</code>)면 쓴 레인만 포워딩, 나머지는 배열 값.
워드 통째로 포워딩하면 안 건드린 바이트가 오염됨.</p>
</li>
<li><p><strong>1사이클 깊이면 충분</strong> — write 다음다음 사이클엔 배열 자체가 새 값을 가짐.
stale인 건 딱 한 사이클.</p>
</li>
</ol>
<blockquote>
<p>태그(<code>safe_tag_rdata</code>)도 같은 원리인데 바이트 분할이 없다(태그는 통째 갱신).</p>
</blockquote>
<hr>
<h2 id="5-hit-판정--4-way-병렬-비교-215230행">5. Hit 판정 — 4-way 병렬 비교 (215~230행)</h2>
<pre><code class="language-systemverilog">for (int w = 0; w &lt; WAYS; w++)
    way_hit[w] = set_valid[w] &amp;&amp; (safe_tag_rdata[w] == s2_tag);

assign cache_hit = s2_valid &amp;&amp; (|way_hit);   // |way_hit = OR 리덕션

for (int w = 0; w &lt; WAYS; w++)
    if (way_hit[w]) hit_way = way_t&#39;(w);      // 어느 way가 hit인지</code></pre>
<ul>
<li>4개 way의 태그를 <strong>동시에</strong> s2_tag와 비교 (direct-mapped는 1개만, 4-way는 4개 병렬).</li>
<li><code>set_valid[w]</code> 조건 필수 — valid 안 된 way의 쓰레기 태그가 우연히 맞는 걸 배제.</li>
<li>정상 캐시라면 set당 같은 태그는 <strong>최대 1개 way</strong>에만 있음 → hit_way 유일.</li>
<li>비교에 <code>safe_tag_rdata</code>(포워딩된 값)를 쓰는 게 중요 — 방금 쓴 태그도 반영.</li>
</ul>
<hr>
<h2 id="6-victim-선택-236257행">6. Victim 선택 (236~257행)</h2>
<pre><code class="language-systemverilog">for (int w = WAYS-1; w &gt;= 0; w--)           // 높은 way→낮은 way
    if (!set_valid[w]) begin
        first_invalid = way_t&#39;(w); has_invalid = 1&#39;b1;
    end
victim_sel = has_invalid ? first_invalid : plru_victim(plru_bits[s2_set]);</code></pre>
<p><strong>우선순위</strong>: ① invalid(빈) way가 있으면 그걸 먼저 채움(cold fill) → ② 없으면 PLRU가 지목.</p>
<ul>
<li>루프가 높은→낮은 순회 + 덮어쓰기라, <code>first_invalid</code>엔 <strong>가장 낮은 번호의 invalid way</strong>가 남음.</li>
<li><code>plru_victim()</code>은 3비트 트리를 따라 내려가 victim 결정 (<a href="../rtl/l1_cache_pkg.sv">l1_cache_pkg.sv</a>).</li>
<li><code>victim_dirty</code> = victim이 valid &amp;&amp; dirty → write-back 필요 여부.</li>
</ul>
<blockquote>
<p>⚠️ 여기 주석의 교훈: <code>always_comb</code> 안에서 <code>logic x = 0;</code> 같은 <strong>선언 초기화 금지</strong>.
절차 블록 변수는 static이라 time 0에 한 번만 초기화됨. 그래서 <code>first_invalid</code>/
<code>has_invalid</code>를 블록 밖에 선언하고 안에서 대입. (실제로 다른 곳에서 이 함정에 물렸음)</p>
</blockquote>
<hr>
<h2 id="7-cpu-응답-262265행">7. CPU 응답 (262~265행)</h2>
<pre><code class="language-systemverilog">assign cpu_rsp_valid = ((state == ST_IDLE)     &amp;&amp; s2_valid &amp;&amp; cache_hit &amp;&amp; !s2_rw)
                    || ((state == ST_COMPLETE) &amp;&amp; s2_valid &amp;&amp;              !s2_rw);

assign cpu_rsp_rdata = (state == ST_COMPLETE) ? fill_data : safe_data_rdata[hit_way];</code></pre>
<p>읽기 응답이 나가는 두 순간:</p>
<ul>
<li><strong>ST_IDLE의 read hit</strong> → 데이터는 배열에서(<code>safe_data_rdata[hit_way]</code>, 포워딩 반영)</li>
<li><strong>ST_COMPLETE의 read</strong> (miss 처리 끝) → 데이터는 fill에서 잡아둔 <code>fill_data</code></li>
</ul>
<p>write는 응답 없음(<code>!s2_rw</code> 조건). fill_data는 FSM이 fetch 중 요청 워드를 잡아둔 값(§8 ST_FILL_RCV).</p>
<hr>
<h2 id="8-fsm-i--순차-블록-무엇을-래치하나-270339행">8. FSM (I) — 순차 블록: 무엇을 래치하나 (270~339행)</h2>
<p><code>state &lt;= next_state</code> 외에, <strong>상태별로 레지스터에 무엇을 저장하는지</strong>.</p>
<pre><code class="language-c">ST_IDLE:
    hit  → PLRU 갱신, write면 dirty 세팅
    miss → victim_way/victim_tag 래치, 카운터 리셋 (miss 시퀀스 준비)

ST_WB_READ:
    if (wb_cnt &gt; 0) wb_buf[wb_cnt-1] &lt;= safe_data_rdata[victim_way];  // 1사이클 뒤 캡처
    wb_cnt &lt;= wb_cnt + 1;

ST_FILL_RCV:
    if (mem_rd_valid) begin
        if (fill_beat == s2_word) fill_data &lt;= mem_rd_data;  // 요청 워드만 잡아둠
        fill_beat &lt;= fill_beat + 1;
    end

ST_COMPLETE:
    valid_bits[s2_set][victim_way] &lt;= 1&#39;b1;      // 라인 유효화
    dirty_bits[s2_set][victim_way] &lt;= s2_rw;     // write miss면 dirty
    plru_bits[s2_set] &lt;= plru_update(..., victim_way);</code></pre>
<p><strong><code>wb_cnt-1</code> 캡처의 의미</strong>: 동기 read라 주소를 준 다음 사이클에 데이터가 옴.
<code>wb_cnt=N</code>일 때 word N 주소를 인가(§3 ②) → <code>wb_cnt=N+1</code>이 됐을 때 word N 데이터 도착
→ <code>wb_buf[N]</code>에 저장. 그래서 &quot;한 사이클 뒤(<code>wb_cnt-1</code>)&quot;에 캡처.</p>
<p><strong>왜 victim을 버퍼에 복사?</strong> fill(ST_FILL_RCV)이 <strong>같은 물리 위치</strong>
<code>{s2_set, victim_way}</code>에 새 라인을 덮어쓴다. 그러니 덮이기 전에 victim을 먼저 읽어내야
한다 → WB_READ에서 <code>wb_buf</code>로 스냅샷.</p>
<hr>
<h2 id="9-fsm-ii--조합-블록-next_state--출력-344444행">9. FSM (II) — 조합 블록: next_state + 출력 (344~444행)</h2>
<p>맨 위에 <strong>기본값 전부 비활성</strong>(모든 we=0, mem_req=0, stall=0…)을 깔고, 상태별로 덮어씀.
이게 latch 방지 + 안전한 기본 상태의 정석.</p>
<pre><code>ST_IDLE:
    hit &amp; write → data_we[hit_way]=1, data_wbe=s2_be   (제자리 갱신)
    miss        → stall=1, next = victim_dirty ? WB_READ : FILL_REQ

ST_WB_READ:  stall=1;  wb_cnt==4 → next=WB_SEND
ST_WB_SEND:  stall=1
    !req_sent → mem_req(rw=1, victim 주소) 발행
    req_sent  → mem_wr_data=wb_buf[wb_ptr], last=마지막워드; ready&amp;last → next=FILL_REQ
ST_FILL_REQ: stall=1;  mem_req(rw=0, 새 라인 주소);  req_ready → next=FILL_RCV
ST_FILL_RCV: stall=1
    mem_rd_valid → data_we[victim_way]=1, wbe=all, {set,fill_beat}에 기록; last → next=COMPLETE
ST_COMPLETE: stall=0(!);  tag_we[victim_way]=1(태그 커밋);
             write면 data_we[victim_way]=1 + s2_be (스토어 반영);  next=IDLE</code></pre><p><strong>miss 처리 흐름 (dirty)</strong>: IDLE → WB_READ(victim 버퍼링) → WB_SEND(메모리로 스트리밍)
→ FILL_REQ(새 라인 요청) → FILL_RCV(4비트 수신) → COMPLETE(커밋+응답) → IDLE.
clean victim이면 WB 두 단계 건너뛰고 바로 FILL_REQ.</p>
<hr>
<h2 id="10-★-반드시-외울-설계-포인트-3개">10. ★ 반드시 외울 설계 포인트 3개</h2>
<h3 id="a-st_complete에서-stall을-푼다-428440행">(a) <code>ST_COMPLETE</code>에서 stall을 <strong>푼다</strong> (428~440행)</h3>
<pre><code class="language-systemverilog">ST_COMPLETE: begin
    pipeline_stall = 1&#39;b0;   // ← 여기서 해제
    ...
end</code></pre>
<p>여기서 stall을 유지하면 다음 사이클 <code>ST_IDLE</code>에서 <strong>S2가 같은 요청을 그대로 들고 있어
응답을 두 번</strong> 내보낸다. stall을 풀어야 파이프라인이 진행되어 S2가 넘어감.</p>
<h3 id="b-왜-조합-루프가-없나">(b) 왜 조합 루프가 없나</h3>
<p><code>tag_raddr</code>가 <code>pipeline_stall</code>로 선택되지만, <code>tag_rdata</code>는 <strong>플롭 출력</strong>(동기 SRAM)이라
<code>stall→raddr→rdata→hit→stall</code> 피드백이 끊긴다. 비동기 SRAM이면 이 mux가 루프를 닫는다
(이전 버전의 675 버그).</p>
<h3 id="c-always_comb-정적-변수-함정-§6">(c) <code>always_comb</code> 정적 변수 함정 (§6)</h3>
<p>절차 블록 선언 초기화는 static. 루프/조합 블록에선 선언과 대입 분리.</p>
<hr>
<h2 id="11-cycle-by-cycle--clean-read-miss-가장-단순">11. Cycle-by-cycle — clean read miss (가장 단순)</h2>
<p>메모리 latency=2 가정. victim이 clean이라 write-back 없음.</p>
<table>
<thead>
<tr>
<th>cycle</th>
<th>state</th>
<th>하는 일</th>
<th>stall</th>
</tr>
</thead>
<tbody><tr>
<td>N</td>
<td>ST_IDLE</td>
<td>S2에 read 도착, miss 판정, victim 래치</td>
<td>1</td>
</tr>
<tr>
<td>N+1</td>
<td>ST_FILL_REQ</td>
<td>mem_req(rw=0) 발행, req_ready 대기</td>
<td>1</td>
</tr>
<tr>
<td>N+2</td>
<td>ST_FILL_RCV</td>
<td>(latency 대기)</td>
<td>1</td>
</tr>
<tr>
<td>N+3</td>
<td>ST_FILL_RCV</td>
<td>beat0 수신 → data[set,0] 기록, 요청워드면 fill_data</td>
<td>1</td>
</tr>
<tr>
<td>N+4~6</td>
<td>ST_FILL_RCV</td>
<td>beat1,2,3 수신·기록; last에서 → COMPLETE</td>
<td>1</td>
</tr>
<tr>
<td>N+7</td>
<td>ST_COMPLETE</td>
<td>valid=1,dirty=0,PLRU갱신; rsp_valid+fill_data; <strong>stall=0</strong></td>
<td>0</td>
</tr>
<tr>
<td>N+8</td>
<td>ST_IDLE</td>
<td>다음 요청 진행</td>
<td>-</td>
</tr>
</tbody></table>
<p>dirty miss면 N+1 앞에 <strong>WB_READ(≈5) + WB_SEND(req+4beat)</strong> 가 추가된다.</p>
<hr>
<h2 id="12-면접-예상-질문">12. 면접 예상 질문</h2>
<table>
<thead>
<tr>
<th>질문</th>
<th>답</th>
</tr>
</thead>
<tbody><tr>
<td>hit는 왜 FSM을 안 거치나?</td>
<td>hit는 조합적으로 응답/write 가능. FSM은 miss(여러 사이클)만 처리. 성능 위해 hit path를 짧게</td>
</tr>
<tr>
<td>4-way에서 hit_way를 어떻게?</td>
<td>4개 태그를 병렬 비교, valid까지 AND, OR 리덕션으로 hit, 인코딩으로 way</td>
</tr>
<tr>
<td>victim은 어떻게 고르나?</td>
<td>invalid way 우선, 없으면 tree-PLRU</td>
</tr>
<tr>
<td>write-back 데이터를 왜 버퍼링?</td>
<td>fill이 같은 {set,victim_way} 위치를 덮으므로, 덮기 전에 victim을 읽어내야 함</td>
</tr>
<tr>
<td>ST_COMPLETE가 없으면?</td>
<td>fill 마지막 beat와 store write가 같은 사이클에 충돌 + 응답 이중 발생</td>
</tr>
<tr>
<td>stall 중 파이프라인 레지스터는?</td>
<td>홀드(<code>!pipeline_stall</code>일 때만 갱신). 요청이 날아가지 않게</td>
</tr>
<tr>
<td>forwarding 비교를 s2로 하면 안 되는 이유?</td>
<td>WB_READ에서 read 주소가 라인을 순회해 s2_word와 다름. 실제 인가한 prev_raddr로 비교해야</td>
</tr>
<tr>
<td>조합 루프가 없는 이유?</td>
<td>rdata가 플롭 출력이라 stall로의 피드백이 끊김 (동기 SRAM 덕분)</td>
</tr>
<tr>
<td>read 채널에 ready가 없는 이유?</td>
<td>fill은 캐시가 자기 배열을 채우는 것 → 항상 수용 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="13-셀프-체크">13. 셀프 체크</h2>
<ol>
<li><code>pipeline_stall</code>이 1이면 S1→S2 레지스터는 어떻게 되나? 왜 그래야 하나?</li>
<li>clean miss와 dirty miss의 상태 경로 차이는?</li>
<li><code>ST_WB_READ</code>에서 <code>wb_buf[wb_cnt-1]</code>로 한 사이클 뒤에 캡처하는 이유는?</li>
<li>forwarding에서 <code>prev_data_raddr</code>(직전 read 주소)로 비교하는 이유는? <code>s2</code>면 왜 안 되나?</li>
<li><code>ST_COMPLETE</code>에서 <code>pipeline_stall = 0</code>으로 푸는 이유는?</li>
<li>victim으로 dirty way가 뽑혔는데 그걸 버퍼에 안 읽고 바로 fill하면 무슨 일이?</li>
</ol>
<details>
<summary>답</summary>

<ol>
<li><strong>홀드</strong>(값 유지). miss 처리 동안 S2가 그 요청을 붙잡고 있어야 요청이 사라지지 않음.</li>
<li>clean: IDLE→FILL_REQ→FILL_RCV→COMPLETE. dirty: 앞에 WB_READ→WB_SEND 추가.</li>
<li>동기 read라 주소 준 다음 사이클에 데이터 도착. word N 주소를 wb_cnt=N에 인가 →
wb_cnt=N+1에 데이터 도착 → wb_buf[N]에 저장.</li>
<li>WB_READ에서 read 주소가 라인을 순회(word 0,1,2,3)해 s2_word와 다름.
&quot;직전에 실제로 인가한 read 주소&quot;와 비교해야 그 위치의 stale을 정확히 잡음.</li>
<li>안 풀면 다음 사이클 IDLE에서 S2가 같은 요청을 들고 있어 응답을 두 번 냄<ul>
<li>fill 마지막 write와 store write 충돌.</li>
</ul>
</li>
<li>fill이 같은 {set,victim_way} 위치를 덮어써서 victim(수정된 값)이 메모리에 못 나가고
사라짐 → 데이터 유실. 그래서 WB_READ로 먼저 스냅샷.</details>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[rtl/l1_cache_core.sv]]></title>
            <link>https://velog.io/@houston_guy2/rtll1cachecore.sv</link>
            <guid>https://velog.io/@houston_guy2/rtll1cachecore.sv</guid>
            <pubDate>Sun, 09 Aug 2026 05:55:52 GMT</pubDate>
            <description><![CDATA[<p>이 프로젝트의 <strong>심장</strong>. 4-way 캐시 컨트롤러 = 2단 파이프라인 + 6상태 FSM +
forwarding. 면접에서 가장 깊게 파고드는 파일이라, 블록별로 &quot;무엇을·왜&quot;를 다 잡는다.</p>
<hr>
<h2 id="0-큰-그림--이-모듈의-구성">0. 큰 그림 — 이 모듈의 구성</h2>
<p>컨트롤러는 크게 <strong>7개 블록</strong>으로 나뉜다. 순서대로 읽으면 데이터 흐름이 보인다.</p>
<table>
<thead>
<tr>
<th>#</th>
<th>블록</th>
<th>종류</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>S1→S2 파이프라인 레지스터</td>
<td><code>always_ff</code></td>
<td>요청을 한 사이클 붙잡아 S2로 넘김</td>
</tr>
<tr>
<td>2</td>
<td>라인 상태 (valid/dirty/PLRU)</td>
<td>플롭 배열</td>
<td>캐시 메타데이터, 리셋 1사이클 flush</td>
</tr>
<tr>
<td>3</td>
<td>배열 read 주소 mux</td>
<td><code>always_comb</code></td>
<td>S1/S2/victim 중 어느 주소를 SRAM에 줄지</td>
</tr>
<tr>
<td>4</td>
<td>forwarding</td>
<td><code>always_ff</code>+<code>always_comb</code></td>
<td>read-before-write hazard 우회</td>
</tr>
<tr>
<td>5</td>
<td>hit 판정</td>
<td><code>always_comb</code></td>
<td>4-way 병렬 태그 비교</td>
</tr>
<tr>
<td>6</td>
<td>victim 선택</td>
<td><code>always_comb</code></td>
<td>교체할 way 결정 (invalid 우선, 아니면 PLRU)</td>
</tr>
<tr>
<td>7</td>
<td>FSM</td>
<td><code>always_ff</code>+<code>always_comb</code></td>
<td>miss 처리(evict/fill) 시퀀싱</td>
</tr>
</tbody></table>
<p>핵심 원칙: <strong>hit는 파이프라인이 조합적으로 처리</strong>(FSM은 ST_IDLE에 머무름),
<strong>miss만 FSM이 여러 사이클에 걸쳐 처리</strong>(그동안 stall).</p>
<hr>
<h2 id="1-s1-→-s2-파이프라인-레지스터-6696행">1. S1 → S2 파이프라인 레지스터 (66~96행)</h2>
<pre><code class="language-c">always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin ... 리셋 ... end
    else if (!pipeline_stall) begin       // ← stall이면 값 유지(홀드)
        s2_valid &lt;= cpu_req_valid;
        s2_rw    &lt;= cpu_req_rw;
        s2_addr  &lt;= cpu_req_addr;
        s2_be    &lt;= cpu_req_be;
        s2_wdata &lt;= cpu_req_wdata;
    end
end</code></pre>
<ul>
<li><strong>S1</strong>(입력 핀) → <strong>S2</strong>(레지스터)로 요청을 1사이클 지연. 이 레지스터가 파이프라인 단 경계.</li>
<li><code>!pipeline_stall</code> 일 때만 갱신 = <strong>stall이면 S2가 그 요청을 붙잡고 있음</strong> (miss 처리 동안 요청이 날아가지 않게).</li>
<li><code>s1_*</code> 는 입력 주소에서 조합적으로 뽑은 필드, <code>s2_*</code> 는 레지스터된 주소에서 뽑은 필드. (<code>get_set</code>/<code>get_word</code>/<code>get_tag</code>는 <a href="../rtl/l1_cache_pkg.sv">l1_cache_pkg.sv</a>)</li>
</ul>
<hr>
<h2 id="2-라인-상태--플롭에-둔-이유-98108행">2. 라인 상태 — 플롭에 둔 이유 (98~108행)</h2>
<pre><code class="language-c">logic [WAYS-1:0] valid_bits [SETS];   // set당 4비트 (way별 valid)
logic [WAYS-1:0] dirty_bits [SETS];
plru_t           plru_bits  [SETS];   // set당 3비트 PLRU</code></pre>
<ul>
<li>valid/dirty/PLRU를 <strong>SRAM이 아니라 플롭</strong>에 뒀다.</li>
<li>이유: <strong>리셋 한 사이클에 캐시 전체가 flush</strong>된다 (FSM 리셋 블록 280~284행에서 전 set을 0으로).
SRAM에 두면 전원 인가 후 전 라인을 순회 무효화하는 상태가 필요하고, 그 전까진
태그가 X라 hit 판정에 합성 불가 <code>===</code>를 써야 한다.</li>
<li>용량: 64 set × 4 way × 2bit(valid+dirty) + 64 × 3(plru) = 감당 가능한 플롭 수.</li>
</ul>
<hr>
<h2 id="3-배열-read-주소-mux-136149행-★">3. 배열 read 주소 mux (136~149행) ★</h2>
<p>동기 SRAM이라 <strong>N에 준 주소가 N+1에 데이터</strong>로 나온다. 그래서 &quot;지금 어느 주소를
줘야 다음 사이클 S2가 원하는 데이터가 오는가&quot;를 3경우로 나눈다.</p>
<pre><code class="language-c">always_comb begin
    if (!pipeline_stall &amp;&amp; cpu_req_valid) begin        // ① 정상
        tag_raddr  = s1_set;
        data_raddr = make_data_addr(s1_set, s1_word);
    end
    else if (state == ST_WB_READ) begin                // ② victim 읽어내는 중
        data_raddr = make_data_addr(s2_set, wb_cnt[1:0]);
    end
    else begin                                          // ③ stall/idle
        tag_raddr  = s2_set;
        data_raddr = make_data_addr(s2_set, s2_word);
    end
end</code></pre>
<table>
<thead>
<tr>
<th>경우</th>
<th>언제</th>
<th>주소</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>① 정상</td>
<td>진행 중 &amp; 유효 요청</td>
<td>S1의 {set,word}</td>
<td>다음 사이클 S2에 이 요청 데이터가 도착</td>
</tr>
<tr>
<td>② WB_READ</td>
<td>dirty victim 읽어낼 때</td>
<td>S2 set + <code>wb_cnt</code> word</td>
<td>victim 라인을 워드별로 순회</td>
</tr>
<tr>
<td>③ stall/idle</td>
<td>그 외</td>
<td>S2의 {set,word}</td>
<td>파이프라인 홀드 중이니 S2 유지. idle 시 X 인덱싱 방지</td>
</tr>
</tbody></table>
<p><strong>왜 ③에서 S2로 폴백?</strong> stall 중엔 S1에 새 주소가 와도 무시해야 하고, 요청이 없을 땐
드라이버가 버스에 X를 실으므로 그 X로 배열을 인덱싱하면 안 된다.</p>
<hr>
<h2 id="4-forwarding--read-before-write-우회-166210행-★★">4. Forwarding — read-before-write 우회 (166~210행) ★★</h2>
<p>동기 SRAM은 &quot;쓰기 직전 값&quot;을 반환한다(<a href="STUDY_sram.md">STUDY_sram.md</a> §4).
그래서 <strong>직전 사이클에 쓴 위치를 지금 읽으면 stale</strong>. 1단 포워딩으로 우회.</p>
<pre><code class="language-c">// 직전 사이클의 write/read 정보를 플롭에 저장
prev_data_we, prev_data_wbe, prev_data_waddr, prev_data_wdata, prev_data_raddr &lt;= ...

// 바이트 단위로 우회
safe_data_rdata[w][b*8 +: 8] =
    (prev_data_we[w] &amp;&amp; (prev_data_waddr == prev_data_raddr) &amp;&amp; prev_data_wbe[b])
    ? prev_data_wdata[b*8 +: 8]     // 직전에 쓴 값
    : data_rdata[w][b*8 +: 8];      // 배열이 준 값</code></pre>
<p><strong>포인트 3개</strong> (면접에서 여기 파고듦):</p>
<ol>
<li><p><strong>비교 대상이 <code>s2</code>가 아니라 <code>prev_*_raddr</code></strong> — 직전 사이클에 <strong>실제로 인가한
read 주소</strong>. <code>ST_WB_READ</code>에선 read 주소가 라인을 순회(§3 ②)하므로 <code>s2_word</code>가 아님.
그래서 &quot;직전에 준 read 주소&quot;를 플롭으로 잡아 비교.</p>
</li>
<li><p><strong>바이트 단위</strong> — partial write(<code>be != 4&#39;b1111</code>)면 쓴 레인만 포워딩, 나머지는 배열 값.
워드 통째로 포워딩하면 안 건드린 바이트가 오염됨.</p>
</li>
<li><p><strong>1사이클 깊이면 충분</strong> — write 다음다음 사이클엔 배열 자체가 새 값을 가짐.
stale인 건 딱 한 사이클.</p>
</li>
</ol>
<blockquote>
<p>태그(<code>safe_tag_rdata</code>)도 같은 원리인데 바이트 분할이 없다(태그는 통째 갱신).</p>
</blockquote>
<hr>
<h2 id="5-hit-판정--4-way-병렬-비교-215230행">5. Hit 판정 — 4-way 병렬 비교 (215~230행)</h2>
<pre><code class="language-systemverilog">for (int w = 0; w &lt; WAYS; w++)
    way_hit[w] = set_valid[w] &amp;&amp; (safe_tag_rdata[w] == s2_tag);

assign cache_hit = s2_valid &amp;&amp; (|way_hit);   // |way_hit = OR 리덕션

for (int w = 0; w &lt; WAYS; w++)
    if (way_hit[w]) hit_way = way_t&#39;(w);      // 어느 way가 hit인지</code></pre>
<ul>
<li>4개 way의 태그를 <strong>동시에</strong> s2_tag와 비교 (direct-mapped는 1개만, 4-way는 4개 병렬).</li>
<li><code>set_valid[w]</code> 조건 필수 — valid 안 된 way의 쓰레기 태그가 우연히 맞는 걸 배제.</li>
<li>정상 캐시라면 set당 같은 태그는 <strong>최대 1개 way</strong>에만 있음 → hit_way 유일.</li>
<li>비교에 <code>safe_tag_rdata</code>(포워딩된 값)를 쓰는 게 중요 — 방금 쓴 태그도 반영.</li>
</ul>
<hr>
<h2 id="6-victim-선택-236257행">6. Victim 선택 (236~257행)</h2>
<pre><code class="language-systemverilog">for (int w = WAYS-1; w &gt;= 0; w--)           // 높은 way→낮은 way
    if (!set_valid[w]) begin
        first_invalid = way_t&#39;(w); has_invalid = 1&#39;b1;
    end
victim_sel = has_invalid ? first_invalid : plru_victim(plru_bits[s2_set]);</code></pre>
<p><strong>우선순위</strong>: ① invalid(빈) way가 있으면 그걸 먼저 채움(cold fill) → ② 없으면 PLRU가 지목.</p>
<ul>
<li>루프가 높은→낮은 순회 + 덮어쓰기라, <code>first_invalid</code>엔 <strong>가장 낮은 번호의 invalid way</strong>가 남음.</li>
<li><code>plru_victim()</code>은 3비트 트리를 따라 내려가 victim 결정 (<a href="../rtl/l1_cache_pkg.sv">l1_cache_pkg.sv</a>).</li>
<li><code>victim_dirty</code> = victim이 valid &amp;&amp; dirty → write-back 필요 여부.</li>
</ul>
<blockquote>
<p>⚠️ 여기 주석의 교훈: <code>always_comb</code> 안에서 <code>logic x = 0;</code> 같은 <strong>선언 초기화 금지</strong>.
절차 블록 변수는 static이라 time 0에 한 번만 초기화됨. 그래서 <code>first_invalid</code>/
<code>has_invalid</code>를 블록 밖에 선언하고 안에서 대입. (실제로 다른 곳에서 이 함정에 물렸음)</p>
</blockquote>
<hr>
<h2 id="7-cpu-응답-262265행">7. CPU 응답 (262~265행)</h2>
<pre><code class="language-systemverilog">assign cpu_rsp_valid = ((state == ST_IDLE)     &amp;&amp; s2_valid &amp;&amp; cache_hit &amp;&amp; !s2_rw)
                    || ((state == ST_COMPLETE) &amp;&amp; s2_valid &amp;&amp;              !s2_rw);

assign cpu_rsp_rdata = (state == ST_COMPLETE) ? fill_data : safe_data_rdata[hit_way];</code></pre>
<p>읽기 응답이 나가는 두 순간:</p>
<ul>
<li><strong>ST_IDLE의 read hit</strong> → 데이터는 배열에서(<code>safe_data_rdata[hit_way]</code>, 포워딩 반영)</li>
<li><strong>ST_COMPLETE의 read</strong> (miss 처리 끝) → 데이터는 fill에서 잡아둔 <code>fill_data</code></li>
</ul>
<p>write는 응답 없음(<code>!s2_rw</code> 조건). fill_data는 FSM이 fetch 중 요청 워드를 잡아둔 값(§8 ST_FILL_RCV).</p>
<hr>
<h2 id="8-fsm-i--순차-블록-무엇을-래치하나-270339행">8. FSM (I) — 순차 블록: 무엇을 래치하나 (270~339행)</h2>
<p><code>state &lt;= next_state</code> 외에, <strong>상태별로 레지스터에 무엇을 저장하는지</strong>.</p>
<pre><code class="language-c">ST_IDLE:
    hit  → PLRU 갱신, write면 dirty 세팅
    miss → victim_way/victim_tag 래치, 카운터 리셋 (miss 시퀀스 준비)

ST_WB_READ:
    if (wb_cnt &gt; 0) wb_buf[wb_cnt-1] &lt;= safe_data_rdata[victim_way];  // 1사이클 뒤 캡처
    wb_cnt &lt;= wb_cnt + 1;

ST_FILL_RCV:
    if (mem_rd_valid) begin
        if (fill_beat == s2_word) fill_data &lt;= mem_rd_data;  // 요청 워드만 잡아둠
        fill_beat &lt;= fill_beat + 1;
    end

ST_COMPLETE:
    valid_bits[s2_set][victim_way] &lt;= 1&#39;b1;      // 라인 유효화
    dirty_bits[s2_set][victim_way] &lt;= s2_rw;     // write miss면 dirty
    plru_bits[s2_set] &lt;= plru_update(..., victim_way);</code></pre>
<p><strong><code>wb_cnt-1</code> 캡처의 의미</strong>: 동기 read라 주소를 준 다음 사이클에 데이터가 옴.
<code>wb_cnt=N</code>일 때 word N 주소를 인가(§3 ②) → <code>wb_cnt=N+1</code>이 됐을 때 word N 데이터 도착
→ <code>wb_buf[N]</code>에 저장. 그래서 &quot;한 사이클 뒤(<code>wb_cnt-1</code>)&quot;에 캡처.</p>
<p><strong>왜 victim을 버퍼에 복사?</strong> fill(ST_FILL_RCV)이 <strong>같은 물리 위치</strong>
<code>{s2_set, victim_way}</code>에 새 라인을 덮어쓴다. 그러니 덮이기 전에 victim을 먼저 읽어내야
한다 → WB_READ에서 <code>wb_buf</code>로 스냅샷.</p>
<hr>
<h2 id="9-fsm-ii--조합-블록-next_state--출력-344444행">9. FSM (II) — 조합 블록: next_state + 출력 (344~444행)</h2>
<p>맨 위에 <strong>기본값 전부 비활성</strong>(모든 we=0, mem_req=0, stall=0…)을 깔고, 상태별로 덮어씀.
이게 latch 방지 + 안전한 기본 상태의 정석.</p>
<pre><code>ST_IDLE:
    hit &amp; write → data_we[hit_way]=1, data_wbe=s2_be   (제자리 갱신)
    miss        → stall=1, next = victim_dirty ? WB_READ : FILL_REQ

ST_WB_READ:  stall=1;  wb_cnt==4 → next=WB_SEND
ST_WB_SEND:  stall=1
    !req_sent → mem_req(rw=1, victim 주소) 발행
    req_sent  → mem_wr_data=wb_buf[wb_ptr], last=마지막워드; ready&amp;last → next=FILL_REQ
ST_FILL_REQ: stall=1;  mem_req(rw=0, 새 라인 주소);  req_ready → next=FILL_RCV
ST_FILL_RCV: stall=1
    mem_rd_valid → data_we[victim_way]=1, wbe=all, {set,fill_beat}에 기록; last → next=COMPLETE
ST_COMPLETE: stall=0(!);  tag_we[victim_way]=1(태그 커밋);
             write면 data_we[victim_way]=1 + s2_be (스토어 반영);  next=IDLE</code></pre><p><strong>miss 처리 흐름 (dirty)</strong>: IDLE → WB_READ(victim 버퍼링) → WB_SEND(메모리로 스트리밍)
→ FILL_REQ(새 라인 요청) → FILL_RCV(4비트 수신) → COMPLETE(커밋+응답) → IDLE.
clean victim이면 WB 두 단계 건너뛰고 바로 FILL_REQ.</p>
<hr>
<h2 id="10-★-반드시-외울-설계-포인트-3개">10. ★ 반드시 외울 설계 포인트 3개</h2>
<h3 id="a-st_complete에서-stall을-푼다-428440행">(a) <code>ST_COMPLETE</code>에서 stall을 <strong>푼다</strong> (428~440행)</h3>
<pre><code class="language-systemverilog">ST_COMPLETE: begin
    pipeline_stall = 1&#39;b0;   // ← 여기서 해제
    ...
end</code></pre>
<p>여기서 stall을 유지하면 다음 사이클 <code>ST_IDLE</code>에서 <strong>S2가 같은 요청을 그대로 들고 있어
응답을 두 번</strong> 내보낸다. stall을 풀어야 파이프라인이 진행되어 S2가 넘어감.</p>
<h3 id="b-왜-조합-루프가-없나">(b) 왜 조합 루프가 없나</h3>
<p><code>tag_raddr</code>가 <code>pipeline_stall</code>로 선택되지만, <code>tag_rdata</code>는 <strong>플롭 출력</strong>(동기 SRAM)이라
<code>stall→raddr→rdata→hit→stall</code> 피드백이 끊긴다. 비동기 SRAM이면 이 mux가 루프를 닫는다
(이전 버전의 675 버그).</p>
<h3 id="c-always_comb-정적-변수-함정-§6">(c) <code>always_comb</code> 정적 변수 함정 (§6)</h3>
<p>절차 블록 선언 초기화는 static. 루프/조합 블록에선 선언과 대입 분리.</p>
<hr>
<h2 id="11-cycle-by-cycle--clean-read-miss-가장-단순">11. Cycle-by-cycle — clean read miss (가장 단순)</h2>
<p>메모리 latency=2 가정. victim이 clean이라 write-back 없음.</p>
<table>
<thead>
<tr>
<th>cycle</th>
<th>state</th>
<th>하는 일</th>
<th>stall</th>
</tr>
</thead>
<tbody><tr>
<td>N</td>
<td>ST_IDLE</td>
<td>S2에 read 도착, miss 판정, victim 래치</td>
<td>1</td>
</tr>
<tr>
<td>N+1</td>
<td>ST_FILL_REQ</td>
<td>mem_req(rw=0) 발행, req_ready 대기</td>
<td>1</td>
</tr>
<tr>
<td>N+2</td>
<td>ST_FILL_RCV</td>
<td>(latency 대기)</td>
<td>1</td>
</tr>
<tr>
<td>N+3</td>
<td>ST_FILL_RCV</td>
<td>beat0 수신 → data[set,0] 기록, 요청워드면 fill_data</td>
<td>1</td>
</tr>
<tr>
<td>N+4~6</td>
<td>ST_FILL_RCV</td>
<td>beat1,2,3 수신·기록; last에서 → COMPLETE</td>
<td>1</td>
</tr>
<tr>
<td>N+7</td>
<td>ST_COMPLETE</td>
<td>valid=1,dirty=0,PLRU갱신; rsp_valid+fill_data; <strong>stall=0</strong></td>
<td>0</td>
</tr>
<tr>
<td>N+8</td>
<td>ST_IDLE</td>
<td>다음 요청 진행</td>
<td>-</td>
</tr>
</tbody></table>
<p>dirty miss면 N+1 앞에 <strong>WB_READ(≈5) + WB_SEND(req+4beat)</strong> 가 추가된다.</p>
<hr>
<h2 id="12-면접-예상-질문">12. 면접 예상 질문</h2>
<table>
<thead>
<tr>
<th>질문</th>
<th>답</th>
</tr>
</thead>
<tbody><tr>
<td>hit는 왜 FSM을 안 거치나?</td>
<td>hit는 조합적으로 응답/write 가능. FSM은 miss(여러 사이클)만 처리. 성능 위해 hit path를 짧게</td>
</tr>
<tr>
<td>4-way에서 hit_way를 어떻게?</td>
<td>4개 태그를 병렬 비교, valid까지 AND, OR 리덕션으로 hit, 인코딩으로 way</td>
</tr>
<tr>
<td>victim은 어떻게 고르나?</td>
<td>invalid way 우선, 없으면 tree-PLRU</td>
</tr>
<tr>
<td>write-back 데이터를 왜 버퍼링?</td>
<td>fill이 같은 {set,victim_way} 위치를 덮으므로, 덮기 전에 victim을 읽어내야 함</td>
</tr>
<tr>
<td>ST_COMPLETE가 없으면?</td>
<td>fill 마지막 beat와 store write가 같은 사이클에 충돌 + 응답 이중 발생</td>
</tr>
<tr>
<td>stall 중 파이프라인 레지스터는?</td>
<td>홀드(<code>!pipeline_stall</code>일 때만 갱신). 요청이 날아가지 않게</td>
</tr>
<tr>
<td>forwarding 비교를 s2로 하면 안 되는 이유?</td>
<td>WB_READ에서 read 주소가 라인을 순회해 s2_word와 다름. 실제 인가한 prev_raddr로 비교해야</td>
</tr>
<tr>
<td>조합 루프가 없는 이유?</td>
<td>rdata가 플롭 출력이라 stall로의 피드백이 끊김 (동기 SRAM 덕분)</td>
</tr>
<tr>
<td>read 채널에 ready가 없는 이유?</td>
<td>fill은 캐시가 자기 배열을 채우는 것 → 항상 수용 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="13-셀프-체크">13. 셀프 체크</h2>
<ol>
<li><code>pipeline_stall</code>이 1이면 S1→S2 레지스터는 어떻게 되나? 왜 그래야 하나?</li>
<li>clean miss와 dirty miss의 상태 경로 차이는?</li>
<li><code>ST_WB_READ</code>에서 <code>wb_buf[wb_cnt-1]</code>로 한 사이클 뒤에 캡처하는 이유는?</li>
<li>forwarding에서 <code>prev_data_raddr</code>(직전 read 주소)로 비교하는 이유는? <code>s2</code>면 왜 안 되나?</li>
<li><code>ST_COMPLETE</code>에서 <code>pipeline_stall = 0</code>으로 푸는 이유는?</li>
<li>victim으로 dirty way가 뽑혔는데 그걸 버퍼에 안 읽고 바로 fill하면 무슨 일이?</li>
</ol>
<details>
<summary>답</summary>

<ol>
<li><strong>홀드</strong>(값 유지). miss 처리 동안 S2가 그 요청을 붙잡고 있어야 요청이 사라지지 않음.</li>
<li>clean: IDLE→FILL_REQ→FILL_RCV→COMPLETE. dirty: 앞에 WB_READ→WB_SEND 추가.</li>
<li>동기 read라 주소 준 다음 사이클에 데이터 도착. word N 주소를 wb_cnt=N에 인가 →
wb_cnt=N+1에 데이터 도착 → wb_buf[N]에 저장.</li>
<li>WB_READ에서 read 주소가 라인을 순회(word 0,1,2,3)해 s2_word와 다름.
&quot;직전에 실제로 인가한 read 주소&quot;와 비교해야 그 위치의 stale을 정확히 잡음.</li>
<li>안 풀면 다음 사이클 IDLE에서 S2가 같은 요청을 들고 있어 응답을 두 번 냄<ul>
<li>fill 마지막 write와 store write 충돌.</li>
</ul>
</li>
<li>fill이 같은 {set,victim_way} 위치를 덮어써서 victim(수정된 값)이 메모리에 못 나가고
사라짐 → 데이터 유실. 그래서 WB_READ로 먼저 스냅샷.</details>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[rtl/sram_macro.sv]]></title>
            <link>https://velog.io/@houston_guy2/rtlsrammacro.sv</link>
            <guid>https://velog.io/@houston_guy2/rtlsrammacro.sv</guid>
            <pubDate>Sun, 09 Aug 2026 05:54:21 GMT</pubDate>
            <description><![CDATA[<p>캐시의 태그/데이터 저장 배열 모델. 작지만 <strong>이 프로젝트 버그 스토리의 핵심</strong>이라,
동기 읽기·read-before-write·forwarding이 왜 한 세트인지 여기서 이해하면 면접에서
크게 쓴다.</p>
<hr>
<h2 id="1-역할">1. 역할</h2>
<p>캐시가 태그와 데이터를 담는 <strong>SRAM의 동작 모델</strong>. 실칩에선 벤더의 컴파일된 SRAM
매크로를 쓰지만, 시뮬레이션에선 그 동작을 흉내 내는 behavioral 모델이 필요하다.</p>
<p>모듈 2개:</p>
<table>
<thead>
<tr>
<th>모듈</th>
<th>용도</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><code>sram_macro</code></td>
<td>태그 배열</td>
<td>평범한 read/write</td>
</tr>
<tr>
<td><code>sram_macro_be</code></td>
<td>데이터 배열</td>
<td>byte enable → partial write 지원</td>
</tr>
</tbody></table>
<p>4-way라서 <a href="../rtl/l1_cache_top.sv">l1_cache_top.sv</a>에서 <strong>way당 하나씩, 총 8개</strong>
(태그 4 + 데이터 4)를 <code>generate</code>로 인스턴스화한다.</p>
<hr>
<h2 id="2-핵심-개념--동기-읽기-synchronous-read">2. 핵심 개념 — 동기 읽기 (synchronous read)</h2>
<pre><code class="language-systemverilog">always_ff @(posedge clk) begin
    rdata &lt;= mem[raddr];    // 주소는 이번 엣지에, 데이터는 다음 엣지에
    ...
end</code></pre>
<blockquote>
<p><strong>&quot;N번째 클럭에 주소(<code>raddr</code>)를 주면, N+1번째 클럭에 데이터(<code>rdata</code>)가 나온다.&quot;</strong></p>
</blockquote>
<p><code>rdata</code>가 플롭(<code>always_ff</code>의 <code>&lt;=</code>)이라서다. 조합이 아니라 레지스터를 거쳐 나온다.</p>
<p>이게 <strong>2단 파이프라인을 성립시킨다</strong>:</p>
<ul>
<li><strong>S1</strong>: CPU 요청 → set index를 SRAM 주소에 인가</li>
<li><strong>S2</strong>: (다음 사이클) 태그/데이터 도착 → hit/miss 판정</li>
</ul>
<p>주소를 준 사이클과 데이터가 나오는 사이클이 1칸 어긋난 것 = 파이프라인 단 경계.</p>
<h3 id="⚠️-버그-스토리-면접-최고-소재">⚠️ 버그 스토리 (면접 최고 소재)</h3>
<p>이전 버전은 <strong>비동기 읽기</strong>였다:</p>
<pre><code class="language-systemverilog">assign rdata = mem[raddr];   // 조합 — 주소 주면 즉시 데이터</code></pre>
<p>두 가지가 망가졌다:</p>
<ol>
<li><strong>조합 루프</strong>: <code>stall → raddr → rdata → hit → stall</code> 이 닫힌 고리 → 합성 시 발진</li>
<li><strong>파이프라인 모델 불일치</strong>: S1 주소 → S2 데이터 구조인데 즉시 읽기라 타이밍 안 맞음
→ read 1495건 중 <strong>675건(45%) 오답</strong></li>
</ol>
<p><code>assign</code> → <code>always_ff @(posedge clk)</code> 한 줄로 675 에러 → 0.
&quot;동기 vs 비동기 SRAM 읽기 차이&quot;를 물으면 이 실제 경험으로 답한다.</p>
<hr>
<h2 id="3-sram_macro-블록별">3. <code>sram_macro</code> 블록별</h2>
<pre><code class="language-c">logic [DATA_WIDTH-1:0] mem [0:(1&lt;&lt;ADDR_WIDTH)-1];</code></pre>
<ul>
<li><code>1&lt;&lt;ADDR_WIDTH</code> = 2^ADDR_WIDTH (비트 시프트로 2의 거듭제곱)</li>
<li>ADDR_WIDTH=8 → <code>mem[0:255]</code> = 256워드, 각 워드 DATA_WIDTH 비트</li>
<li><code>[DATA_WIDTH-1:0]</code> = <strong>packed</strong>(워드 안 비트), <code>[0:...]</code> = <strong>unpacked</strong>(워드 개수)</li>
</ul>
<pre><code class="language-c">always_ff @(posedge clk) begin
    rdata &lt;= mem[raddr];              // 읽기
    if (we) mem[waddr] &lt;= wdata;      // 쓰기 (we=1일 때만)
end</code></pre>
<ul>
<li>매 상승엣지에 읽기·쓰기 동시 (1 read port + 1 write port)</li>
<li><code>we</code>(write enable)가 1일 때만 <code>waddr</code>에 기록</li>
</ul>
<hr>
<h2 id="4-미묘하지만-중요--read-before-write-충돌">4. 미묘하지만 중요 — read-before-write 충돌</h2>
<p>주석의 &quot;sampled before this edge&#39;s write&quot;가 이 부분. 같은 사이클에 <strong>같은 주소를
읽고 쓰면?</strong></p>
<pre><code class="language-c">rdata &lt;= mem[raddr];           // ①
if (we) mem[waddr] &lt;= wdata;   // ②</code></pre>
<p><code>&lt;=</code>(non-blocking)의 규칙: <strong>우변(RHS)을 먼저 전부 계산 → 좌변(LHS)을 나중에 한꺼번에 갱신.</strong></p>
<p><code>raddr == waddr</code> 이고 <code>we=1</code> 일 때:</p>
<ul>
<li>①의 우변 <code>mem[raddr]</code>은 <strong>쓰기 전 옛날 값</strong>으로 계산됨</li>
<li>②는 그 후에 <code>mem</code> 갱신</li>
</ul>
<p>→ <strong><code>rdata</code>는 옛날 값을 내보낸다</strong> (방금 쓴 값이 아니라).</p>
<p>포인트: <strong>두 문장 순서를 바꿔도 결과 동일</strong>. NBA는 우변을 전부 먼저 평가하니까.
이게 &quot;read-before-write&quot; / &quot;read-old-data&quot; 동작이고, 실제 SRAM의 흔한 모드다.</p>
<h3 id="그래서-코어에-forwarding이-필요">그래서 코어에 forwarding이 필요</h3>
<p>배열이 &quot;쓰기 직전 값&quot;을 주므로, <strong>직전 사이클에 쓴 위치를 지금 읽으면 stale</strong>.
<a href="../rtl/l1_cache_core.sv">l1_cache_core.sv</a>의 <code>safe_data_rdata</code>/<code>safe_tag_rdata</code>가 우회:</p>
<blockquote>
<p>&quot;직전 사이클에 이 주소에 썼나? 그러면 배열 값 대신 그때 쓴 데이터를 써라&quot;</p>
</blockquote>
<p>이 SRAM의 read-before-write 성질과 코어의 forwarding은 <strong>한 세트</strong>.
하나를 이해하면 다른 하나가 왜 있는지 보인다.</p>
<p><strong>타이밍 정리</strong></p>
<pre><code>cycle N   : mem[A]에 write (엣지 끝에서 mem 갱신)
            같은 A를 raddr로도 인가
cycle N+1 : rdata = (N 시점의 옛날 mem[A])  ← stale! forwarding이 여기서 개입
cycle N+2 : rdata = (새 값)                  ← 이제 배열 자체가 새 값</code></pre><p>forwarding이 딱 <strong>1사이클 깊이</strong>면 충분한 이유가 이 표에 있다.</p>
<hr>
<h2 id="5-sram_macro_be--byte-enable-버전">5. <code>sram_macro_be</code> — byte enable 버전</h2>
<p>데이터 배열은 partial write(워드 일부 바이트만)를 지원해야 한다. 차이는 쓰기부:</p>
<pre><code class="language-systemverilog">if (we) begin
    for (int b = 0; b &lt; BE_WIDTH; b++)
        if (wbe[b]) mem[waddr][b*8 +: 8] &lt;= wdata[b*8 +: 8];
end</code></pre>
<ul>
<li><code>BE_WIDTH = DATA_WIDTH/8 = 4</code> (32비트 = 4바이트)</li>
<li>바이트 레인 4개를 각각 확인: <code>wbe[b]=1</code>인 레인만 갱신</li>
<li><code>wbe=4&#39;b0001</code> → 최하위 바이트만, <code>4&#39;b1111</code> → 전체 워드</li>
</ul>
<p>예: 워드 <code>0xAABBCCDD</code>, <code>wdata=0x11223344</code>, <code>wbe=4&#39;b0011</code>
→ 결과 <code>0xAABB3344</code> (상위 2바이트 유지, 하위 2바이트 교체)</p>
<h3 id="-연산자-indexed-part-select--문법-단골"><code>+:</code> 연산자 (indexed part-select) — 문법 단골</h3>
<pre><code class="language-systemverilog">wdata[b*8 +: 8]   // &quot;b*8 비트부터 위로 8비트&quot;</code></pre>
<table>
<thead>
<tr>
<th>b</th>
<th><code>b*8 +: 8</code></th>
<th>실제 비트</th>
<th>바이트</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td><code>0 +: 8</code></td>
<td><code>[7:0]</code></td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td><code>8 +: 8</code></td>
<td><code>[15:8]</code></td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td><code>16 +: 8</code></td>
<td><code>[23:16]</code></td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td><code>24 +: 8</code></td>
<td><code>[31:24]</code></td>
<td>3</td>
</tr>
</tbody></table>
<p><code>for</code> 변수로 비트 범위를 <code>[b*8+7 : b*8]</code> 처럼 쓰면 <strong>변수 범위 불가로 문법 에러</strong>.
그래서 <code>+:</code>(또는 <code>-:</code>)를 쓴다.</p>
<hr>
<h2 id="6-큰-그림">6. 큰 그림</h2>
<pre><code>CPU 요청 ──▶ [코어 S1] ──raddr──▶ [이 SRAM] ──rdata(1사이클 뒤)──▶ [코어 S2] ──▶ hit/miss
                                       ▲
                             read-before-write 성질
                                       │
                            [코어 forwarding이 보완]</code></pre><hr>
<h2 id="7-면접-예상-질문">7. 면접 예상 질문</h2>
<table>
<thead>
<tr>
<th>질문</th>
<th>답</th>
</tr>
</thead>
<tbody><tr>
<td>동기 vs 비동기 읽기?</td>
<td>동기=주소→데이터 1사이클 지연(플롭), 비동기=즉시(조합). 실 SRAM은 동기. 비동기는 조합 루프·타이밍 문제</td>
</tr>
<tr>
<td><code>&lt;=</code>와 <code>=</code> 차이가 왜 중요?</td>
<td>NBA라 우변 먼저 전부 평가 → 같은 주소 read/write 시 읽기가 옛날 값(read-before-write)</td>
</tr>
<tr>
<td>read-before-write가 문제되나?</td>
<td>직전 사이클 쓴 값을 바로 읽으면 stale → 코어 forwarding으로 해결</td>
</tr>
<tr>
<td>왜 데이터 배열만 byte enable?</td>
<td>태그는 통째 갱신, 데이터는 CPU가 바이트 단위 write 가능</td>
</tr>
<tr>
<td><code>1&lt;&lt;ADDR_WIDTH</code>?</td>
<td>2^ADDR_WIDTH, 메모리 깊이</td>
</tr>
<tr>
<td>합성되나?</td>
<td>block RAM으로 추론(infer)됨. 실칩에선 보통 벤더 매크로로 교체</td>
</tr>
<tr>
<td><code>+:</code>는 왜?</td>
<td>for 변수로 비트 범위 잡을 때 <code>[a:b]</code>는 변수 불가라 indexed part-select 사용</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-셀프-체크-안-보고-답해보기">8. 셀프 체크 (안 보고 답해보기)</h2>
<ol>
<li><code>raddr==waddr</code>, <code>we=1</code> 일 때 <code>rdata</code>에 나오는 건 새 값인가 옛날 값인가? 왜?</li>
<li><code>assign rdata = mem[raddr]</code> 로 바꾸면 어떤 두 가지가 깨지나?</li>
<li>forwarding이 1사이클 깊이면 충분한 이유는?</li>
<li><code>wbe=4&#39;b1010</code>, 워드 <code>0x11223344</code>, <code>wdata=0xAABBCCDD</code> → 결과는?</li>
<li>왜 태그 배열엔 byte enable이 없나?</li>
</ol>
<details>
<summary>답</summary>

<ol>
<li><strong>옛날 값</strong>. NBA라 ①의 우변 <code>mem[raddr]</code>이 쓰기 전에 평가됨.</li>
<li>조합 루프(<code>stall→raddr→rdata→hit→stall</code>), 파이프라인 타이밍 불일치(675 오답).</li>
<li>write 다음 사이클엔 배열 자체가 이미 새 값이라, stale인 건 딱 그 한 사이클뿐.</li>
<li><code>0xAA22CC44</code> (레인 3,1만 교체: <code>AA</code>·<code>CC</code>, 레인 2,0은 유지: <code>22</code>·<code>44</code>).
→ <code>wbe[3]=1</code>→byte3=AA, <code>wbe[1]=1</code>→byte1=CC, 나머지 유지.</li>
<li>태그는 항상 워드 전체를 통째로 갱신(부분 태그 쓰기가 없음).</details>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[l1_cache_pkg.sv]]></title>
            <link>https://velog.io/@houston_guy2/L1-Cache-1-l1cachepkg</link>
            <guid>https://velog.io/@houston_guy2/L1-Cache-1-l1cachepkg</guid>
            <pubDate>Thu, 06 Aug 2026 04:41:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>L1 Cache Controller 프로젝트 시리즈 1편
캐시의 기하학적 정의, 주소 분해, FSM 상태, PLRU 함수</p>
</blockquote>
<hr>
<h2 id="이-파일의-역할">이 파일의 역할</h2>
<pre><code>l1_cache_pkg = 캐시의 &quot;설계도 상수 + 도구 모음&quot;

모든 캐시 파일이 이 패키지를 import해서
  - 캐시 크기/구조 (파라미터)
  - 주소를 어떻게 나누는지 (헬퍼 함수)
  - FSM 상태
  - PLRU 로직
을 공유한다. &quot;단일 진실 공급원(single source of truth)&quot;.</code></pre><hr>
<h2 id="목차">목차</h2>
<ol>
<li>Cache Geometry — 캐시 구조 파라미터</li>
<li>주소 분해 — tag/set/word/byte</li>
<li>타입 정의 (typedef)</li>
<li>Controller FSM — 상태 정의</li>
<li>Tag 배열 구조</li>
<li>주소 헬퍼 함수</li>
<li>Tree-PLRU 함수</li>
</ol>
<hr>
<h2 id="1-cache-geometry--캐시-구조">1. Cache Geometry — 캐시 구조</h2>
<pre><code class="language-c">localparam int ADDR_WIDTH     = 32;
localparam int DATA_WIDTH     = 32;
localparam int BE_WIDTH       = DATA_WIDTH/8;          // 4 (byte enable)

localparam int WAYS           = 4;                     // 4-way
localparam int WAY_BITS       = 2;                     // $clog2(4)
localparam int SETS           = 64;                    // 64 sets
localparam int SET_BITS       = 6;                     // $clog2(64)
localparam int WORDS_PER_LINE = 4;                     // 라인당 4워드
localparam int WORD_BITS      = 2;                     // $clog2(4)
localparam int BYTE_BITS      = 2;                     // 워드 안 바이트
localparam int OFFSET_BITS    = WORD_BITS + BYTE_BITS; // 4 → 16B 라인
localparam int TAG_BITS       = ADDR_WIDTH - SET_BITS - OFFSET_BITS;  // 22</code></pre>
<h3 id="4kb-계산">4KB 계산</h3>
<pre><code>캐시 라인 = WORDS_PER_LINE × 4바이트
          = 4 × 4 = 16바이트

전체 크기 = 라인 × SETS × WAYS
          = 16B × 64 × 4
          = 4096B = 4KB ✅</code></pre><table>
<thead>
<tr>
<th>파라미터</th>
<th>값</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>WAYS</td>
<td>4</td>
<td>한 set에 4개 라인 (4-way)</td>
</tr>
<tr>
<td>SETS</td>
<td>64</td>
<td>set 개수</td>
</tr>
<tr>
<td>WORDS_PER_LINE</td>
<td>4</td>
<td>한 라인에 4워드</td>
</tr>
<tr>
<td>BE_WIDTH</td>
<td>4</td>
<td>byte enable (바이트별 쓰기)</td>
</tr>
<tr>
<td>OFFSET_BITS</td>
<td>4</td>
<td>라인 안 위치 (16B → 4비트)</td>
</tr>
<tr>
<td>TAG_BITS</td>
<td>22</td>
<td>태그 폭</td>
</tr>
</tbody></table>
<h3 id="clog2--비트-폭-계산">$clog2 — 비트 폭 계산</h3>
<pre><code>$clog2(N) = N을 표현하는 데 필요한 비트 수 (ceiling log2)

$clog2(4)  = 2   (WAYS=4 → WAY_BITS=2)
$clog2(64) = 6   (SETS=64 → SET_BITS=6)

왜? 4개를 구분하려면 2비트 (00,01,10,11)
    64개를 구분하려면 6비트</code></pre><h3 id="data_addr_bits">DATA_ADDR_BITS</h3>
<pre><code class="language-c">localparam int DATA_ADDR_BITS = SET_BITS + WORD_BITS;  // 8 → 256 words/way</code></pre>
<pre><code>데이터 배열 주소 = {set, word}
  = 6비트 + 2비트 = 8비트
  = 2^8 = 256워드 per way

한 way에 256워드 = 64 set × 4 word ✅</code></pre><hr>
<h2 id="2-주소-분해--핵심-개념">2. 주소 분해 — 핵심 개념</h2>
<pre><code>addr[31:10] = tag          (22b)
addr[ 9: 4] = set index    (6b → 64 sets)
addr[ 3: 2] = word in line (2b → 4 words)
addr[ 1: 0] = byte in word (2b)</code></pre><h3 id="그림">그림</h3>
<pre><code>32비트 주소:

 31                    10  9      4  3   2  1   0
┌────────────────────────┬─────────┬──────┬──────┐
│         tag            │   set   │ word │ byte │
│        (22비트)         │ (6비트)  │(2비트)│(2비트)│
└────────────────────────┴─────────┴──────┴──────┘
         │                    │        │       │
    &quot;이 데이터 맞나&quot;         64개 set    라인 안   워드 안
     확인용                 중 선택     4워드     4바이트</code></pre><h3 id="각-필드가-하는-일">각 필드가 하는 일</h3>
<pre><code class="language-c">byte (addr[1:0]):
  워드(4바이트) 안에서 어느 바이트
  → byte enable로 선택

word (addr[3:2]):
  캐시 라인(16B=4워드) 안에서 어느 워드
  → 2^2 = 4워드

set (addr[9:4]):
  64개 set 중 어디에 매핑되나
  → 2^6 = 64 set

tag (addr[31:10]):
  같은 set에 여러 주소가 매핑되므로
  → 태그로 &quot;진짜 이 주소 맞나&quot; 확인
  → 4개 way의 태그와 비교</code></pre>
<h3 id="왜-이렇게-나누나-지역성">왜 이렇게 나누나 (지역성)</h3>
<pre><code>낮은 비트(byte/word) = offset
  → 인접 주소는 같은 라인 → 공간 지역성 활용

중간 비트(set) = index
  → 주소를 64개 set에 분산

높은 비트(tag) = 나머지
  → 같은 set 안에서 구분</code></pre><hr>
<h2 id="3-타입-정의-typedef">3. 타입 정의 (typedef)</h2>
<pre><code class="language-c">typedef logic [SET_BITS-1:0]       set_t;       // set 번호 (6비트)
typedef logic [TAG_BITS-1:0]       tag_t;       // 태그 (22비트)
typedef logic [WAY_BITS-1:0]       way_t;       // way 번호 (2비트)
typedef logic [WORD_BITS-1:0]      word_t;      // word 번호 (2비트)
typedef logic [DATA_ADDR_BITS-1:0] data_addr_t; // 데이터 주소 (8비트)
typedef logic [BE_WIDTH-1:0]       be_t;        // byte enable (4비트)</code></pre>
<pre><code>typedef = 타입에 이름 붙이기

logic [5:0] 대신 set_t
→ 코드가 의미로 읽힘: set_t set = get_set(addr);
→ 폭이 바뀌어도 typedef만 고치면 됨

이름 규칙: _t 접미사 = type</code></pre><hr>
<h2 id="4-controller-fsm--상태-정의">4. Controller FSM — 상태 정의</h2>
<pre><code class="language-c">typedef enum logic [2:0] {
    ST_IDLE     = 3&#39;d0,
    ST_WB_READ  = 3&#39;d1,   // dirty victim을 데이터 배열에서 읽기
    ST_WB_SEND  = 3&#39;d2,   // victim을 메인 메모리로 전송
    ST_FILL_REQ = 3&#39;d3,   // 라인 fetch 요청
    ST_FILL_RCV = 3&#39;d4,   // 버스트 받아서 배열에 씀
    ST_COMPLETE = 3&#39;d5    // tag/valid/dirty/PLRU 갱신, CPU 응답
} cache_state_t;</code></pre>
<h3 id="상태-흐름">상태 흐름</h3>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/09548291-697c-4dcc-bcbd-b3d4e1b9ffe8/image.png" alt=""></p>
<h3 id="각-상태의-역할">각 상태의 역할</h3>
<pre><code>ST_IDLE:     CPU 요청 대기. hit이면 여기서 바로 처리
ST_WB_READ:  쫓겨날 victim이 dirty면 먼저 데이터 배열에서 읽음
ST_WB_SEND:  읽은 victim을 메인 메모리에 씀 (write-back)
ST_FILL_REQ: 새 라인을 메모리에서 가져오라고 요청
ST_FILL_RCV: 버스트로 오는 데이터를 받아서 배열에 저장
ST_COMPLETE: 태그/valid/dirty/PLRU 갱신하고 CPU에 응답</code></pre><h3 id="주석의-핵심--st_complete의-이유">주석의 핵심 — ST_COMPLETE의 이유</h3>
<pre><code>// ST_COMPLETE costs one extra cycle per miss but removes the structural
// conflict between the last fill beat and the pending store&#39;s write.</code></pre><pre><code>ST_COMPLETE를 따로 둔 이유:

마지막 fill beat(FILL_RCV)와
대기 중인 store의 쓰기가 같은 사이클에 충돌
→ 구조적 충돌 (structural conflict)

해결: 완료 사이클을 하나 더 둠 (ST_COMPLETE)
→ miss당 1사이클 손해
→ 대신 충돌 제거

이게 이력서의 combinational loop 이슈와 같은 맥락!
  (구조적 충돌을 상태 분리로 해결)</code></pre><h3 id="write-back--write-allocate와-연결">write-back / write-allocate와 연결</h3>
<pre><code>miss가 나면 (write-allocate):
  victim 있음 + dirty → WB_READ → WB_SEND (메모리에 씀)
  victim 없음 or clean → 바로 FILL_REQ

→ dirty victim만 write-back (깨끗하면 그냥 덮어씀)
→ write-back 정책: 캐시에만 쓰다가 쫓길 때 메모리로</code></pre><hr>
<h2 id="5-tag-배열-구조">5. Tag 배열 구조</h2>
<pre><code class="language-systemverilog">typedef struct packed {
    tag_t tag;
} tag_data_t;</code></pre>
<pre><code>태그 배열의 한 엔트리 = 태그만 담음

주석: &quot;valid/dirty live in flops in the core (so reset flushes the
       whole cache in one cycle); only the tag needs an SRAM.&quot;

valid/dirty는 플립플롭에 (SRAM 아님)
  → 리셋 시 한 사이클에 전체 캐시 무효화 가능
  (SRAM은 한 번에 리셋 못 함)

태그는 SRAM에
  → 용량이 크니까 (22비트 × 256엔트리)

struct packed = 구조체를 비트로 촘촘히 packing</code></pre><pre><code>왜 valid/dirty를 플롭에?
  리셋하면 모든 valid=0 되어야 함 (캐시 비우기)
  SRAM은 일괄 리셋 불가 → 플롭이면 한 사이클에 전부 0
  태그는 valid=0이면 어차피 무시되므로 리셋 불필요 → SRAM OK</code></pre><hr>
<h2 id="6-주소-헬퍼-함수">6. 주소 헬퍼 함수</h2>
<p>주소에서 각 필드를 뽑는 함수들. <strong>단일 진실 공급원</strong>.</p>
<pre><code class="language-systemverilog">function automatic set_t get_set(input logic [ADDR_WIDTH-1:0] addr_in);
    return addr_in[OFFSET_BITS +: SET_BITS];       // addr[4 +: 6] = addr[9:4]
endfunction

function automatic tag_t get_tag(input logic [ADDR_WIDTH-1:0] addr_in);
    return addr_in[ADDR_WIDTH-1 -: TAG_BITS];      // addr[31 -: 22] = addr[31:10]
endfunction

function automatic word_t get_word(input logic [ADDR_WIDTH-1:0] addr_in);
    return addr_in[BYTE_BITS +: WORD_BITS];        // addr[2 +: 2] = addr[3:2]
endfunction</code></pre>
<h3 id="-와---문법"><code>+:</code> 와 <code>-:</code> 문법</h3>
<pre><code>+: = &quot;시작 비트부터 위로 N비트&quot;
addr[4 +: 6] = addr[4], addr[5], ... addr[9] = addr[9:4]

-: = &quot;시작 비트부터 아래로 N비트&quot;
addr[31 -: 22] = addr[31], addr[30], ... addr[10] = addr[31:10]

왜 이 문법? 폭(TAG_BITS 등)이 파라미터라
          addr[31:10] 처럼 못 씀 (숫자 고정)
          → +:/-: 로 &quot;시작점 + 폭&quot;으로 표현</code></pre><h3 id="⚠️-주석의-xsim-함정-매우-중요">⚠️ 주석의 XSim 함정 (매우 중요)</h3>
<pre><code>// the formal argument names are deliberately verbose. Short names
// like `a`, `t`, `i` collide with ordinary loop/local variables at call
// sites, and xsim was observed evaluating the actual argument expression
// in the callee&#39;s scope when the names matched — silently passing 0.</code></pre><pre><code>XSim 버그:
  함수 인자 이름이 호출부의 변수 이름과 같으면
  → XSim이 엉뚱한 스코프에서 값을 읽음
  → 조용히 0이 전달됨! (에러 없이)

예: get_set(a) 호출 시 인자 이름도 a면 충돌

해결: addr_in처럼 긴 이름 사용
  → 짧은 이름(a, t, i)이 루프 변수와 겹치는 걸 회피

→ AXI SVA의 vacuous assertion과 같은 종류의 XSim 함정
→ &quot;조용히 틀린 값&quot; = 가장 위험한 버그</code></pre><p>이건 면접에서 좋은 이야깃거리예요. &quot;시뮬레이터의 조용한 버그를 어떻게 피했나&quot;.</p>
<h3 id="라인-주소-헬퍼">라인 주소 헬퍼</h3>
<pre><code class="language-systemverilog">// addr가 속한 라인의 시작(word 0) 주소
function automatic logic [ADDR_WIDTH-1:0] get_line_addr(input logic [ADDR_WIDTH-1:0] addr_in);
    return {addr_in[ADDR_WIDTH-1 : OFFSET_BITS], {OFFSET_BITS{1&#39;b0}}};
endfunction</code></pre>
<pre><code>offset 비트를 0으로 만들어 라인 시작 주소 계산

addr = 0x...1234 (offset=0100 = word 1, byte 0)
  → 상위 비트 유지 + 하위 4비트 0
  → 0x...1230 (라인 시작)

{상위, {4{1&#39;b0}}} = 상위 비트 + 0000 이어붙이기</code></pre><p>이건 AXI ref_model의 <code>word_base = a - (a % 4)</code>와 같은 원리예요 (경계 정렬). 여기선 16B 라인 정렬.</p>
<pre><code class="language-systemverilog">// 태그+set으로 라인 주소 재구성
function automatic logic [ADDR_WIDTH-1:0] make_line_addr(input tag_t tag_in, input set_t set_in);
    return {tag_in, set_in, {OFFSET_BITS{1&#39;b0}}};
endfunction

// set+word로 데이터 배열 주소
function automatic data_addr_t make_data_addr(input set_t set_in, input word_t word_in);
    return {set_in, word_in};
endfunction</code></pre>
<pre><code>make_line_addr: {tag, set, 0000} → 라인 주소 조립
  (victim을 메모리에 쓸 때 주소 계산 등)

make_data_addr: {set, word} → 데이터 배열 인덱스
  (SRAM 접근 주소)</code></pre><hr>
<h2 id="7-tree-plru-함수-핵심">7. Tree-PLRU 함수 (핵심)</h2>
<p>앞서 개념으로 배운 tree-PLRU의 실제 코드.</p>
<pre><code class="language-systemverilog">localparam int PLRU_BITS = 3;                 // 4-way → 3비트
typedef logic [PLRU_BITS-1:0] plru_t;</code></pre>
<h3 id="3비트-트리-구조">3비트 트리 구조</h3>
<pre><code>        [bit0]  ← root: 왼쪽{0,1} vs 오른쪽{2,3}
        /      \
   [bit1]    [bit2]
   0 vs 1    2 vs 3

bit0 = 0 → ways{0,1}이 더 오래됨 (victim 후보)
bit0 = 1 → ways{2,3}이 더 오래됨
bit1 → way 0 vs 1 선택
bit2 → way 2 vs 3 선택</code></pre><h3 id="victim-선택">victim 선택</h3>
<pre><code class="language-systemverilog">function automatic way_t plru_victim(input plru_t plru_in);
    return plru_in[0] ? (plru_in[2] ? way_t&#39;(3) : way_t&#39;(2))
                      : (plru_in[1] ? way_t&#39;(1) : way_t&#39;(0));
endfunction</code></pre>
<pre><code>루트부터 화살표 따라 내려가며 victim 결정:

plru[0]=0 → 왼쪽으로
  plru[1]=0 → way 0
  plru[1]=1 → way 1
plru[0]=1 → 오른쪽으로
  plru[2]=0 → way 2
  plru[2]=1 → way 3

way_t&#39;(3) = 3을 way_t 타입으로 캐스트</code></pre><h3 id="예시">예시</h3>
<pre><code>plru = 3&#39;b101 (bit0=1, bit1=0, bit2=1)

plru[0]=1 → 오른쪽
plru[2]=1 → way 3

→ victim = way 3</code></pre><h3 id="plru-갱신-접근-시">PLRU 갱신 (접근 시)</h3>
<pre><code class="language-systemverilog">function automatic plru_t plru_update(input plru_t plru_in, input way_t way_in);
    plru_t nxt = plru_in;
    nxt[0] = ~way_in[1];
    if (way_in[1] == 1&#39;b0) nxt[1] = ~way_in[0];
    else                   nxt[2] = ~way_in[0];
    return nxt;
endfunction</code></pre>
<pre><code>&quot;방금 접근한 way의 반대로 화살표를 돌림&quot;
(그 way를 victim에서 멀어지게 = 최근 사용 표시)

way_in을 이진수로 보면:
  way 0 = 00, way 1 = 01, way 2 = 10, way 3 = 11
  way_in[1] = 상위 비트 (왼쪽/오른쪽)
  way_in[0] = 하위 비트 (쌍 안에서)

nxt[0] = ~way_in[1]:
  way 0,1 접근(way_in[1]=0) → nxt[0]=1 (오른쪽 가리킴, 0/1 보호)
  way 2,3 접근(way_in[1]=1) → nxt[0]=0 (왼쪽 가리킴, 2/3 보호)

if 분기:
  왼쪽 쌍(0,1) 접근 → nxt[1] 갱신
  오른쪽 쌍(2,3) 접근 → nxt[2] 갱신
  → 해당 쌍 안에서도 접근한 것 반대로</code></pre><h3 id="예시로-따라가기">예시로 따라가기</h3>
<pre><code>초기 plru = 000, way 2에 접근

way 2 = 10 → way_in[1]=1, way_in[0]=0

nxt[0] = ~way_in[1] = ~1 = 0  (왼쪽 가리킴, way 2,3 보호)
way_in[1]=1이므로 else 분기:
  nxt[2] = ~way_in[0] = ~0 = 1  (way 3 가리킴, way 2 보호)

결과 plru = 3&#39;b100 (bit2=1, bit0=0)

이제 victim 찾으면:
  plru[0]=0 → 왼쪽 → plru[1]=0 → way 0
  → way 2를 방금 썼으니 way 0이 victim (오래된 것) ✅</code></pre><hr>
<h2 id="왜-이-파일이-중요한가">왜 이 파일이 중요한가</h2>
<pre><code>이 패키지 하나가 캐시 전체의 기반:

주소 분해   → 컨트롤러가 hit/miss 판정하는 근거
FSM 상태    → 컨트롤러 동작의 뼈대
PLRU 함수   → victim 선택 로직
헬퍼 함수   → 모든 파일이 공유하는 주소 계산

→ 여기가 정확해야 캐시 전체가 정확
→ &quot;단일 진실 공급원&quot;이라 버그가 여기 있으면 전체 영향</code></pre><hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<pre><code>l1_cache_pkg = 캐시의 설계도 상수 + 도구 모음

① geometry: 4-way, 64 set, 16B line = 4KB
② 주소 분해: [tag 22 | set 6 | word 2 | byte 2]
③ typedef: set_t, tag_t, way_t 등 (의미 있는 타입)
④ FSM: IDLE→WB_READ→WB_SEND→FILL_REQ→FILL_RCV→COMPLETE
⑤ tag 배열: 태그만 SRAM, valid/dirty는 플롭(리셋용)
⑥ 헬퍼: get_set/tag/word, +:/-: part-select
⑦ tree-PLRU: 3비트로 4-way victim 선택/갱신

핵심 디테일:
  - ST_COMPLETE 분리 → 구조적 충돌 해결 (combinational loop 맥락)
  - 긴 인자 이름 → XSim의 조용한 0 전달 버그 회피
  - valid/dirty 플롭 → 한 사이클 캐시 무효화</code></pre><hr>
<h2 id="다음-편-예고">다음 편 예고</h2>
<p>이 패키지의 정의들이 실제로 쓰이는 곳:</p>
<ul>
<li><strong>캐시 컨트롤러</strong> (FSM 구현, hit/miss 판정)</li>
<li><strong>데이터 배열 / 태그 배열</strong> (SRAM)</li>
<li><strong>UVM 검증 환경</strong> (golden model이 이 주소 분해를 독립 구현)</li>
</ul>
<p>특히 이력서의 <strong>combinational loop 버그</strong>는 컨트롤러의 stall 경로에서 나오는데, 그때 이 FSM과 주소 헬퍼가 어떻게 얽히는지가 핵심이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[면접 예상질문]]></title>
            <link>https://velog.io/@houston_guy2/%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81%EC%A7%88%EB%AC%B8</link>
            <guid>https://velog.io/@houston_guy2/%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81%EC%A7%88%EB%AC%B8</guid>
            <pubDate>Wed, 05 Aug 2026 04:13:59 GMT</pubDate>
            <description><![CDATA[<h1 id="axi4-uvm--interview-qa">AXI4 UVM — Interview Q&amp;A</h1>
<blockquote>
<p>DV Intern interview preparation
Speak these out loud until they feel natural. Keep answers ~30-60 seconds each.</p>
</blockquote>
<hr>
<h2 id="how-to-use-this">How to use this</h2>
<ul>
<li><strong>Bold</strong> = the one-sentence answer (lead with this)</li>
<li>Then 2-3 supporting sentences</li>
<li>⭐ = high priority, almost certain to be asked</li>
</ul>
<hr>
<h1 id="part-1-uvm-architecture">Part 1: UVM Architecture</h1>
<h2 id="q1-⭐-why-are-uvm-components-split-into-phases-what-is-the-order-and-direction-of-build--connect--run">Q1. ⭐ Why are UVM components split into phases? What is the order and direction of build / connect / run?</h2>
<p><strong>Phases give every component a synchronized, predictable lifecycle, so construction, connection, and execution never overlap.</strong></p>
<p><code>build_phase</code> runs <strong>top-down</strong> — the test builds the env, the env builds the agent, the agent builds the driver and monitor. This order matters because a parent must create and configure a child before the child builds itself. <code>connect_phase</code> runs <strong>bottom-up</strong> and wires the TLM ports together, for example connecting the driver&#39;s <code>seq_item_port</code> to the sequencer&#39;s export. <code>run_phase</code> is the only time-consuming phase, where the actual stimulus runs. Splitting these prevents a component from trying to connect to something that doesn&#39;t exist yet.</p>
<hr>
<h2 id="q2-⭐-why-do-we-need-config_db-what-happens-without-it">Q2. ⭐ Why do we need config_db? What happens without it?</h2>
<p><strong>config_db is how the testbench passes configuration and the virtual interface down the hierarchy without hard-coding dependencies.</strong></p>
<p>The classic use is the virtual interface: <code>tb_top</code> sets it into config_db, and the driver and monitor get it in their build phase. Without config_db, I&#39;d have to manually pass handles through every constructor, which breaks encapsulation and makes the environment rigid. It also lets me override behavior — like changing <code>max_outstanding</code> from a test — without touching the agent or driver code.</p>
<p>The key pattern is <strong>set</strong> at a higher level and <strong>get</strong> at a lower level, matched by a string key like <code>&quot;vif&quot;</code> or <code>&quot;cfg&quot;</code>.</p>
<hr>
<h2 id="q3-⭐-why-use-the-factory-whats-the-difference-between-new-and-type_idcreate">Q3. ⭐ Why use the factory? What&#39;s the difference between <code>new</code> and <code>type_id::create</code>?</h2>
<p><strong>The factory lets me swap component or object types at runtime without editing the code that instantiates them.</strong></p>
<p><code>new</code> constructs a fixed, hard-coded type — whatever class name I wrote. <code>type_id::create</code> asks the factory which type to build, so a test can override it, for example replacing the base sequence item with an error-injecting subclass. For sequence items and components I always use <code>create</code>; <code>new</code> would defeat the purpose of the factory. In my environment, the derived tests rely on this — they override <code>get_seq()</code> to return different sequence types, all through the factory.</p>
<hr>
<h2 id="q4-⭐-how-do-the-driver-and-sequencer-communicate">Q4. ⭐ How do the driver and sequencer communicate?</h2>
<p><strong>Through a TLM pull port — the driver pulls sequence items from the sequencer one at a time.</strong></p>
<p>The driver calls <code>get_next_item()</code>, which blocks until a sequence produces an item; it drives that item onto the pins, then calls <code>item_done()</code>. On the sequence side, <code>start_item</code> and <code>finish_item</code> are the matching handshake. In my pipelined driver, the twist is that I call <code>item_done()</code> immediately after queuing the burst, not after it finishes on the bus — that&#39;s what lets the sequence run ahead and create multiple outstanding transactions.</p>
<hr>
<h2 id="q5-why-is-the-monitor-separate-from-the-driver">Q5. Why is the monitor separate from the driver?</h2>
<p><strong>Because they answer different questions: the driver drives the protocol, the monitor passively observes what actually happened on the pins.</strong></p>
<p>If the monitor reused the driver&#39;s internal knowledge, it would check the testbench&#39;s intent against itself, not against the DUT. Keeping it separate means the monitor reconstructs transactions purely from pin activity, so it can catch cases where the DUT did something different from what the driver sent. It also makes the monitor reusable in passive mode, where there&#39;s no driver at all.</p>
<hr>
<h2 id="q6-why-is-the-analysis-port-1n">Q6. Why is the analysis port 1:N?</h2>
<p><strong>So one observed transaction can fan out to multiple subscribers that each analyze it differently.</strong></p>
<p>In my environment the monitor&#39;s analysis port connects to both the scoreboard and the coverage collector. The scoreboard asks &quot;was it correct?&quot; and coverage asks &quot;did we exercise it?&quot; — two independent questions on the same data. A 1:N port means I can add more subscribers, like a second coverage model or a logger, without changing the monitor at all.</p>
<hr>
<h2 id="q7-⭐-what-happens-if-you-dont-raise-an-objection">Q7. ⭐ What happens if you don&#39;t raise an objection?</h2>
<p><strong>The run phase ends immediately, so the simulation finishes before the stimulus even runs.</strong></p>
<p>Objections tell UVM &quot;I still have work to do, don&#39;t end the phase.&quot; I raise one before starting the sequence and drop it after draining. Without it, <code>run_phase</code> would return right away. This is also why draining matters in my pipelined driver — I have to keep the objection raised until the driver goes idle, otherwise the last bursts get cut off mid-flight and the scoreboard never checks them.</p>
<hr>
<h1 id="part-2-verification-methodology">Part 2: Verification Methodology</h1>
<h2 id="q8-⭐-how-does-the-scoreboard-know-the-correct-answer">Q8. ⭐ How does the scoreboard know the correct answer?</h2>
<p><strong>It doesn&#39;t know in advance — it records observed writes into an independent reference model, then predicts read data from that model.</strong></p>
<p>When the monitor reports a write burst, the scoreboard applies it to the reference model, storing the written bytes at spec-correct addresses. When a read comes back, the scoreboard asks the model what those addresses should contain and compares byte by byte. So the &quot;answer&quot; comes from the write values I actually observed, while the addressing rules come from the AXI spec — specifically the <code>beat_addr()</code> function that implements correct FIXED/INCR/WRAP addressing.</p>
<hr>
<h2 id="q9-⭐-why-build-the-reference-model-independently-of-the-dut">Q9. ⭐ Why build the reference model independently of the DUT?</h2>
<p><strong>If the model copies the DUT&#39;s implementation, it inherits the DUT&#39;s bugs, and both agree while both are wrong.</strong></p>
<p>I wrote the model from the AXI spec, not from the RTL. That&#39;s exactly why it caught the WRAP bug: the model wraps addresses correctly per spec, while the DUT incremented them linearly. Because the two derive the address differently, they disagreed, and the scoreboard flagged it. A model built by reading the DUT code would have made the same mistake and passed silently.</p>
<hr>
<h2 id="q10-⭐-does-100-functional-coverage-mean-verification-is-done">Q10. ⭐ Does 100% functional coverage mean verification is done?</h2>
<p><strong>No — coverage only means I exercised those scenarios, not that they were correct.</strong></p>
<p>Coverage answers &quot;did I stimulate this?&quot; while the scoreboard and assertions answer &quot;was it right?&quot; You can hit 100% coverage with a weak checker and still miss bugs. In fact I hit exactly this problem earlier in another project: my regression passed while an assertion was silently not firing — high coverage, blind checker. So coverage is necessary to show completeness, but it&#39;s meaningless without strong checking behind it.</p>
<hr>
<h2 id="q11-⭐-when-do-you-use-ignore_bins-whats-the-risk-of-overusing-it">Q11. ⭐ When do you use <code>ignore_bins</code>? What&#39;s the risk of overusing it?</h2>
<p><strong>I use it to exclude bins that are structurally unreachable, so they don&#39;t hold coverage below 100% forever.</strong></p>
<p>For example, on a 32-bit bus a transfer size above 4 bytes can never happen, and a single-beat WRAP burst is illegal by spec, so those bins are ignored. The rule I follow is that every ignore must be <strong>justified</strong> — I can explain why it&#39;s physically impossible. The risk of overuse is hiding a real hole: if I ignore a bin just because it&#39;s hard to hit, I&#39;m claiming coverage I don&#39;t actually have. That turns 100% into a lie.</p>
<hr>
<h2 id="q12-⭐-whats-the-difference-between-sva-and-the-scoreboard-why-didnt-sva-catch-the-wrap-bug">Q12. ⭐ What&#39;s the difference between SVA and the scoreboard? Why didn&#39;t SVA catch the WRAP bug?</h2>
<p><strong>SVA checks protocol rules cycle by cycle; the scoreboard checks data correctness per transaction. They operate at different levels.</strong></p>
<p>My assertions check things like VALID staying asserted until READY, payload stability, and WLAST landing on the right beat. The WRAP bug wasn&#39;t a protocol violation — every handshake was legal, WLAST was in the right place, the burst was well-formed. The bytes just landed at the wrong addresses. That&#39;s a data-correctness question, which only the scoreboard&#39;s byte-level comparison against the reference model could catch. So they&#39;re complementary: SVA guards the protocol, the scoreboard guards the data.</p>
<hr>
<h2 id="q13-can-bugs-remain-even-at-100-coverage">Q13. Can bugs remain even at 100% coverage?</h2>
<p><strong>Yes — coverage is only as complete as the coverage model I wrote.</strong></p>
<p>If I never defined a coverpoint or cross for some condition, hitting 100% says nothing about it. Coverage measures what I chose to measure. A scenario I didn&#39;t think to model is invisible to the coverage number. That&#39;s why coverage, scoreboard, and assertions are three separate axes — no single one is sufficient.</p>
<hr>
<h1 id="part-3-axi-protocol">Part 3: AXI Protocol</h1>
<h2 id="q14-⭐-what-are-the-handshake-rules-when-can-valid-be-deasserted">Q14. ⭐ What are the handshake rules? When can VALID be deasserted?</h2>
<p><strong>A transfer happens on any rising clock edge where both VALID and READY are high. VALID must stay asserted until that handshake completes.</strong></p>
<p>The key rules: the source may assert VALID without waiting for READY — waiting on both sides would deadlock. Once VALID is high, it cannot drop until READY is seen. READY, on the other hand, can change freely. So VALID can only be deasserted <em>after</em> the handshake, never before. My assertions enforce exactly this — VALID held until READY, and payload stable while stalled.</p>
<hr>
<h2 id="q15-why-does-the-4kb-boundary-rule-exist">Q15. Why does the 4KB boundary rule exist?</h2>
<p><strong>Because memory is managed in 4KB pages, and a single burst crossing a page could hit two regions with different properties or permissions.</strong></p>
<p>A page boundary might separate cached from uncached memory, or valid from unmapped addresses. If one burst spanned that line, the slave couldn&#39;t handle it coherently. So AXI forbids a single burst from crossing a 4KB boundary. In my sequence item, a constraint enforces that the start offset plus the total transfer size stays within the page.</p>
<hr>
<h2 id="q16-why-are-wrap-lengths-limited-to-2-4-8-or-16-beats">Q16. Why are WRAP lengths limited to 2, 4, 8, or 16 beats?</h2>
<p><strong>Because the wrap boundary is implemented by masking address bits, which only works cleanly when the window size is a power of two.</strong></p>
<p>WRAP is used for cache-line fills, where the critical word is fetched first and the address wraps within the aligned line. To wrap, the hardware computes <code>base = addr &amp; ~(total-1)</code>, and that masking is only valid if <code>total</code> is a power of two — which forces the beat count to 2, 4, 8, or 16. In my item, a constraint restricts WRAP bursts to lengths 1, 3, 7, 15, which encode those beat counts.</p>
<hr>
<h2 id="q17-in-a-narrow-transfer-how-is-wstrb-determined">Q17. In a narrow transfer, how is wstrb determined?</h2>
<p><strong>By the address and size — the active byte lanes are the ones the current beat&#39;s address maps to within the bus width.</strong></p>
<p>If the transfer is narrower than the bus, only some lanes are valid each beat, and which lanes shift as the address increments. In my item, <code>lane_mask()</code> computes this: it takes the beat address modulo the bus width to find the starting lane, then sets <code>size</code> bytes from there. Randomized strobes are masked down to these legal lanes — deasserting some is still legal, but driving lanes outside the addressed range is not.</p>
<hr>
<h2 id="q18-how-does-a-slave-support-outstanding-transactions">Q18. How does a slave support outstanding transactions?</h2>
<p><strong>By accepting a new address before the previous transaction&#39;s response is sent, usually with an internal queue that tracks in-flight requests by ID.</strong></p>
<p>A single-outstanding slave holds AWREADY low until it finishes the current burst. A multi-outstanding slave buffers several accepted addresses and processes them in order or by ID. In my environment, the v2 DUT has an outstanding depth of 4, and my driver proves it reached the DUT using peak-concurrency counters — the single-outstanding v1 can never push those above 1.</p>
<hr>
<h1 id="part-4-project-specific">Part 4: Project-Specific</h1>
<h2 id="q19-⭐-why-did-you-separate-aw-and-w-in-the-driver">Q19. ⭐ Why did you separate AW and W in the driver?</h2>
<p><strong>So the address and data channels run independently, which is what allows multiple bursts to overlap on the bus.</strong></p>
<p>If I drove AW then W then waited for B in a single loop, a second address could never be issued while the first burst&#39;s data was still going — no outstanding transactions, so the slave&#39;s outstanding logic never gets exercised. Running AW, W, and B as separate threads lets the address channel keep issuing while data and responses proceed in parallel. That&#39;s the whole point of a pipelined driver.</p>
<hr>
<h2 id="q20-⭐-in-a-pipelined-driver-how-do-you-stop-a-read-from-overtaking-a-write">Q20. ⭐ In a pipelined driver, how do you stop a read from overtaking a write?</h2>
<p><strong>I run writes and reads as separate passes with a drain in between, because the AXI read and write channels are independent.</strong></p>
<p>If I mixed them freely, a read could reach the slave before an earlier write to the same address had completed, and I&#39;d read stale data — a false failure. So in my outstanding test, phase one issues all the writes and drains until the driver is idle, then phase two issues the reads. That guarantees every write has landed before any read-back, preserving ordering across the independent channels.</p>
<hr>
<h2 id="q21-⭐-what-symptom-appears-if-the-monitor-cant-keep-up-with-outstanding-transactions">Q21. ⭐ What symptom appears if the monitor can&#39;t keep up with outstanding transactions?</h2>
<p><strong>The monitor silently drops bursts, and the scoreboard reports false mismatches or missing transactions that look like DUT bugs.</strong></p>
<p>This actually happened in my project — I saw 48 scoreboard failures and initially suspected the DUT. Root-causing it, the problem was the monitor: under overlapping traffic it lost track of a burst because it wasn&#39;t correlating address and data phases correctly. The fix was a channel-parallel monitor with FIFO-based correlation, one queue per phase, so it can reconstruct transactions no matter how many are in flight. The lesson was that a scoreboard failure isn&#39;t always a DUT bug — the environment can be wrong too.</p>
<hr>
<h2 id="q22-⭐-how-did-you-confirm-the-assertions-actually-work">Q22. ⭐ How did you confirm the assertions actually work?</h2>
<p><strong>With negative testing — I deliberately introduced a failing condition and confirmed the assertion fired.</strong></p>
<p>This mattered especially because of a simulator quirk: Vivado XSim silently ignores assertions with untyped property arguments or dynamic arrays, which makes the whole checker vacuous with no error at all. So &quot;no assertion failures&quot; could mean the checks are passing, or it could mean they&#39;re not running. I proved they were live by writing a self-test assertion that must fail and confirming it did. That&#39;s the same principle as fixing a blind pass criterion — I verify that the checker fails when it should, not just that it passes.</p>
<hr>
<h1 id="part-5-deeper-uvm-frequently-asked-follow-ups">Part 5: Deeper UVM (frequently asked follow-ups)</h1>
<h2 id="q23-⭐-what-is-a-virtual-interface-and-why-is-it-needed">Q23. ⭐ What is a virtual interface and why is it needed?</h2>
<p><strong>It&#39;s a handle that lets dynamic UVM class objects reach the static hardware pins, which they otherwise can&#39;t touch.</strong></p>
<p>UVM components are class objects created at runtime, but the DUT and interface are static hardware elaborated at time zero. A class can&#39;t directly connect to a physical signal. The virtual interface is a pointer to the actual interface instance, passed in through config_db, so the driver can drive <code>vif.master_cb.awvalid</code> and the monitor can sample it. Without it, there&#39;s no bridge between the object world and the pin world.</p>
<hr>
<h2 id="q24-⭐-whats-the-difference-between-a-sequence-and-a-sequence-item">Q24. ⭐ What&#39;s the difference between a sequence and a sequence item?</h2>
<p><strong>A sequence item is one transaction — the data; a sequence is the program that generates a stream of them — the behavior.</strong></p>
<p>The item, my <code>axi4_seq_item</code>, holds the address, length, size, burst type, and data for a single burst. The sequence&#39;s <code>body()</code> task decides how many items to create, with what constraints, and in what order — for example write-then-read-back, or a directed corner-case sweep. The same item is reused by every sequence; only the generation logic changes. That separation is why I can have one item class and a dozen sequences.</p>
<hr>
<h2 id="q25-what-is-the-difference-between-uvm_component-and-uvm_object">Q25. What is the difference between <code>uvm_component</code> and <code>uvm_object</code>?</h2>
<p><strong>Components are permanent and live in the hierarchy with phases; objects are transient data that flow through the components.</strong></p>
<p>Driver, monitor, scoreboard, and agent are components — they&#39;re built once, have a parent, and run through build/connect/run phases. Sequence items and the reference model are objects — they&#39;re created and discarded freely and have no phases. A quick test: if it has a fixed place in the testbench tree and a lifecycle, it&#39;s a component; if it&#39;s data being passed around or randomized, it&#39;s an object.</p>
<hr>
<h2 id="q26-⭐-walk-me-through-what-happens-from-run_test-to-the-first-pin-wiggle">Q26. ⭐ Walk me through what happens from run_test() to the first pin wiggle.</h2>
<p><strong>run_test builds the test, the test builds the environment top-down, ports connect bottom-up, then run_phase starts the sequence which feeds the driver.</strong></p>
<p><code>run_test</code> creates the test via the factory. The test&#39;s build phase creates the cfg, pulls the virtual interface from config_db, and builds the env. The env builds the agent, scoreboard, and coverage; the agent builds the sequencer, driver, and monitor. In connect phase, the driver&#39;s port binds to the sequencer&#39;s export and the monitor&#39;s analysis port binds to the scoreboard and coverage. Then run_phase raises an objection, starts the sequence, the sequence produces an item, the driver pulls it with <code>get_next_item</code>, and drives it onto <code>vif</code> — that&#39;s the first pin wiggle.</p>
<hr>
<h2 id="q27-how-does-uvm-know-when-to-end-the-simulation">Q27. How does UVM know when to end the simulation?</h2>
<p><strong>When all raised objections have been dropped, the run phase ends and UVM moves to the cleanup phases.</strong></p>
<p>Every component that has ongoing work raises an objection; the phase stays alive as long as any objection is outstanding. In my test, I raise one before the sequence and drop it after draining the pipeline. Once dropped and no one else is holding one, UVM proceeds to extract, check, and report phases, where the scoreboard and coverage print their final results.</p>
<hr>
<h2 id="q28-why-randomize-with-constraints-instead-of-just-writing-directed-tests">Q28. Why randomize with constraints instead of just writing directed tests?</h2>
<p><strong>Constrained-random explores combinations I wouldn&#39;t think to write by hand, while constraints keep every generated case legal.</strong></p>
<p>Directed tests only cover what I explicitly code, so I&#39;ll miss unexpected corners. Constrained-random generates legal-but-surprising combinations and, with enough seeds, fills coverage far faster. But randomness alone plateaus — in my project it stalled at 98.3%. So the real strategy is both: constrained-random for breadth, then directed sweeps to close the specific bins randomness kept missing. Constraints are what make the random stimulus legal in the first place.</p>
<hr>
<h1 id="part-6-debugging--pressure-questions">Part 6: Debugging &amp; Pressure Questions</h1>
<h2 id="q29-⭐-a-test-is-failing-walk-me-through-how-you-debug-it">Q29. ⭐ A test is failing. Walk me through how you debug it.</h2>
<p><strong>First I decide whether it&#39;s a DUT bug or an environment bug, because those lead to completely different fixes.</strong></p>
<p>I start from the scoreboard error — it tells me the beat, lane, address, expected value, and actual value. I check whether the reference model&#39;s prediction is right by hand for that one transaction; if the model is wrong, it&#39;s an environment bug. If the model is right, I look at the waveform at that exact address and time to see what the DUT actually drove. I also check whether the monitor reconstructed the transaction correctly, because I&#39;ve been burned before by the monitor dropping bursts and masquerading as a DUT bug. Only once I&#39;ve localized it do I fix — and I re-run to confirm the fix and confirm I didn&#39;t mask something else.</p>
<hr>
<h2 id="q30-⭐-how-do-you-know-your-testbench-itself-is-correct">Q30. ⭐ How do you know your testbench itself is correct?</h2>
<p><strong>I verify that it fails when it should — a checker that never fails is worthless, and a passing test proves nothing on its own.</strong></p>
<p>Two concrete ways I did this. First, negative testing on the assertions: I injected a deliberately failing self-test and confirmed it fired, because XSim silently ignores certain assertion forms and would otherwise pass vacuously. Second, running the regression against a known-buggy DUT revision — it must fail on v1 and pass on v2. If a testbench passes on buggy hardware, the testbench is broken. That&#39;s the same lesson as fixing a blind pass criterion: I check that failure is detectable, not just that success is reported.</p>
<hr>
<h2 id="q31-your-reference-model-and-the-dut-disagree-how-do-you-know-which-one-is-wrong">Q31. Your reference model and the DUT disagree. How do you know which one is wrong?</h2>
<p><strong>I work out the correct answer from the spec by hand for that single case, independent of both.</strong></p>
<p>The reference model and the DUT are both just implementations that could be wrong. So I go back to the AXI spec and manually compute what should be at that address for that burst. Whichever matches the spec is right. In the WRAP case, I traced the wrap window by hand — base address, window size, wrap point — and the reference model matched the spec while the DUT didn&#39;t. That confirmed it was a real DUT bug, not a modeling error.</p>
<hr>
<h2 id="q32-what-would-you-add-if-you-had-more-time-on-this-project">Q32. What would you add if you had more time on this project?</h2>
<p><strong>Error-response testing and out-of-order ID handling — two things the current DUT and environment don&#39;t exercise.</strong></p>
<p>The DUT hardwires responses to OKAY, so I ignore the error-response bins today; with a DUT that can signal SLVERR or DECERR, I&#39;d add stimulus and checks for those. I&#39;d also test out-of-order completion by ID, which a more capable slave supports and my current single-ID-per-burst approach doesn&#39;t stress. On the environment side, I&#39;d add random backpressure on the master&#39;s READY signals to exercise the slave&#39;s response-buffering under stall, which my always-ready driver doesn&#39;t cover today.</p>
<hr>
<h2 id="q33-how-would-this-environment-scale-to-multiple-masters-or-a-full-interconnect">Q33. How would this environment scale to multiple masters or a full interconnect?</h2>
<p><strong>I&#39;d instantiate multiple agents, make the scoreboard interconnect-aware, and add ID-based routing checks.</strong></p>
<p>Each master becomes its own active agent with its own sequencer and driver, all reusing the same item and interface. The scoreboard has to become aware of which master owns which transaction — typically by ID or by a per-master address map — so it predicts and checks per source. I&#39;d also add assertions for interconnect properties like ID uniqueness and response routing. The agent/env structure is built for this: adding an agent doesn&#39;t change the existing components.</p>
<hr>
<h2 id="q34-whats-the-hardest-part-of-verifying-outstanding-transactions-specifically">Q34. What&#39;s the hardest part of verifying outstanding transactions specifically?</h2>
<p><strong>Correlation — matching each response back to the right outstanding request when several are in flight at once.</strong></p>
<p>On the driving side it&#39;s easy to just keep issuing addresses. The hard part is the monitor and scoreboard: when responses can come back while multiple bursts are still open, the monitor has to correlate the address phase with the correct data and response phase. I solved it with one FIFO per phase, moving a transaction from queue to queue as each phase completes. If that correlation is even slightly wrong, transactions get mismatched and you get false failures that look exactly like DUT bugs.</p>
<hr>
<h1 id="rapid-fire-summary-one-liners-to-memorize">Rapid-fire summary (one-liners to memorize)</h1>
<pre><code>build_phase        → top-down (parent builds child)
connect_phase      → bottom-up (wire TLM ports)
config_db          → set high, get low, by string key
factory/create     → runtime type override
virtual interface  → bridges class objects to static pins
get_next_item      → driver pulls from sequencer
analysis port 1:N  → one transaction, many subscribers
objection          → keeps run_phase alive; all dropped → sim ends
component vs object → hierarchy+phases vs transient data
sequence vs item   → behavior (program) vs data (transaction)
reference model    → independent, spec-based (not DUT-based)
coverage 100%      → &quot;exercised&quot;, not &quot;correct&quot;
ignore_bins        → unreachable only, must justify
SVA                → protocol, cycle-level
scoreboard         → data, transaction-level
4KB boundary       → pages have different properties
WRAP 2/4/8/16      → power-of-two for bit masking
AW/W split         → enables outstanding
drain between W/R  → prevents read overtaking write
negative test      → proves assertions aren&#39;t vacuous
debug first step   → DUT bug or environment bug?
outstanding hard   → correlation, one FIFO per phase</code></pre><hr>
<h1 id="the-three-stories-that-tie-it-together">The three stories that tie it together</h1>
<p>If an interviewer asks an open-ended &quot;tell me about a hard bug,&quot; these three carry the whole interview:</p>
<p><strong>1. The WRAP bug (data correctness)</strong>
Independent reference model wrapped correctly, DUT incremented linearly. A symmetric WRAP-write / WRAP-read hid it because the DUT was consistently wrong on both sides. I designed an asymmetric WRAP-write / INCR-read cross-check that exposed 24 byte-level mismatches.</p>
<p><strong>2. The 48 false failures (environment vs DUT)</strong>
Scoreboard reported failures I first blamed on the DUT. Root cause was the monitor silently dropping bursts under outstanding traffic. Taught me a scoreboard failure can be an environment bug.</p>
<p><strong>3. The vacuous checker (verifying the verification)</strong>
XSim silently ignored assertions with untyped arguments — the checker looked like it passed but wasn&#39;t running. I proved liveness with a deliberately failing self-test. Same principle as fixing a blind regression pass criterion.</p>
<p>Common thread: <strong>I don&#39;t just find bugs — I understand why they stayed hidden, and I change the verification method to expose them.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (12) — SVA]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-12-SVA</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-12-SVA</guid>
            <pubDate>Thu, 30 Jul 2026 21:23:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: ... · sequences · coverage · <strong>SVA</strong></p>
</blockquote>
<hr>
<h2 id="sva란">SVA란</h2>
<pre><code>SVA = SystemVerilog Assertions
    = &quot;프로토콜 규칙을 매 사이클 자동으로 감시하는 감시자&quot;

스코어보드/커버리지가 &quot;완성된 버스트&quot;를 사후 검사한다면,
SVA는 &quot;매 클록마다&quot; 프로토콜 위반을 실시간 감시</code></pre><p>앞서 배운 VALID/READY 골든 룰을 <strong>코드로 자동 감시</strong>하는 게 이 파일이다. 이력서의 <strong>&quot;~25 bind-based SVA protocol assertions&quot;</strong>의 실제 구현.</p>
<hr>
<h2 id="스코어보드커버리지와-다른-위치">스코어보드/커버리지와 다른 위치</h2>
<pre><code>스코어보드: &quot;완성된 버스트가 맞나?&quot; (트랜잭션 단위, 사후)
커버리지:   &quot;이 조합을 봤나?&quot; (트랜잭션 단위, 사후)
SVA:        &quot;지금 이 클록에 프로토콜 위반했나?&quot; (사이클 단위, 실시간)

예: VALID를 올렸다가 READY 전에 내리면
  → 스코어보드는 모름 (데이터만 봄)
  → SVA는 그 순간 즉시 에러! ✅</code></pre><pre><code class="language-mermaid">flowchart LR
    BUS[&quot;AXI 버스&lt;br/&gt;매 클록 신호&quot;]
    BUS --&gt;|매 사이클 감시| SVA[&quot;SVA&lt;br/&gt;프로토콜 위반?&lt;br/&gt;(실시간)&quot;]
    BUS --&gt;|완성된 버스트| SB[&quot;Scoreboard&lt;br/&gt;데이터 맞나?&lt;br/&gt;(사후)&quot;]
    BUS --&gt;|완성된 버스트| COV[&quot;Coverage&lt;br/&gt;조합 봤나?&lt;br/&gt;(사후)&quot;]</code></pre>
<hr>
<h2 id="bind--rtl을-안-건드리고-주입">bind — RTL을 안 건드리고 주입</h2>
<pre><code>// Written as a standalone module and `bind`-ed into tb_top, so the
// checks are injected without touching the interface, the testbench
// or the RTL.</code></pre><p>이게 SVA의 핵심 설계 패턴이다.</p>
<pre><code>bind = 별도 모듈을 기존 모듈에 &quot;붙이는&quot; 문법

axi4_sva를 독립 모듈로 작성
→ bind로 tb_top에 주입
→ 인터페이스/테스트벤치/RTL 코드를 하나도 안 건드림!

장점 (실무 표준):
  프로토콜 체커가 설계 밖에 존재
  → 자유롭게 붙였다 뗐다 가능
  → +define+AXI4_NO_SVA로 통째로 제거 가능</code></pre><h3 id="왜-tb_top에-bind하나-interface가-아니라">왜 tb_top에 bind하나 (interface가 아니라)</h3>
<pre><code>// a module may not be instantiated inside an interface
// (XSim rejects that with VRFC 10-3535). Binding at tb_top gives one
// checker instance that works with either DUT.

interface 안에는 module을 못 넣음 (XSim 제약)
→ tb_top에 bind
→ 두 DUT(v1/v2) 다 같은 인터페이스에 연결되므로
  체커 하나가 양쪽 다 커버</code></pre><pre><code class="language-systemverilog">bind tb_top axi4_sva u_axi4_sva (
    .clk     (clk),
    .awid    (axi.awid),
    ...
);</code></pre>
<pre><code>&quot;tb_top 안에 axi4_sva를 u_axi4_sva라는 이름으로 붙여라&quot;
신호는 tb_top의 axi 인터페이스 신호에 연결</code></pre><hr>
<h2 id="⚠️-xsim-제약--이-파일의-숨은-고생">⚠️ XSim 제약 — 이 파일의 숨은 고생</h2>
<p>주석 곳곳에 XSim(Vivado 시뮬레이터)의 함정이 기록돼있다. 이게 이력서의 <strong>&quot;validated assertion liveness via negative testing&quot;</strong>의 배경이다.</p>
<h3 id="함정-1--untyped-property-argument가-조용히-무시됨">함정 1 — untyped property argument가 조용히 무시됨</h3>
<pre><code>// properties with *untyped* formal arguments are silently ignored by
// Vivado XSim — &quot;untyped port &quot;valid&quot; found ... It will be ignored.&quot;
// which makes the whole checker vacuous without any error.</code></pre><pre><code>문제:
  파라미터화된 property를 쓰면
  XSim이 조용히 무시함 (에러도 없이!)
  → 체커가 아무것도 안 하는데 &quot;통과&quot;로 보임 (vacuous = 공허한)

이게 앞서 배운 blind pass criterion과 같은 위험!
  → 체커가 눈멀어있는데 아무도 모름

해결:
  파라미터 property 대신 매크로로 인라인 확장
  → 각 assertion을 직접 펼침</code></pre><h3 id="함정-2--dynamic-array가-assertion에서-무시됨">함정 2 — dynamic array가 assertion에서 무시됨</h3>
<pre><code>// XSim cannot evaluate dynamic arrays/queues inside a concurrent
// assertion (&quot;Dynamic array inside concurrent assertion. It will be
// ignored.&quot; — silently vacuous!). So the outstanding-address tracker is
// a STATIC circular buffer.</code></pre><pre><code>문제:
  outstanding 추적에 queue를 쓰고 싶은데
  XSim이 assertion 안의 dynamic array를 무시 → 또 vacuous

해결:
  static circular buffer(고정 크기 원형 버퍼)로 구현
  → assertion은 거기서 뽑은 scalar만 참조</code></pre><h3 id="그래서-negative-testing으로-검증">그래서 negative testing으로 검증</h3>
<pre><code>// the checker was validated with a deliberately failing self-test assertion.

체커가 &quot;진짜 동작하는지&quot; 확인하려고
일부러 실패하는 assertion을 넣어봄
→ 정말 FAIL 나는지 확인
→ &quot;vacuous가 아니라 실제로 감시 중&quot;임을 증명

= assertion liveness 검증 (체커를 검증하는 것)</code></pre><hr>
<h2 id="매크로--인라인-확장">매크로 — 인라인 확장</h2>
<pre><code class="language-systemverilog">`define AXI_VALID_HELD(nm, vld, rdy)                                     \
    nm: assert property (@(posedge clk) disable iff (rst)                 \
        ((vld) &amp;&amp; !(rdy)) |=&gt; (vld))                                      \
        else $error(&quot;AXI-SVA: %s - VALID de-asserted before READY&quot;, `&quot;nm`&quot;);

`define AXI_STABLE(nm, vld, rdy, sig)                                    \
    nm: assert property (@(posedge clk) disable iff (rst)                 \
        ((vld) &amp;&amp; !(rdy)) |=&gt; $stable(sig))                               \
        else $error(&quot;AXI-SVA: %s - payload changed while stalled&quot;, `&quot;nm`&quot;);</code></pre>
<pre><code>매크로 = property를 파라미터화하는 대신 텍스트로 펼치기
  (XSim의 untyped argument 함정 회피)

nm  = assertion 이름
vld = VALID 신호
rdy = READY 신호
sig = 안정성 체크할 payload

`&quot;nm`&quot; = 매크로 인자를 문자열로 (에러 메시지용)</code></pre><hr>
<h2 id="assertion-종류별-정리">assertion 종류별 정리</h2>
<h3 id="1-valid-유지-골든-룰-2">1. VALID 유지 (골든 룰 2)</h3>
<pre><code class="language-systemverilog">`AXI_VALID_HELD(a_awvalid_held, awvalid, awready)
`AXI_VALID_HELD(a_wvalid_held,  wvalid,  wready)
`AXI_VALID_HELD(a_bvalid_held,  bvalid,  bready)
`AXI_VALID_HELD(a_arvalid_held, arvalid, arready)
`AXI_VALID_HELD(a_rvalid_held,  rvalid,  rready)</code></pre>
<pre><code>규칙: VALID를 올렸으면 READY 올 때까지 못 내림

((vld) &amp;&amp; !(rdy)) |=&gt; (vld)
  = &quot;VALID인데 READY 아직 안 옴&quot; → &quot;다음 사이클에도 VALID&quot;

|=&gt; = &quot;다음 클록에&quot; (overlapping이 아닌 next-cycle implication)

5개 채널 전부 감시 → 앞서 배운 골든 룰 2의 자동화</code></pre><h3 id="2-payload-안정-골든-룰">2. Payload 안정 (골든 룰)</h3>
<pre><code class="language-systemverilog">`AXI_STABLE(a_awaddr_stable,  awvalid, awready, awaddr)
`AXI_STABLE(a_awlen_stable,   awvalid, awready, awlen)
...
`AXI_STABLE(a_wdata_stable,   wvalid,  wready,  wdata)
...</code></pre>
<pre><code>규칙: 전송이 stall된 동안 payload가 바뀌면 안 됨

((vld) &amp;&amp; !(rdy)) |=&gt; $stable(sig)
  = &quot;VALID인데 READY 안 옴&quot; → &quot;다음 사이클에 sig가 그대로&quot;

$stable(sig) = 이전 값과 같은지 (안 바뀌었는지)

→ VALID 올려놓고 주소/데이터를 바꾸는 위반 감지
  awaddr, awlen, awsize, awburst, awid,
  wdata, wstrb, wlast, araddr..., bresp, rresp, rlast 전부</code></pre><h3 id="3-예약불법-인코딩">3. 예약/불법 인코딩</h3>
<pre><code class="language-systemverilog">a_awburst_legal: assert property (@(posedge clk) disable iff (rst)
    awvalid |-&gt; awburst != 2&#39;b11)
    else $error(&quot;AXI-SVA: AWBURST uses the reserved encoding 2&#39;b11&quot;);

a_awsize_fits: assert property (@(posedge clk) disable iff (rst)
    awvalid |-&gt; ((1 &lt;&lt; awsize) &lt;= STRB_WIDTH))
    else $error(&quot;AXI-SVA: AWSIZE exceeds the data bus width&quot;);</code></pre>
<pre><code>burst 2&#39;b11 = 예약값 (사용 금지) → 쓰면 위반
size가 버스 폭 초과 → 위반 (32비트 버스에 8B 전송 불가)

|-&gt; = &quot;즉시&quot; implication (같은 사이클)
disable iff (rst) = 리셋 중엔 검사 안 함</code></pre><h3 id="4-xz-감지">4. X/Z 감지</h3>
<pre><code class="language-systemverilog">a_aw_known: assert property (@(posedge clk) disable iff (rst)
    awvalid |-&gt; !$isunknown({awaddr, awlen, awsize, awburst, awid}))
    else $error(&quot;AXI-SVA: X/Z on the AW channel while AWVALID&quot;);</code></pre>
<pre><code>VALID인데 payload에 X/Z가 있으면 위반

$isunknown(sig) = sig에 X나 Z가 있나?
!$isunknown(...) = X/Z가 없어야 함

{a, b, c} = 신호들을 이어붙여서 한 번에 검사

→ 앞서 배운 &quot;X 전파&quot; 문제를 프로토콜 레벨에서 감지
  (VALID일 때는 값이 확정돼야 함)</code></pre><h3 id="5-리셋-중-응답-금지">5. 리셋 중 응답 금지</h3>
<pre><code class="language-systemverilog">a_no_resp_in_reset: assert property (@(posedge clk)
    rst |-&gt; (!bvalid &amp;&amp; !rvalid))
    else $error(&quot;AXI-SVA: BVALID/RVALID asserted during reset&quot;);</code></pre>
<pre><code>리셋 중엔 BVALID/RVALID가 뜨면 안 됨

rst |-&gt; (!bvalid &amp;&amp; !rvalid)
  = &quot;리셋이면&quot; → &quot;bvalid도 rvalid도 0&quot;

이건 disable iff (rst)가 없음! (리셋 중을 검사하는 게 목적이니까)</code></pre><p>이게 이력서의 <strong>&quot;cycle-accurate SVA catching reset-phase violations&quot;</strong>와 연결되는 부분이에요.</p>
<h3 id="6-wlastrlast-위치-가장-복잡">6. WLAST/RLAST 위치 (가장 복잡)</h3>
<p>버스트 beat 개수와 LAST 위치가 맞는지 검사. outstanding까지 지원하려고 tracker를 쓴다.</p>
<pre><code class="language-systemverilog">localparam int SVA_QDEPTH = 16;
int unsigned aw_len_mem [SVA_QDEPTH];   // static 배열 (dynamic 금지)
int unsigned aw_wr, aw_rd, ar_wr, ar_rd;
int unsigned w_beats, r_beats;

wire aw_pending = (aw_wr != aw_rd);     // 대기 중인 AW 있나
wire [31:0] aw_head = aw_len_mem[aw_rd]; // 가장 오래된 AWLEN</code></pre>
<pre><code>static circular buffer로 outstanding 주소 추적:
  AW 수락되면 → awlen을 버퍼에 저장 (aw_wr++)
  WLAST 오면 → 버퍼에서 제거 (aw_rd++)

w_beats = 현재 몇 번째 W beat인가 카운트
aw_head = 지금 처리 중인 버스트의 예상 길이(AWLEN)</code></pre><pre><code class="language-systemverilog">always @(posedge clk) begin
    if (rst) begin ... 리셋 ... end
    else begin
        if (awvalid &amp;&amp; awready) begin       // AW 수락
            aw_len_mem[aw_wr] &lt;= awlen;
            aw_wr &lt;= (aw_wr + 1) % SVA_QDEPTH;
        end
        if (wvalid &amp;&amp; wready) begin          // W beat
            if (wlast) begin
                w_beats &lt;= 0;
                if (aw_wr != aw_rd) aw_rd &lt;= (aw_rd + 1) % SVA_QDEPTH;
            end
            else w_beats &lt;= w_beats + 1;
        end
        ...
    end
end</code></pre>
<pre><code>% SVA_QDEPTH = 원형 버퍼 (16 넘으면 0으로 되돌아감)

WLAST 위치 검사:</code></pre><pre><code class="language-systemverilog">a_wlast_position: assert property (@(posedge clk) disable iff (rst)
    (wvalid &amp;&amp; wready &amp;&amp; wlast &amp;&amp; aw_pending) |-&gt; (w_beats == aw_head))
    else $error(&quot;AXI-SVA: WLAST on beat %0d but AWLEN implies beat %0d&quot;,
                w_beats, aw_head);</code></pre>
<pre><code>&quot;WLAST가 떴을 때 → 현재 beat 번호가 AWLEN과 같아야&quot;

w_beats == aw_head
  = &quot;지금까지 센 beat 수&quot; == &quot;예상 길이&quot;
  → WLAST가 너무 일찍/늦게 뜨면 위반!

overrun 검사도 있음:
a_w_no_overrun: !wlast일 때 w_beats &lt; aw_head
  = &quot;아직 마지막 아닌데 예상 길이 넘으면&quot; 위반</code></pre><hr>
<h2 id="전체-어서션-지도">전체 어서션 지도</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/9d307505-2806-44eb-aa7c-981c902993c9/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>어서션 그룹</th>
<th>감시 내용</th>
<th>개수</th>
</tr>
</thead>
<tbody><tr>
<td>VALID 유지</td>
<td>READY 전 VALID 못 내림</td>
<td>5</td>
</tr>
<tr>
<td>Payload 안정</td>
<td>stall 중 값 고정</td>
<td>16</td>
</tr>
<tr>
<td>불법 인코딩</td>
<td>예약값/크기 초과</td>
<td>4</td>
</tr>
<tr>
<td>X/Z 감지</td>
<td>VALID 시 값 확정</td>
<td>3</td>
</tr>
<tr>
<td>리셋 응답 금지</td>
<td>리셋 중 응답 없음</td>
<td>1</td>
</tr>
<tr>
<td>LAST 위치/overrun</td>
<td>beat 개수 일치</td>
<td>4</td>
</tr>
</tbody></table>
<p>합계 <del>33개 (이력서의 &quot;</del>25&quot;는 대략치)</p>
<hr>
<h2 id="defineaxi4_no_sva--통째로-끄기">+define+AXI4_NO_SVA — 통째로 끄기</h2>
<pre><code class="language-systemverilog">`ifndef AXI4_NO_SVA
    // ... 모든 assertion ...
`endif</code></pre>
<pre><code>+define+AXI4_NO_SVA로 컴파일하면
→ 모든 assertion이 사라짐
→ 체커 없이 순수 시뮬레이션

용도: 디버깅 시 체커 노이즈 제거, 성능 측정 등
bind 패턴 덕에 이런 on/off가 자유로움</code></pre><hr>
<h2 id="sva-vs-scoreboard-vs-coverage--검증의-3축">SVA vs Scoreboard vs Coverage — 검증의 3축</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/f3a997de-d69b-40be-bf27-01dc230ef80c/image.png" alt=""></p>
<table>
<thead>
<tr>
<th></th>
<th>SVA</th>
<th>Scoreboard</th>
<th>Coverage</th>
</tr>
</thead>
<tbody><tr>
<td><strong>질문</strong></td>
<td>규칙 지켰나?</td>
<td>맞았나?</td>
<td>봤나?</td>
</tr>
<tr>
<td><strong>단위</strong></td>
<td>사이클</td>
<td>트랜잭션</td>
<td>트랜잭션</td>
</tr>
<tr>
<td><strong>시점</strong></td>
<td>실시간</td>
<td>사후</td>
<td>사후</td>
</tr>
<tr>
<td><strong>감시 대상</strong></td>
<td>VALID/READY, 인코딩</td>
<td>데이터 값</td>
<td>시나리오 조합</td>
</tr>
<tr>
<td><strong>위반 시</strong></td>
<td>즉시 $error</td>
<td>mismatch</td>
<td>낮은 %</td>
</tr>
</tbody></table>
<pre><code>세 개가 상호보완:
  SVA        → 프로토콜을 어겼는지 (핸드셰이크, 타이밍)
  Scoreboard → 데이터가 틀렸는지 (값)
  Coverage   → 충분히 테스트했는지 (범위)

→ 셋 다 있어야 &quot;규칙 지키고, 정확하고, 빠짐없이&quot; 검증</code></pre><hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<pre><code>SVA = 프로토콜 규칙을 매 사이클 실시간 감시하는 어서션 체커

bind 패턴:
  독립 모듈로 작성 → tb_top에 주입 → RTL/TB 안 건드림
  +define+AXI4_NO_SVA로 통째로 on/off

감시 항목 (~25+):
  VALID 유지, payload 안정, 불법 인코딩,
  X/Z 감지, 리셋 중 응답 금지, WLAST/RLAST 위치

XSim 함정 (실무 교훈):
  untyped property, dynamic array가 조용히 무시됨 (vacuous)
  → 매크로 인라인 + static buffer로 회피
  → negative testing으로 &quot;진짜 감시 중&quot;임을 증명

검증 3축:
  SVA(규칙) + Scoreboard(정확성) + Coverage(완결성)</code></pre><hr>
<h2 id="시리즈-완결--검증의-3축-완성">시리즈 완결 — 검증의 3축 완성</h2>
<p>이제 AXI4 UVM 환경의 <strong>정말 모든 파일</strong>을 다뤘습니다.</p>
<pre><code>                        tb_top
                            │
                    test → sequence → seq_item
                            │
              cfg/agent/env ─ driver ─ interface ─ DUT
                            │
                        monitor
              ┌─────────────┼─────────────┐
        scoreboard      coverage        SVA
        (맞았나?)       (봤나?)     (규칙 지켰나?)
              │             │             │
          ref_model    covergroup    assertions
              └─────────────┼─────────────┘
                     검증의 3축 완성</code></pre><p>검증의 세 가지 질문에 모두 답합니다:</p>
<ul>
<li><strong>정확성</strong>: scoreboard가 데이터를 ref_model과 비교 → &quot;맞았나?&quot;</li>
<li><strong>완결성</strong>: coverage가 시나리오를 추적 → &quot;봤나?&quot;</li>
<li><strong>준수성</strong>: SVA가 프로토콜을 실시간 감시 → &quot;규칙 지켰나?&quot;</li>
</ul>
<p>이 셋이 만나 &quot;규칙을 지키며, 정확하고, 빠짐없이 검증했다&quot;는 완전한 신뢰를 만듭니다. 이것으로 AXI4-Full UVM 검증 환경 시리즈를 마칩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (11) — coverage ]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-11-coverage</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-11-coverage</guid>
            <pubDate>Thu, 30 Jul 2026 21:18:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: ... · base_test · sequences · <strong>coverage</strong></p>
</blockquote>
<hr>
<h2 id="coverage란">Coverage란</h2>
<pre><code>&quot;우리가 실제로 무엇을 검증했나?&quot;를 추적하는 컴포넌트

스코어보드와는 다른 질문을 던짐:
  스코어보드: &quot;맞았나?&quot; (correctness)
  커버리지:   &quot;이 시나리오를 실행했나?&quot; (completeness)</code></pre><p>env에서 monitor의 analysis port에 스코어보드와 <strong>함께</strong> 연결됐던 그 커버리지다. 같은 트랜잭션을 받아서 &quot;어떤 조합을 봤는지&quot; 체크리스트를 채운다.</p>
<ul>
<li>Code Coverage: which lines/branches/states executed (what the RTL DID)</li>
<li>Functional Coverage: which spec requirements were exercised (What the RTL was supposed to do)</li>
<li>Assertion Coverage: which assertions actually fired</li>
</ul>
<hr>
<h2 id="왜-커버리지가-필요한가">왜 커버리지가 필요한가</h2>
<pre><code>스코어보드만 있으면:
  &quot;실행한 것&quot;은 다 맞는지 확인함
  근데 &quot;무엇을 안 실행했는지&quot;는 모름!

예:
  WRAP 버스트를 한 번도 안 보냈으면?
  → 스코어보드는 &quot;통과&quot;만 보고함 (WRAP 관련 검증 0)
  → &quot;우리가 WRAP을 검증했나?&quot; 답을 못 함

커버리지:
  &quot;WRAP 봤음/안 봤음&quot;을 체크리스트로 추적
  → 안 본 시나리오(구멍)를 드러냄
  → 검증의 완결성(completeness) 측정</code></pre><p>이게 이력서의 <strong>&quot;100% functional coverage&quot;</strong>가 측정하는 것이다.</p>
<hr>
<h2 id="클래스-선언--uvm_subscriber">클래스 선언 — uvm_subscriber</h2>
<pre><code class="language-c">class axi4_coverage extends uvm_subscriber #(axi4_seq_item);
    `uvm_component_utils(axi4_coverage)

    axi4_seq_item   tr;
    axi_strb_kind_e strb_kind;
    int unsigned    n_sampled;</code></pre>
<h3 id="uvm_subscriber--analysis-port-받는-전용-베이스">uvm_subscriber — analysis port 받는 전용 베이스</h3>
<pre><code class="language-c">uvm_subscriber #(T):
  → 내장 analysis_export를 가짐
  → write() 함수만 구현하면 트랜잭션을 받음

스코어보드는 uvm_analysis_imp를 수동 선언했는데
커버리지는 uvm_subscriber라 analysis_export가 이미 내장

env의 연결:
  agent.mon.ap.connect(cov.analysis_export);  ← 이 export가 내장된 것</code></pre>
<pre><code>uvm_subscriber = &quot;analysis port 하나만 받으면 되는 컴포넌트&quot;용 축약형
  → write() 하나만 구현하면 끝</code></pre><hr>
<h2 id="strb_kind-enum--파생-값">strb_kind enum — 파생 값</h2>
<pre><code class="language-c">typedef enum { STRB_NONE, STRB_PARTIAL, STRB_FULL } axi_strb_kind_e;</code></pre>
<pre><code>write strobe의 &quot;전체 모양&quot;을 3가지로 분류:
  STRB_NONE    : 아무 lane도 안 씀
  STRB_PARTIAL : 일부만 씀
  STRB_FULL    : 모든 유효 lane을 다 씀

→ covergroup이 이 분류를 커버 (strobe 다양성 측정)</code></pre><hr>
<h2 id="span-stylecolorlightgreencovergroup--커버리지-정의-핵심span"><span style="color:Lightgreen">covergroup — 커버리지 정의 (핵심)</span></h2>
<pre><code class="language-c">covergroup axi_cg;
    option.per_instance = 1;
    option.name         = &quot;axi4_functional_coverage&quot;;
    ...
endgroup</code></pre>
<pre><code class="language-c">covergroup = &quot;무엇을 추적할지&quot; 정의하는 묶음
  coverpoint = 개별 추적 항목
  cross      = 항목들의 조합 추적
  bins       = 각 항목의 세부 카테고리

option.per_instance = 1 → 인스턴스별로 커버리지 따로 집계</code></pre>
<p>Where should covergroups be sampled? =&gt; Monitor</p>
<h2 id="coverpoint-하나씩">coverpoint 하나씩</h2>
<h3 id="cp_dir--방향">cp_dir — 방향</h3>
<pre><code class="language-c">cp_dir: coverpoint tr.dir {
    bins rd = {AXI_READ};
    bins wr = {AXI_WRITE};
}</code></pre>
<pre><code>읽기와 쓰기를 각각 봤나?
  bins rd → 읽기 트랜잭션이 오면 체크
  bins wr → 쓰기 트랜잭션이 오면 체크
둘 다 체크돼야 100%</code></pre><h3 id="cp_burst--버스트-타입">cp_burst — 버스트 타입</h3>
<pre><code class="language-c">cp_burst: coverpoint tr.burst {
    bins fixed = {AXI_FIXED};
    bins incr  = {AXI_INCR};
    bins wrap  = {AXI_WRAP};
}</code></pre>
<pre><code>FIXED/INCR/WRAP 각각 봤나?
→ WRAP bin은 wrap_seq/cov_seq에서만 채워짐 (일반 시퀀스는 WRAP 제외)</code></pre><h3 id="cp_size--전송-크기-span-stylecolororangeignore_bins-등장span">cp_size — 전송 크기 <span style="color:orange">(ignore_bins 등장)</span></h3>
<pre><code class="language-c">cp_size: coverpoint tr.size {
    bins bytes1 = {0};
    bins bytes2 = {1};
    bins bytes4 = {2};
    ignore_bins too_wide = {[3:7]};   // impossible on a 32-bit bus
}</code></pre>
<pre><code>size 0(1B)/1(2B)/2(4B) 각각 봤나?

ignore_bins too_wide = {[3:7]}
  → size 3~7은 32비트 버스에서 불가능 (8B 이상)
  → &quot;일부러 무시&quot; → 커버리지 계산에서 제외
  → 안 그러면 영원히 100% 못 채움 (도달 불가능한데 카운트되니까)</code></pre><p>이게 <span style="color:orange"><strong>ignore_bins의 핵심 용도</strong>예요.</span></p>
<p>ignore_bins = &quot;구조적으로 도달 불가능한 것을 명시적으로 제외&quot;
  → 커버리지 구멍이 아니라 &quot;일부러 뺀 것&quot;임을 문서화</p>
<h3 id="cp_len--버스트-길이">cp_len — 버스트 길이</h3>
<pre><code class="language-c">cp_len: coverpoint tr.len {
    bins single   = {0};            // 1 beat
    bins len_2_4  = {[1:3]};
    bins len_5_8  = {[4:7]};
    bins len_9_16 = {[8:15]};
    ignore_bins beyond = {[16:255]}; // stimulus is constrained to &lt;=16 beats
}</code></pre>
<pre><code>길이를 클래스로 묶어서 추적:
  single / 2-4 / 5-8 / 9-16 beats

ignore_bins beyond = {[16:255]}
  → 자극이 16 beats 이하로 제약됨 → 그 위는 안 나옴 → 무시

범위 bins:
  {[1:3]} = 1,2,3 중 아무거나 오면 이 bin 체크</code></pre><h3 id="cp_resp--응답-문서화된-dut-한계">cp_resp — 응답 (문서화된 DUT 한계)</h3>
<pre><code class="language-systemverilog">cp_resp: coverpoint tr.resp {
    bins okay = {AXI_OKAY};
    ignore_bins unreachable = {AXI_EXOKAY, AXI_SLVERR, AXI_DECERR};
}</code></pre>
<pre><code>주석: &quot;The DUT hardwires bresp/rresp to OKAY, so error responses are
       unreachable by construction.&quot;

DUT가 항상 OKAY만 응답 → 에러 응답은 절대 안 나옴
→ ignore_bins으로 제외 (DUT 한계를 문서화)
→ &quot;커버리지 구멍이 아니라 의도된 것&quot;</code></pre><h3 id="cp_region--주소-영역">cp_region — 주소 영역</h3>
<pre><code class="language-systemverilog">cp_region: coverpoint tr.addr[15:12] {
    bins region[16] = {[0:15]};
}</code></pre>
<pre><code>addr[15:12] = 상위 4비트 = 4KB 영역 구분 (64KB / 4KB = 16개)

bins region[16] = {[0:15]}
  → 16개 bin 자동 생성 (region[0]~region[15])
  → 각 4KB 영역에 접근했나 추적

[15:12] 뽑는 이유:
  64KB 공간을 4KB로 나누면 상위 4비트가 영역 번호
  0x0xxx→0, 0x1xxx→1, ..., 0xFxxx→15</code></pre><h3 id="cp_strb--strobe-모양-조건부-커버">cp_strb — strobe 모양 (조건부 커버)</h3>
<pre><code class="language-systemverilog">cp_strb: coverpoint strb_kind iff (tr.dir == AXI_WRITE) {
    bins none    = {STRB_NONE};
    bins partial = {STRB_PARTIAL};
    bins full    = {STRB_FULL};
}</code></pre>
<pre><code>iff (tr.dir == AXI_WRITE)
  = &quot;쓰기일 때만 이 coverpoint를 샘플&quot;
  → 읽기는 strobe가 의미 없으니 (읽기는 전부 1로 채움) 제외

none/partial/full 각각 봤나 → strobe 다양성 측정</code></pre><pre><code>iff = 조건부 커버리지 (이 조건일 때만 카운트)</code></pre><h2 id="span-stylecolorlightgreencross-coveragespan"><span style="color:lightgreen">Cross Coverage</span></h2>
<pre><code class="language-c">x_burst_size : cross cp_burst, cp_size;

x_burst_len  : cross cp_burst, cp_len {
    ignore_bins wrap_single = binsof(cp_burst.wrap) &amp;&amp; binsof(cp_len.single);
}

x_dir_burst  : cross cp_dir,   cp_burst;</code></pre>
<p><strong>cross = 두 coverpoint의 &quot;조합&quot;을 추적</strong></p>
<ul>
<li>x_burst_size: burst 3종 × size 3종 = 9개 조합 다 봤나?</li>
<li>x_burst_len:  burst × length 조합</li>
<li>x_dir_burst:  방향 × burst 조합</li>
</ul>
<p>→ 개별로는 다 봤어도 &quot;조합&quot;으로는 안 봤을 수 있음
  예: INCR도 봤고 size1도 봤지만 &quot;INCR+size1&quot;은 안 봤을 수 있음
  → cross가 이걸 잡음</p>
<p><strong>Cross coverage tracks combinations of coverpoints. If coverpoint A has 4bins and coverpoint B has 3bins, the cross has 4x3=12bins</strong></p>
<p><span style="color:orange"><strong>State space explosion</strong></span>: Crossing a 32 bit address(4B bins) with a 32 bit data(4B bins) creates 1.8x10^19 bins -&gt; simulator crashes. 
-&gt; alwasy reduce coverpoints to rchitecturally siginificant bins before crossing.</p>
<h3 id="cross의-ignore_bins">cross의 ignore_bins</h3>
<pre><code class="language-systemverilog">ignore_bins wrap_single = binsof(cp_burst.wrap) &amp;&amp; binsof(cp_len.single);</code></pre>
<pre><code>주석: &quot;A WRAP burst must be 2,4,8,16 beats, so a single-beat WRAP
       cannot exist.&quot;

WRAP + single(1 beat) 조합은 불가능 (WRAP은 최소 2 beats)
  binsof(cp_burst.wrap) &amp;&amp; binsof(cp_len.single)
  = &quot;burst가 wrap이면서 len이 single인&quot; 교차 bin
  → ignore로 제외 (도달 불가능)

binsof(...) = 특정 bin을 지칭하는 문법</code></pre><p>이게 이력서의 <span style="color:orange"><strong>&quot;formally justifying structurally unreachable bins via ignore_bins&quot;</strong></span>예요.</p>
<hr>
<h2 id="커버리지-구조-시각화">커버리지 구조 시각화</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/2aafb2e8-ac60-43a9-ab6f-f97388cce4a2/image.png" alt=""></p>
<hr>
<h2 id="new--write">new() &amp; write()</h2>
<pre><code class="language-systemverilog">function new(string name, uvm_component parent);
    super.new(name, parent);
    axi_cg = new();          // covergroup 인스턴스화
endfunction

function void write(axi4_seq_item t);
    tr        = t;
    strb_kind = classify_strb(t);
    axi_cg.sample();         // 커버리지 샘플!
    n_sampled++;
endfunction</code></pre>
<pre><code>write() = monitor가 ap.write(tr) 하면 자동 호출

1. tr에 받은 트랜잭션 저장 (covergroup이 tr을 읽음)
2. strb_kind 계산 (파생 값)
3. axi_cg.sample() → 현재 tr 값으로 모든 coverpoint 체크!
4. 카운트 증가

axi_cg = new() 필수:
  covergroup은 선언만으로 인스턴스 안 됨 → new()로 생성해야 샘플 가능</code></pre><pre><code>sample()이 하는 일:
  현재 tr.dir, tr.burst, tr.size... 값을 보고
  해당하는 bin을 체크 (예: tr.burst=INCR이면 cp_burst.incr 체크)</code></pre><hr>
<h2 id="classify_strb--strobe-분류">classify_strb() — strobe 분류</h2>
<pre><code class="language-systemverilog">protected function axi_strb_kind_e classify_strb(axi4_seq_item t);
    bit all_full = 1;
    bit all_zero = 1;
    foreach (t.strb[i]) begin
        if (t.strb[i] !== t.lane_mask(i)) all_full = 0;
        if (t.strb[i] !== &#39;0)             all_zero = 0;
    end
    if (all_zero) return STRB_NONE;
    if (all_full) return STRB_FULL;
    return STRB_PARTIAL;
endfunction</code></pre>
<pre><code>각 beat의 strobe를 검사:

all_full: 모든 beat가 자기 유효 lane을 다 켰나?
  strb[i] == lane_mask(i) → 그 beat는 full
  하나라도 아니면 all_full = 0

all_zero: 모든 beat가 아무것도 안 켰나?
  strb[i] == 0 → 그 beat는 empty
  하나라도 아니면 all_zero = 0

판정:
  전부 0 → STRB_NONE
  전부 full → STRB_FULL
  그 외 → STRB_PARTIAL

lane_mask(i) = seq_item의 그 함수! (유효 lane 계산)</code></pre><hr>
<h2 id="coverage_report--자체-리포트">coverage_report() — 자체 리포트</h2>
<pre><code class="language-systemverilog">function string coverage_report();
    coverage_report = {
        &quot;=========== AXI4 functional coverage ===========\n&quot;,
        $sformatf(&quot;  bursts sampled : %0d\n&quot;, n_sampled),
        $sformatf(&quot;  %-14s %7.2f %%\n&quot;, &quot;cp_dir&quot;, axi_cg.cp_dir.get_inst_coverage()),
        ...
        $sformatf(&quot;  %-14s %7.2f %%\n&quot;, &quot;TOTAL&quot;, axi_cg.get_inst_coverage())
    };
endfunction</code></pre>
<p>주석에 중요한 실무 이유가 있다:</p>
<pre><code>// Vivado&#39;s external report generator (xcrg) needs a PRO license tier,
// which the free BASIC tier does not have — so we build the report
// from the SystemVerilog coverage API instead.</code></pre><pre><code>Vivado 무료 버전은 커버리지 리포트 생성기(xcrg)가 없음
→ SystemVerilog 커버리지 API로 직접 리포트 작성
→ 어떤 시뮬레이터에서도 동작, 무료

get_inst_coverage() = 그 coverpoint/cross의 현재 커버리지 %
  cp_dir.get_inst_coverage() → cp_dir이 몇 % 채워졌나
  axi_cg.get_inst_coverage() → 전체 covergroup 몇 %</code></pre><p>출력 예:</p>
<pre><code>=========== AXI4 functional coverage ===========
  bursts sampled : 351
  ---------------------------------------------
  cp_dir          100.00 %
  cp_burst        100.00 %
  cp_size         100.00 %
  cp_len          100.00 %
  cp_resp         100.00 %
  cp_region       100.00 %
  cp_strb         100.00 %
  ---------------------------------------------
  x_burst_size    100.00 %
  x_burst_len     100.00 %
  x_dir_burst     100.00 %
  =============================================
  TOTAL           100.00 %
===============================================</code></pre><p>이게 이력서의 <strong>&quot;100% functional + cross coverage&quot;</strong> 증거 출력이에요.</p>
<hr>
<h2 id="report_phase">report_phase()</h2>
<pre><code class="language-systemverilog">function void report_phase(uvm_phase phase);
    super.report_phase(phase);
    `uvm_info(&quot;COV&quot;, coverage_report(), UVM_LOW)
endfunction</code></pre>
<pre><code>시뮬레이션 끝에 커버리지 리포트 출력
→ 스코어보드의 report_phase(미스매치 개수)와 나란히 로그에 찍힘

두 개를 같이 보면:
  스코어보드: &quot;다 맞았나?&quot; (0 mismatch)
  커버리지:   &quot;다 봤나?&quot; (100%)
  → 둘 다 만족해야 검증 완료</code></pre><hr>
<h2 id="스코어보드-vs-커버리지--두-질문">스코어보드 vs 커버리지 — 두 질문</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/ed93f697-24ee-4f16-98a7-5f1d0857a6f6/image.png" alt=""></p>
<table>
<thead>
<tr>
<th></th>
<th>Scoreboard</th>
<th>Coverage</th>
</tr>
</thead>
<tbody><tr>
<td><strong>질문</strong></td>
<td>맞았나? (correctness)</td>
<td>봤나? (completeness)</td>
</tr>
<tr>
<td><strong>베이스</strong></td>
<td>uvm_scoreboard</td>
<td>uvm_subscriber</td>
</tr>
<tr>
<td><strong>핵심</strong></td>
<td>ref_model과 비교</td>
<td>covergroup 샘플</td>
</tr>
<tr>
<td><strong>실패 시</strong></td>
<td>uvm_error (버그)</td>
<td>낮은 % (검증 부족)</td>
</tr>
<tr>
<td><strong>목표</strong></td>
<td>0 mismatch</td>
<td>100%</td>
</tr>
</tbody></table>
<hr>
<h2 id="왜-둘-다-필요한가-매우-중요">왜 둘 다 필요한가 (매우 중요)</h2>
<pre><code>커버리지 100%인데 스코어보드 약함:
  → 다 실행했지만 틀린 걸 못 잡음 (체커 부실)

스코어보드 강한데 커버리지 낮음:
  → 실행한 건 정확히 검증하지만 안 본 게 많음 (자극 부족)

→ 둘 다 있어야 &quot;많이 실행하고(coverage) 정확히 검증한다(scoreboard)&quot;

이력서의 blind pass criterion 버그가 이 교훈:
  커버리지는 높은데 체커가 눈멀어있었음
  → coverage ≠ correctness</code></pre><hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<pre><code>Coverage = &quot;우리가 무엇을 검증했나&quot;를 추적하는 수집기

uvm_subscriber로 monitor의 ap를 받아
  covergroup을 sample() → 각 조합을 봤는지 체크리스트 채움

coverpoint: dir/burst/size/len/region/strb 개별 추적
cross:      조합 추적 (burst×size, burst×len, dir×burst)
ignore_bins: 도달 불가능한 것 제외 (구멍 아니라 의도)

스코어보드와 다른 질문:
  스코어보드 = &quot;맞았나?&quot;
  커버리지   = &quot;봤나?&quot;
  → 둘 다 만족해야 검증 완료</code></pre><hr>
<h2 id="시리즈-완결--전체-uvm-환경">시리즈 완결 — 전체 UVM 환경</h2>
<p>이제 AXI4 UVM 환경의 <strong>모든 파일</strong>을 다뤘습니다.</p>
<pre><code>                        tb_top (조립 &amp; 시작)
                            │
                    test (지휘, get_seq)
                            │
                    sequence (자극 생성)
                            │
                    seq_item (트랜잭션 단위)
                            │
        cfg/agent/env (계층) ─ driver (구동)
                            │
                    interface (신호+clocking)
                            │
                          DUT
                            │
                    monitor (관찰)
                            │
              ┌─────────────┴─────────────┐
        scoreboard (맞나?)          coverage (봤나?)
              │                           │
         ref_model                   covergroup
              └─────────────┬─────────────┘
                       pass/fail + 100%</code></pre><p>검증의 두 축이 완성됩니다:</p>
<ul>
<li><strong>정확성(correctness)</strong>: scoreboard가 ref_model과 비교</li>
<li><strong>완결성(completeness)</strong>: coverage가 모든 조합을 추적</li>
</ul>
<p>이 둘이 만나서 &quot;많이 실행하고, 정확히 검증했다&quot;는 신뢰를 만듭니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (10) — sequences ]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-10-sequences</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-10-sequences</guid>
            <pubDate>Thu, 30 Jul 2026 21:13:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: ... · agent_cfg/agent/env · base_test · <strong>sequences</strong></p>
</blockquote>
<hr>
<h2 id="sequence란">Sequence란</h2>
<pre><code>&quot;어떤 트랜잭션을 어떤 순서로 만들지&quot;를 정의하는 자극 프로그램

test가 get_seq()로 고르는 그 시퀀스들
→ 실제로 아이템(axi4_seq_item)을 randomize해서 시퀀서에 밀어넣음</code></pre><p>지금까지: test가 &quot;어떤 시퀀스 쓸지&quot; 골랐다면, 이 파일은 <strong>그 시퀀스가 실제로 뭘 하는지</strong>다.</p>
<hr>
<h2 id="전체-구조">전체 구조</h2>
<pre><code class="language-c">axi4_base_seq (부모)
    ├── num 필드 (몇 개 만들지)
    └── wr_then_rd() 헬퍼 (쓰고 같은 영역 읽기)

    ↓ 상속

axi4_write_seq    : N개 랜덤 쓰기
axi4_read_seq     : N개 랜덤 읽기
axi4_rand_seq     : N개 랜덤 (읽기/쓰기 섞임)
axi4_wr_rd_seq    : 쓰고 → 같은 영역 읽기 (self-checking, 기본 시퀀스)
axi4_os_*_seq     : outstanding용 (고정 주소 8개)
axi4_cov_seq      : 커버리지 채우기 (모든 조합 sweep)
axi4_directed_seq : 손으로 고른 코너 케이스
axi4_wrap_seq     : WRAP 버그 재현 (FAIL 예상)</code></pre>
<h2 id="⭐-가장-중요한-설계-결정--wrap-격리">⭐ 가장 중요한 설계 결정 — WRAP 격리</h2>
<pre><code class="language-c">// the general-purpose sequences deliberately exclude AXI_WRAP so
// the normal regression is clean.</code></pre>
<pre><code class="language-c">일반 시퀀스들은 일부러 AXI_WRAP을 제외함!

이유:
  DUT가 WRAP을 선형 증가로 처리하는 버그가 있음
  → 일반 시퀀스에 WRAP을 넣으면 정상 회귀도 FAIL
  → WRAP은 별도 시퀀스/테스트로 격리해서 &quot;일부러&quot; 버그 시연

→ 그래서 대부분 시퀀스에 burst inside {AXI_INCR, AXI_FIXED}
  (WRAP 빠짐)</code></pre>
<p>이건 실무에서 중요한 판단이에요. &quot;알려진 버그&quot;를 정상 회귀에서 분리해서, 회귀는 clean하게 유지하고 버그는 별도로 문서화/추적하는 방식.</p>
<hr>
<h1 id="part-1-axi4_base_seq--부모--헬퍼">Part 1: axi4_base_seq — 부모 &amp; 헬퍼</h1>
<pre><code class="language-c">class axi4_base_seq extends uvm_sequence #(axi4_seq_item);
    `uvm_object_utils(axi4_base_seq)
    rand int unsigned num = 5;   // how many transactions / pairs

    function new(string name = &quot;axi4_base_seq&quot;);
        super.new(name);
    endfunction</code></pre>
<pre><code class="language-c">uvm_sequence #(axi4_seq_item) 상속
  → axi4_seq_item 타입 아이템을 생성/전달하는 시퀀스

num = 생성할 트랜잭션 개수 (기본 5)
  → test에서 s.num = 40 처럼 바꿀 수 있음 (cov_test에서 봤던 것)</code></pre>
<h2 id="wr_then_rd-헬퍼--핵심-도구">wr_then_rd() 헬퍼 — 핵심 도구</h2>
<pre><code class="language-c">task wr_then_rd(bit [AXI_ADDR_WIDTH-1:0] a,
                bit [7:0]                l,
                bit [2:0]                s,
                axi_burst_e              b);
    axi4_seq_item wr, rd;

    wr = axi4_seq_item::type_id::create(&quot;wr&quot;);
    start_item(wr);
    if (!wr.randomize() with { dir == AXI_WRITE; addr == a;
                               len == l; size == s; burst == b; })
        `uvm_error(&quot;RAND&quot;, &quot;directed write randomize() failed&quot;)
    finish_item(wr);

    rd = axi4_seq_item::type_id::create(&quot;rd&quot;);
    start_item(rd);
    if (!rd.randomize() with { dir == AXI_READ; addr == a;
                               len == l; size == s; burst == b;
                               id == wr.id; })
        `uvm_error(&quot;RAND&quot;, &quot;directed read randomize() failed&quot;)
    finish_item(rd);
endtask</code></pre>
<pre><code>&quot;쓰고 → 정확히 같은 영역 읽기&quot; 한 세트

같은 addr/len/size/burst로:
  1. 쓰기 아이템 생성 → 랜덤 데이터로 씀
  2. 읽기 아이템 생성 → 같은 영역 읽음
  → 스코어보드가 &quot;쓴 값 == 읽은 값&quot; 비교 → self-checking

정상 DUT면 데이터 일치해야 함</code></pre><h3 id="start_item--finish_item--시퀀스-드라이버-핸드셰이크">start_item / finish_item — 시퀀스-드라이버 핸드셰이크</h3>
<pre><code class="language-c">start_item(wr)   → &quot;이 아이템 보낼게, 시퀀서 준비되면 알려줘&quot;
                   (드라이버가 get_next_item 할 때까지 대기)
wr.randomize()   → 제약 안에서 랜덤화
finish_item(wr)  → &quot;완성됐어, 드라이버로 보내&quot;
                   (드라이버가 item_done 할 때까지 대기)

→ driver의 get_next_item() / item_done()과 짝을 이루는 반대편!</code></pre>
<h3 id="randomize-with-----인라인-제약"><code>randomize() with { ... }</code> — 인라인 제약</h3>
<pre><code class="language-c">randomize() with { dir == AXI_WRITE; addr == a; ... }

= &quot;이번 랜덤화는 이 조건들을 지켜라&quot;
  dir는 WRITE로 고정
  addr는 인자 a로 고정
  나머지(data, strb)는 seq_item 제약 안에서 랜덤

→ seq_item의 constraint(c_4k, c_align 등)는 그대로 적용되면서
  추가로 이 조건들을 얹음</code></pre>
<h3 id="읽기가-id--wrid-하는-이유">읽기가 <code>id == wr.id</code> 하는 이유</h3>
<pre><code>읽기 아이템의 id를 쓰기와 같게 맞춤
→ 같은 트랜잭션 쌍임을 ID로 표시
→ (outstanding 환경에서 매칭에 도움)</code></pre><hr>
<h1 id="part-2-기본-랜덤-시퀀스들">Part 2: 기본 랜덤 시퀀스들</h1>
<h2 id="write--read--rand--거의-동일">write / read / rand — 거의 동일</h2>
<pre><code class="language-c">class axi4_write_seq extends axi4_base_seq;
    task body();
        repeat (num) begin
            req = axi4_seq_item::type_id::create(&quot;req&quot;);
            start_item(req);
            if (!req.randomize() with { dir == AXI_WRITE;
                                        burst inside {AXI_INCR, AXI_FIXED}; })
                `uvm_error(&quot;RAND&quot;, &quot;write item randomize() failed&quot;)
            finish_item(req);
        end
    endtask
endclass</code></pre>
<pre><code class="language-c">body() = 시퀀스의 메인 (test가 seq.start() 하면 실행됨)

repeat (num) → num개 생성
req = built-in 핸들 (uvm_sequence가 제공)

세 시퀀스 차이는 dir 제약만:
  write_seq: dir == AXI_WRITE
  read_seq:  dir == AXI_READ
  rand_seq:  dir 제약 없음 (읽기/쓰기 랜덤 혼합)

공통: burst inside {AXI_INCR, AXI_FIXED}  ← WRAP 제외!</code></pre>
<h2 id="wr_rd_seq--기본-시퀀스-base_test가-씀">wr_rd_seq — 기본 시퀀스 (base_test가 씀)</h2>
<pre><code class="language-c">class axi4_wr_rd_seq extends axi4_base_seq;
    task body();
        repeat (num) begin
            axi4_seq_item wr, rd;

            wr = axi4_seq_item::type_id::create(&quot;wr&quot;);
            start_item(wr);
            if (!wr.randomize() with { dir == AXI_WRITE; burst == AXI_INCR; })
                `uvm_error(&quot;RAND&quot;, &quot;write randomize() failed&quot;)
            finish_item(wr);

            rd = axi4_seq_item::type_id::create(&quot;rd&quot;);
            start_item(rd);
            if (!rd.randomize() with { dir == AXI_READ; burst == AXI_INCR;
                                       addr == wr.addr; len == wr.len;
                                       size == wr.size; id == wr.id; })
                `uvm_error(&quot;RAND&quot;, &quot;read randomize() failed&quot;)
            finish_item(rd);
        end
    endtask
endclass</code></pre>
<pre><code>랜덤 쓰기 → 같은 영역 읽기 (wr_then_rd와 비슷하지만 주소도 랜덤)

wr는 완전 랜덤 (addr/len/size 랜덤)
rd는 wr를 따라감 (addr == wr.addr, len == wr.len, ...)
→ 랜덤한 위치에 쓰고 그 위치를 읽어서 검증

이게 base_test의 기본 자극 → &quot;환경이 제대로 도나&quot; 확인</code></pre><p><img src="https://velog.velcdn.com/images/houston_guy2/post/00f29ec3-b9b5-4607-9c48-fd5a95e4d27a/image.png" alt=""></p>
<hr>
<h1 id="part-3-outstanding-시퀀스">Part 3: outstanding 시퀀스</h1>
<h2 id="고정-주소-계획">고정 주소 계획</h2>
<pre><code class="language-c">class axi4_os_base_seq extends axi4_base_seq;
    localparam int OS_COUNT = 8;
    // 0x5000, 0x5100, ... 0x5700
    function bit [AXI_ADDR_WIDTH-1:0] os_addr(int i);
        return 16&#39;h5000 + (i * 16&#39;h0100);
    endfunction
endclass</code></pre>
<pre><code>8개 버스트를 고정된 겹치지 않는 주소에 배치
  0x5000, 0x5100, 0x5200, ... 0x5700
  각 4 beat × 4B, 전부 4KB 안전

왜 고정 주소? outstanding은 &quot;여러 개 동시 진행&quot;을 봐야 하는데
            주소가 겹치면 검증이 복잡 → 겹치지 않게 미리 계획</code></pre><h2 id="write--read-분리">write / read 분리</h2>
<pre><code class="language-c">class axi4_os_write_seq extends axi4_os_base_seq;
    task body();
        for (int i = 0; i &lt; OS_COUNT; i++) begin
            req = axi4_seq_item::type_id::create(&quot;req&quot;);
            start_item(req);
            if (!req.randomize() with { dir   == AXI_WRITE;
                                        burst == AXI_INCR;
                                        addr  == os_addr(i);
                                        len   == 3;
                                        size  == 3&#39;d2;
                                        foreach (strb[j])
                                            strb[j] == {AXI_STRB_WIDTH{1&#39;b1}}; })
                `uvm_error(&quot;RAND&quot;, &quot;outstanding write randomize() failed&quot;)
            finish_item(req);
        end
    endtask
endclass</code></pre>
<pre><code>write와 read가 별도 시퀀스인 이유:
  test에서 사이에 drain을 넣어야 함
  → 읽기가 아직 안 끝난 쓰기를 추월하면 안 됨
  → (AXI 읽기/쓰기 채널 독립)

foreach (strb[j]) strb[j] == {AXI_STRB_WIDTH{1&#39;b1}}
  = 모든 strobe 비트를 1로 (전체 바이트 쓰기)
  {N{1&#39;b1}} = 1을 N번 반복 = 전부 1</code></pre><p>이게 outstanding_test에서 봤던 &quot;phase 1 쓰기 → drain → phase 2 읽기&quot;의 시퀀스 쪽 구현이에요.</p>
<hr>
<h1 id="part-4-cov_seq--커버리지-채우기">Part 4: cov_seq — 커버리지 채우기</h1>
<pre><code>// this sequence allows AXI_WRAP, so it can legitimately surface
// the DUT&#39;s WRAP addressing bug as scoreboard mismatches.</code></pre><p>가장 복잡한 시퀀스. 세 단계로 커버리지를 채운다.</p>
<pre><code class="language-c">task body();
    foreach_burst_size();   // 모든 (burst × size) 조합
    burst_len_sweep();      // 모든 (burst × length class) 조합
    repeat (num) begin      // 넓은 랜덤 (length/region/strobe bin 채우기)
        req = axi4_seq_item::type_id::create(&quot;req&quot;);
        start_item(req);
        if (!req.randomize())
            `uvm_error(&quot;RAND&quot;, &quot;coverage item randomize() failed&quot;)
        finish_item(req);
    end
endtask</code></pre>
<h2 id="왜-sweep이-필요한가">왜 sweep이 필요한가</h2>
<pre><code>랜덤만으로는 특정 조합이 안 채워짐 (plateau)
  FIXED/WRAP은 가중치가 낮아서 (dist 8:1:1)
  → burst × length 교차 bin에 구멍이 남

해결: deterministic sweep으로 모든 조합을 명시적으로 방문
  → 랜덤이 못 가는 코너를 확정적으로 채움</code></pre><p>이게 이력서의 <strong>&quot;constrained-random (plateau 98.3%) + directed sweeps&quot;</strong>의 실제 코드예요.</p>
<h2 id="foreach_burst_size--모든-버스트-×-크기">foreach_burst_size — 모든 (버스트 × 크기)</h2>
<pre><code class="language-c">task foreach_burst_size();
    axi_burst_e blist[3] = &#39;{AXI_FIXED, AXI_INCR, AXI_WRAP};
    bit [AXI_ADDR_WIDTH-1:0] a = 16&#39;h2000;
    foreach (blist[b]) begin
        for (int s = 0; s &lt;= 2; s++) begin
            automatic bit [7:0] l = (blist[b] == AXI_WRAP) ? 8&#39;d3 : 8&#39;d2;
            wr_then_rd(a, l, s[2:0], blist[b]);
            a += 16&#39;h0100;
        end
    end
endtask</code></pre>
<pre><code>3가지 burst × 3가지 size = 9개 조합 전부 방문
  각각 wr_then_rd로 쓰고 읽음

WRAP일 때만 len=3 (4 beats, WRAP 합법)
나머지는 len=2

automatic = 각 반복마다 새 변수 (루프 변수 캡처 문제 방지)</code></pre><h2 id="burst_len_sweep--모든-버스트-×-길이">burst_len_sweep — 모든 (버스트 × 길이)</h2>
<pre><code class="language-c">task burst_len_sweep();
    bit [7:0] lens[4] = &#39;{8&#39;d0, 8&#39;d2, 8&#39;d6, 8&#39;d12};  // 각 길이 클래스 대표

    foreach (lens[i]) begin
        wr_then_rd(a, lens[i], 3&#39;d2, AXI_FIXED); a += 16&#39;h0100;
        wr_then_rd(a, lens[i], 3&#39;d2, AXI_INCR);  a += 16&#39;h0100;
    end
    // WRAP은 2/4/8/16 beats만 합법
    wr_then_rd(a, 8&#39;d1,  3&#39;d2, AXI_WRAP); ...
    wr_then_rd(a, 8&#39;d3,  3&#39;d2, AXI_WRAP); ...
    wr_then_rd(a, 8&#39;d7,  3&#39;d2, AXI_WRAP); ...
    wr_then_rd(a, 8&#39;d15, 3&#39;d2, AXI_WRAP);
endtask</code></pre>
<pre><code>길이 클래스: single(0) / 2-4(2) / 5-8(6) / 9-16(12)
  각 클래스 대표값으로 FIXED, INCR 방문

WRAP은 합법 길이(1/3/7/15 = 2/4/8/16 beats)만 별도로</code></pre><hr>
<h1 id="part-5-directed_seq--코너-케이스">Part 5: directed_seq — 코너 케이스</h1>
<pre><code class="language-c">task body();
    //          addr      len   size  burst        what it exercises
    wr_then_rd(16&#39;h0000, 8&#39;d0,  3&#39;d2, AXI_INCR);  // single beat, full width
    wr_then_rd(16&#39;h0100, 8&#39;d15, 3&#39;d2, AXI_INCR);  // max 16-beat burst
    wr_then_rd(16&#39;h0FC0, 8&#39;d15, 3&#39;d2, AXI_INCR);  // ends exactly at 4KB edge
    wr_then_rd(16&#39;h0200, 8&#39;d7,  3&#39;d0, AXI_INCR);  // narrow: 1 byte/beat
    wr_then_rd(16&#39;h0300, 8&#39;d3,  3&#39;d1, AXI_INCR);  // narrow: 2 bytes/beat
    wr_then_rd(16&#39;h0400, 8&#39;d3,  3&#39;d2, AXI_FIXED); // FIXED: same address
endtask</code></pre>
<pre><code>손으로 고른 위험한 코너들:

single beat     : len=0 → 1 beat (경계값)
max burst       : len=15 → 16 beats (최대)
4KB edge        : 0x0FC0 + 16beat×4B = 0x1000 딱 경계
narrow 1B/beat  : size=0 → lane_mask가 토글하는 케이스
narrow 2B/beat  : size=1
FIXED           : 같은 주소 반복

→ 랜덤이 잘 안 만드는 경계값을 확정적으로 테스트</code></pre><p>이게 directed_test가 쓰는 시퀀스. 주석에 각 케이스가 뭘 테스트하는지 다 적혀있어요.</p>
<hr>
<h1 id="part-6-wrap_seq--버그-재현-핵심">Part 6: wrap_seq — 버그 재현 (핵심)</h1>
<pre><code>// The reference model wraps correctly, the DUT does not,
// so this sequence is EXPECTED to raise scoreboard mismatches.</code></pre><h2 id="왜-wrap-write--incr-read인가-가장-중요">왜 WRAP write + INCR read인가 (가장 중요)</h2>
<pre><code class="language-c">task wrap_write_incr_read(bit [AXI_ADDR_WIDTH-1:0] a,
                          bit [7:0]                l,
                          bit [2:0]                s);
    axi4_seq_item wr, rd;
    int unsigned nbytes = 1 &lt;&lt; s;
    int unsigned total  = (l + 1) * nbytes;          // wrap window size
    bit [AXI_ADDR_WIDTH-1:0] base = a - (a % total); // window base

    // WRAP으로 쓰기 (window 중간에서 시작 → 실제로 wrap 발생)
    wr = axi4_seq_item::type_id::create(&quot;wr_wrap&quot;);
    start_item(wr);
    if (!wr.randomize() with { dir == AXI_WRITE; addr == a;
                               len == l; size == s; burst == AXI_WRAP;
                               foreach (strb[j])
                                   strb[j] == {AXI_STRB_WIDTH{1&#39;b1}}; })
        `uvm_error(&quot;RAND&quot;, &quot;wrap write randomize() failed&quot;)
    finish_item(wr);

    // INCR로 window 전체 읽기
    rd = axi4_seq_item::type_id::create(&quot;rd_incr&quot;);
    start_item(rd);
    if (!rd.randomize() with { dir == AXI_READ; addr == base;
                               len == l; size == s; burst == AXI_INCR; })
        `uvm_error(&quot;RAND&quot;, &quot;incr read-back randomize() failed&quot;)
    finish_item(rd);
endtask</code></pre>
<p>주석이 핵심을 설명한다:</p>
<pre><code>// Why not a plain WRAP write + WRAP read?  Because that hides the
// bug: the DUT is consistently wrong on both sides, so it reads
// from the same (wrong) places it wrote to and the data matches.
// Only an INCR read exposes where the bytes actually landed.</code></pre><pre><code>왜 WRAP 쓰기 + WRAP 읽기가 아니라 WRAP 쓰기 + INCR 읽기?

WRAP write + WRAP read (대칭):
  DUT가 쓸 때도 틀리고 읽을 때도 똑같이 틀림
  → 틀린 위치에 쓰고, 그 틀린 위치에서 읽음
  → 데이터는 일치! → 버그 숨겨짐 ❌

WRAP write + INCR read (비대칭):
  DUT: WRAP 쓰기를 틀린(선형) 위치에 씀
  INCR 읽기: 올바른 순서로 window 전체를 읽음
  → &quot;바이트가 실제로 어디 떨어졌나&quot; 드러남
  → 예상과 불일치 → 버그 노출! ✅</code></pre><p>이게 이력서의 <strong>&quot;asymmetric WRAP-write / INCR-read cross-check that surfaced 24 mismatches&quot;</strong>의 정확한 코드예요.</p>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/b85215c1-06c8-4a20-b4c8-07545ffdf5c0/image.png" alt=""></p>
<h2 id="body--실제-실행">body — 실제 실행</h2>
<pre><code class="language-c">task body();
    // len 1/3/7/15 -&gt; 2/4/8/16 beats
    wrap_write_incr_read(16&#39;h0A08, 8&#39;d3, 3&#39;d2);  // window 0x0A00..0x0A0F
    wrap_write_incr_read(16&#39;h0B10, 8&#39;d7, 3&#39;d2);  // window 0x0B00..0x0B1F
endtask</code></pre>
<pre><code>0x0A08에서 WRAP 쓰기 시작 (window 0x0A00~0x0A0F 중간)
  → 0x0A08부터 쓰다가 0x0A0F 넘으면 0x0A00으로 되돌아와야 함 (정상)
  → DUT는 안 되돌아오고 계속 직진 (버그)

base(0x0A00)부터 INCR로 window 전체 읽기
  → 실제로 어디 썼는지 드러남 → 미스매치!</code></pre><hr>
<h1 id="전체-시퀀스-지도">전체 시퀀스 지도</h1>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/1cf5bd2b-e650-49d1-874d-6575a6cd6852/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>시퀀스</th>
<th>WRAP 포함?</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>write/read/rand</td>
<td>❌</td>
<td>N개 랜덤 (INCR/FIXED만)</td>
</tr>
<tr>
<td>wr_rd</td>
<td>❌</td>
<td>쓰고 읽기, 기본 자극</td>
</tr>
<tr>
<td>os_*</td>
<td>❌</td>
<td>고정 주소, 파이프라인용</td>
</tr>
<tr>
<td>cov</td>
<td>✅</td>
<td>커버리지 sweep (WRAP 버그 노출 가능)</td>
</tr>
<tr>
<td>directed</td>
<td>❌</td>
<td>손으로 고른 코너</td>
</tr>
<tr>
<td>wrap</td>
<td>✅</td>
<td>비대칭 cross-check (버그 재현)</td>
</tr>
</tbody></table>
<hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<pre><code>Sequence = 어떤 트랜잭션을 어떤 순서로 만들지 정의하는 자극 프로그램

axi4_base_seq: 부모, wr_then_rd() 헬퍼 (쓰고 같은 영역 읽기)

핵심 설계:
  1. 일반 시퀀스는 WRAP 제외 → 정상 회귀 clean 유지
  2. WRAP은 wrap_seq로 격리 → 비대칭(WRAP write + INCR read)으로
     버그를 &quot;일부러&quot; 노출
  3. cov_seq는 deterministic sweep으로 랜덤 plateau의 구멍을 채움

start_item/finish_item = driver의 get_next_item/item_done과 짝
randomize() with {...} = seq_item 제약 위에 추가 조건</code></pre><hr>
<h2 id="시리즈-마무리--전체-데이터-경로">시리즈 마무리 — 전체 데이터 경로</h2>
<p>이제 AXI4 UVM 환경의 모든 파일을 봤습니다.</p>
<pre><code>tb_top          → 조립 &amp; 시작
cfg/agent/env   → 계층 빌드
base_test       → 지휘 (get_seq로 시퀀스 선택)
sequence        → 자극 생성 (이 글!)  ← randomize()
    ↓
seq_item        → 트랜잭션 단위
    ↓
driver          → 핀 구동
    ↓ (인터페이스 = 신호 + clocking block)
DUT
    ↓
monitor         → 관찰 &amp; 재조립
    ↓
scoreboard      → ref_model과 비교 → pass/fail</code></pre><p>자극이 생성(sequence)되어 아이템(seq_item)으로 만들어지고, 드라이버가 핀을 흔들어 DUT에 전달하고, 모니터가 관찰해서 스코어보드가 정답지와 비교하는 — 검증의 전체 순환이 완성됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Study guide + Summary]]></title>
            <link>https://velog.io/@houston_guy2/Study-guide-Summary</link>
            <guid>https://velog.io/@houston_guy2/Study-guide-Summary</guid>
            <pubDate>Thu, 30 Jul 2026 17:04:16 GMT</pubDate>
            <description><![CDATA[<h1 id="pipelined-l1-cache-controller-uvm">Pipelined L1 Cache Controller UVM</h1>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/f8fa7f21-087d-40ed-bd30-e8d5058d10b4/image.png" alt=""></p>
<p>이 문서 하나로 공부할 수 있게 <strong>방법 + 내용</strong>을 모두 담았다.</p>
<ul>
<li><strong>Part 0~1</strong> — 어떻게, 어떤 순서로 공부할지</li>
<li><strong>Part 2</strong> — 실제 학습 내용 (핵심)</li>
<li><strong>Part 3~7</strong> — 확인·연습·면접 대비</li>
</ul>
<hr>
<h1 id="part-0-공부-3원칙">Part 0. 공부 3원칙</h1>
<p><strong>① 캐시는 &quot;구조&quot;보다 &quot;타이밍&quot;이다.</strong>
4-way, PLRU, write-back 같은 용어는 하루면 외운다. 이 프로젝트에서 진짜 배울 건
<strong>어느 사이클에 무엇이 유효한가</strong> 다. 배열 read가 registered라는 사실 하나가
파이프라인 구조·포워딩·FSM을 전부 결정한다. 사이클을 못 세면 아무것도 이해 못 한 것이다.</p>
<p><strong>② 파일 순서가 아니라 &quot;요청 하나의 일생&quot;을 따라가라.</strong>
<code>ls</code> 순서로 읽으면 죽는다. CPU 요청 하나가
<strong>태어나서(seq) → 핀을 흔들고(driver) → S1/S2를 통과하고(DUT) → 관측되고(monitor) → 검사받는(scoreboard)</strong>
경로를 따라가면 30개 파일이 하나의 이야기로 연결된다.</p>
<p><strong>③ &quot;PASS&quot;를 믿지 마라 — 이 프로젝트의 핵심 교훈.</strong>
이 저장소의 역사는 <strong>통과하는데 아무것도 검증하지 않던 테스트벤치</strong>를 고친 기록이다
(Part 4). 그러니 공부할 때도 같은 태도로: &quot;이 체크가 실제로 발화한 적이 있나?&quot;를
계속 물어라. 이게 이 프로젝트에서 얻어갈 가장 값비싼 습관이다.</p>
<hr>
<h1 id="part-1-학습-경로-3개-레이어">Part 1. 학습 경로 (3개 레이어)</h1>
<h2 id="🟦-layer-a--큰-그림--캐시-기본">🟦 Layer A — 큰 그림 + 캐시 기본</h2>
<table>
<thead>
<tr>
<th>순서</th>
<th>무엇</th>
<th>집중</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><a href="OVERVIEW.svg">OVERVIEW.svg</a></td>
<td><strong>손으로 따라 그리기.</strong> 백지에 4블록(TB / if / DUT / 주소분할) 재현되면 통과</td>
</tr>
<tr>
<td>2</td>
<td><code>rtl/l1_cache_pkg.sv</code></td>
<td>60줄짜리 파일 하나에 geometry·FSM·PLRU가 다 있다. <strong>여기부터 읽어라</strong></td>
</tr>
<tr>
<td>3</td>
<td>Part 2.1~2.2 + 연습문제</td>
<td>주소 분해를 손으로 3번</td>
</tr>
<tr>
<td>4</td>
<td><code>run.bat l1_cache_smoke_test</code> 로그</td>
<td>코드 전에 <strong>살아있는 걸 먼저</strong>. hit rate·way 분포·커버리지</td>
</tr>
</tbody></table>
<h2 id="🟩-layer-b--dut-타이밍-⭐-질문의-절반">🟩 Layer B — DUT 타이밍 ⭐ (질문의 절반)</h2>
<table>
<thead>
<tr>
<th>순서</th>
<th>파일</th>
<th>집중</th>
</tr>
</thead>
<tbody><tr>
<td>5</td>
<td><code>rtl/sram_macro.sv</code> (63줄)</td>
<td><strong><code>rdata &lt;= mem[raddr]</code></strong>. 왜 non-blocking인가, write와의 순서</td>
</tr>
<tr>
<td>6</td>
<td><code>rtl/l1_cache_core.sv:63~230</code></td>
<td>파이프라인 레지스터 → 배열 주소 mux → <strong>포워딩</strong> → hit 판정</td>
</tr>
<tr>
<td>7</td>
<td><code>rtl/l1_cache_core.sv:262~444</code></td>
<td>응답 조건식, FSM 순차/조합 두 블록</td>
</tr>
<tr>
<td>8</td>
<td>Part 2.5 사이클 표</td>
<td>dirty miss 사이클을 <strong>직접 세어보기</strong></td>
</tr>
</tbody></table>
<h2 id="🟨-layer-c--uvm-배선--검증-장치">🟨 Layer C — UVM 배선 + 검증 장치</h2>
<table>
<thead>
<tr>
<th>순서</th>
<th>파일</th>
<th>집중</th>
</tr>
</thead>
<tbody><tr>
<td>9</td>
<td><code>tb/l1_cache_if.sv</code></td>
<td>clocking block 4개, SVA 15개, <code>assert_reset()</code></td>
</tr>
<tr>
<td>10</td>
<td><code>tb/agent/l1_cache_cpu_driver.sv</code></td>
<td><code>try_next_item</code> 으로 만드는 <strong>진짜 back-to-back</strong></td>
</tr>
<tr>
<td>11</td>
<td><code>tb/agent/l1_cache_cpu_monitor.sv</code> ⭐</td>
<td>왜 analysis port가 <strong>2개</strong>인가</td>
</tr>
<tr>
<td>12</td>
<td><code>tb/env/l1_cache_scoreboard.sv</code> ⭐⭐</td>
<td><strong>가장 중요.</strong> 골든 캐시 모델 = 이 프로젝트의 심장</td>
</tr>
<tr>
<td>13</td>
<td><code>tb/agent/l1_cache_mem_driver.sv</code></td>
<td>reactive responder, <code>disable fork</code> 를 안 쓴 이유</td>
</tr>
<tr>
<td>14</td>
<td><code>tb/agent/l1_cache_reset_agent.sv</code> + SCB <code>write_rst()</code></td>
<td>리셋 동기화, poisoning</td>
</tr>
<tr>
<td>15</td>
<td><code>tb/env/l1_cache_coverage.sv</code>, <code>l1_cache_fsm_cov.sv</code></td>
<td>블랙박스 vs 화이트박스(<code>bind</code>)</td>
</tr>
<tr>
<td>16</td>
<td><code>tb/tests/l1_cache_base_test.sv</code> + <code>l1_cache_config.sv</code></td>
<td>config 객체로 11개 시나리오 만드는 법</td>
</tr>
</tbody></table>
<hr>
<h1 id="part-2-학습-내용-정리-⭐">Part 2. 학습 내용 정리 ⭐</h1>
<h2 id="21-캐시-기본기--이-dut-기준으로">2.1 캐시 기본기 — 이 DUT 기준으로</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>이 프로젝트</th>
<th>대안과 트레이드오프</th>
</tr>
</thead>
<tbody><tr>
<td>구조</td>
<td><strong>4-way set associative</strong></td>
<td>direct-mapped: 싸지만 같은 index 두 주소가 영구 thrash / full-assoc: 비교기 N개, 면적·타이밍 폭발</td>
</tr>
<tr>
<td>크기</td>
<td>64 set × 4 way × 16 B = <strong>4 KB</strong></td>
<td></td>
</tr>
<tr>
<td>쓰기 정책</td>
<td><strong>write-back</strong></td>
<td>write-through: 매 store마다 메모리 트래픽. write-back은 dirty bit + eviction 로직이 필요한 대신 대역폭을 아낌</td>
</tr>
<tr>
<td>할당 정책</td>
<td><strong>write-allocate</strong></td>
<td>no-write-allocate: write miss 시 라인을 안 가져옴. 이 DUT는 store도 miss면 라인을 fetch한 뒤 쓴다</td>
</tr>
<tr>
<td>교체 정책</td>
<td><strong>tree-PLRU (3 bit/set)</strong></td>
<td>true LRU 4-way는 순서 정보에 log2(4!)≈5 bit. PLRU는 3 bit로 근사</td>
</tr>
<tr>
<td>쓰기 단위</td>
<td><strong>byte enable</strong> <code>be[3:0]</code></td>
<td></td>
</tr>
</tbody></table>
<p><strong>3C miss 분류</strong> — 스코어보드가 실제로 이 분류를 만들어 커버리지로 보낸다
(<code>l1_cache_item::victim_e</code>):</p>
<table>
<thead>
<tr>
<th>bin</th>
<th>의미</th>
<th>언제</th>
</tr>
</thead>
<tbody><tr>
<td><code>VICTIM_INVALID</code></td>
<td><strong>Compulsory(cold)</strong></td>
<td>그 way를 한 번도 안 썼다</td>
</tr>
<tr>
<td><code>VICTIM_CLEAN</code></td>
<td><strong>Conflict/Capacity, write-back 없음</strong></td>
<td>victim이 valid하지만 clean</td>
</tr>
<tr>
<td><code>VICTIM_DIRTY</code></td>
<td><strong>Conflict/Capacity + write-back</strong></td>
<td>victim이 dirty → 메모리로 4 beat 내보냄</td>
</tr>
<tr>
<td><code>VICTIM_NONE</code></td>
<td>hit</td>
<td>아무것도 교체 안 함</td>
</tr>
</tbody></table>
<blockquote>
<p>이걸 핀에서는 못 본다. 그래서 커버리지를 <strong>모니터가 아니라 스코어보드에서</strong> 샘플한다.</p>
</blockquote>
<hr>
<h2 id="22-주소-분할--반드시-손으로-계산">2.2 주소 분할 — 반드시 손으로 계산</h2>
<pre><code>addr[31:10] = TAG   (22b)   → 4-way 비교 대상
addr[ 9: 4] = SET   ( 6b)   → 64 set 선택
addr[ 3: 2] = WORD  ( 2b)   → 라인 안 4워드 중 선택
addr[ 1: 0] = BYTE  ( 2b)   → 미사용 (be[3:0] 가 바이트 선택)</code></pre><p>정의는 <code>rtl/l1_cache_pkg.sv:74~97</code> 한 곳뿐이고 <strong>RTL과 TB가 같은 함수를 쓴다</strong>.
스코어보드가 <code>get_set()</code> 을 직접 호출한다(<code>l1_cache_scoreboard.sv:123</code>) — 주소 분할이
어긋날 수 없는 구조다.</p>
<h3 id="1-먼저-왜-쪼개는지--사물함-비유">1. 먼저 &quot;왜&quot; 쪼개는지 — 사물함 비유</h3>
<p>캐시를 사물함이라고 생각하세요.</p>
<pre><code>캐시 = 사물함 64칸  (칸 = set)
칸 하나 = 서랍 4개   (서랍 = way)
서랍 하나 = 물건 4개  (물건 = word)
CPU가 &quot;0x1234 주소 줘&quot;라고 하면 캐시는 이렇게 움직입니다:</code></pre><table>
<thead>
<tr>
<th>순서</th>
<th>하는 일</th>
<th>필요한 정보</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>몇 번 칸으로 갈지 정한다</td>
<td>set</td>
</tr>
<tr>
<td>2</td>
<td>그 칸의 서랍 4개를 열고 이름표가 맞는지 본다</td>
<td>tag</td>
</tr>
<tr>
<td>3</td>
<td>맞는 서랍에서 몇 번째 물건을 꺼낸다</td>
<td>word</td>
</tr>
</tbody></table>
<p>주소 하나에 이 세 정보가 다 들어있어야 하죠. 그래서 32비트를 잘라 쓰는 겁니다.</p>
<pre><code>addr[31:10] = TAG   ← 이름표 (서랍이 진짜 내 것 맞나?)
addr[ 9: 4] = SET   ← 몇 번 칸?
addr[ 3: 2] = WORD  ← 서랍 안 몇 번째 물건?
addr[ 1: 0] = BYTE  ← 안 씀</code></pre><h3 id="2-계산-3단계">2. 계산 3단계</h3>
<p>십진수 변환 필요 없습니다. hex 마지막 한 글자가 열쇠예요.</p>
<pre><code>┌─────────────────────────────────────────────┐
│ ① 마지막 hex 글자 → ÷4 하면 word 번호            │
│                                              │
│ ② 마지막 글자를 뗀 나머지 → 64로 나눈다             │
│      몫    = tag                             │
│      나머지 = set                             │
│                                             │
│ ③ 마지막 글자를 0으로 → line base 주소           │
└─────────────────────────────────────────────┘</code></pre><p>왜 이렇게 되나 (한 번만 이해하면 됨)
hex 한 글자 = 4비트입니다. 그런데 word(2비트) + byte(2비트) = 4비트
→ 마지막 hex 글자 하나가 딱 word+byte예요. 그래서 떼어내면 됩니다.
마지막 글자를 떼면 남는 건 {tag, set}이 붙어있는 수입니다.
set이 6비트 = 64가지니까, 64로 나누면 나머지가 set, 몫이 tag입니다.</p>
<h3 id="3-0x0000_1234-직접-해보기">3. 0x0000_1234 직접 해보기</h3>
<p>① word 구하기</p>
<pre><code>0x1234  →  마지막 글자 = 4
           4 ÷ 4 = 1
                        → word 1</code></pre><p>② tag / set 구하기</p>
<pre><code>0x1234  →  마지막 글자 떼면  0x123

0x123 을 십진수로:  1×256 + 2×16 + 3 = 291

291 ÷ 64 = 4 … 나머지 35
           ↑         ↑
          tag       set</code></pre><p>③ line base 주소</p>
<pre><code>0x1234  →  마지막 글자를 0으로  →  0x1230</code></pre><p>정리해서 읽으면
35번 칸으로 가서, 서랍의 이름표가 4인지 확인하고,
맞으면 그 서랍의 1번 물건을 꺼낸다.
없으면(miss) 메모리에 0x1230부터 4개를 달라고 한다.</p>
<h3 id="4-연습-2--같은-라인인가">4. 연습 2 — &quot;같은 라인인가?&quot;</h3>
<p>이건 훨씬 쉬워요. hex 마지막 한 글자만 빼고 같으면 같은 라인입니다.</p>
<pre><code>0x1230  →  앞부분 &quot;123&quot;
0x1234  →  앞부분 &quot;123&quot;   ← 같다! → 같은 라인 ✅
0x1238  →  앞부분 &quot;123&quot;   ← 같다!
0x123C  →  앞부분 &quot;123&quot;   ← 같다!

0x1240  →  앞부분 &quot;124&quot;   ← 다르다 → 다른 라인 ❌</code></pre><p>한 라인 = 주소 16개(4워드)</p>
<pre><code>0x1230  word 0  ┐
0x1234  word 1  │  이 4개가 한 덩어리 (16 B)
0x1238  word 2  │  메모리에서 한 번에 4 beat로 가져옴
0x123C  word 3  ┘</code></pre><p>그래서 l1_cache_line_seq가 하는 일:</p>
<pre><code>0x1234 읽기  →  miss!  →  0x1230,34,38,3C 4개 전부 가져옴
0x1230 읽기  →  hit 여야 함  ✅
0x1238 읽기  →  hit 여야 함  ✅
0x123C 읽기  →  hit 여야 함  ✅</code></pre><blockquote>
<p>hit: 찾는게 캐시에 있었다
miss: 찾는게 캐시에 없었다 -&gt; 메모리 가서 가져옴
miss는 에러가 아니라 그냥 &quot;아직 없음&quot; 상태예요. 처음 접근하는 주소는 당연히 miss입니다 (아무것도 없는 빈 캐시니까). 캐시는 &quot;두 번째부터 빨라지는&quot; 장치입니다.</p>
</blockquote>
<p>miss 났을 때 캐시가 &quot;요청한 워드 1개&quot;만 가져온다고 생각하고 계실 거예요.
실제로는 4개를 통째로 가져옵니다.</p>
<p>한 번 miss 났으면 나머지 3개는 무조건 hit여야 합니다. 여기서 miss가 나면 → burst로 가져온 4개가 서랍 안 제자리에 안 들어갔다는 뜻이에요.</p>
<p> 핵심: 가지러 갈 때 4개를 들고 온다
사물함 비유로 다시 갈게요.</p>
<pre><code>[1] CPU: &quot;0x1234 줘&quot;
    35번 칸 열어봄 → 이름표 4인 서랍 없음 → MISS ❌
    창고(메모리)로 감
      ↓
    ⚠️ 여기가 포인트: 물건 1개만 안 가져온다
       &quot;0x1230부터 4개 다 줘&quot;  ← 4-beat burst
      ↓
    beat 0 → 0x1230 의 값
    beat 1 → 0x1234 의 값
    beat 2 → 0x1238 의 값
    beat 3 → 0x123C 의 값
      ↓
    서랍에 4개 다 넣고 이름표 &quot;4&quot; 붙임
    서랍 상태:  [ 0x1230 | 0x1234 | 0x1238 | 0x123C ]
                  word0    word1    word2    word3
    CPU한테는 0x1234 값만 돌려줌</code></pre><p>이제 그 다음:</p>
<pre><code>[2] CPU: &quot;0x1230 줘&quot;  →  35번 칸, 이름표 4, word0 → 이미 있음 → HIT ✅
[3] CPU: &quot;0x1238 줘&quot;  →                    word2 → 이미 있음 → HIT ✅
[4] CPU: &quot;0x123C 줘&quot;  →                    word3 → 이미 있음 → HIT ✅
한 번 창고에 갔을 때 4개를 다 들고 왔으니까, 나머지 3개는 갈 필요가 없는 겁니다.</code></pre><p>0x1240은?</p>
<pre><code>0x1240  →  마지막 글자 떼면  0x124
           0x124 = 1×256 + 2×16 + 4 = 292
           292 ÷ 64 = 4 … 나머지 36
                              ↑
                          set 36  (0x1234는 set 35)
→ 다른 칸입니다. 서로 아무 상관 없는 라인이에요.</code></pre><h3 id="5-직접-풀어보기">5. 직접 풀어보기</h3>
<p>문제 1. 0x0000_2008 을 분해하라</p>
<details><summary>정답</summary>

<pre><code>① 마지막 글자 8  →  8 ÷ 4 = 2        →  word 2
② 떼면 0x200 = 2×256 = 512
   512 ÷ 64 = 8 … 나머지 0           →  tag 8, set 0
③ line base = 0x2000
→ 0번 칸에서 이름표 8인 서랍의 2번 물건</code></pre></details>

<p>문제 2. 0x0000_00FC 를 분해하라</p>
<details><summary>정답</summary>

<p>① 마지막 글자 C(=12)  →  12 ÷ 4 = 3  →  word 3
② 떼면 0x00F = 15
   15 ÷ 64 = 0 … 나머지 15           →  tag 0, set 15
③ line base = 0x00F0</p>
</details>

<p>문제 3. ⭐ 0x0000_1234 와 0x0000_5234 는 같은 칸(set)인가?</p>
<details><summary>정답</summary>

<p>0x1234  →  0x123 = 291  →  291 ÷ 64 = 4  … 35   →  tag 4,  set 35
0x5234  →  0x523 = 1315 →  1315 ÷ 64 = 20 … 35  →  tag 20, set 35
→ 같은 set(35), 다른 tag!</p>
<p>이게 바로 conflict 상황입니다. 같은 칸에 서로 다른 두 이름표가 들어가려는 거죠.</p>
<p>direct-mapped(서랍 1개)라면 → 둘이 번갈아 오면 매번 서로를 쫓아냄 (영구 thrash 💀)
4-way(서랍 4개)라면 → 서랍 2개를 나눠 쓰면 됨 → 둘 다 hit ✅
&quot;4-way set-associative를 왜 쓰나&quot;의 답이 바로 이 문제입니다.</p>
</details>

<ol start="6">
<li><p>한 장 요약 (외울 것)</p>
<pre><code>addr = 0x . . . . . . . X
                     ↑
              마지막 hex 한 글자

X ÷ 4        = word 번호  (0,4,8,C → 0,1,2,3)
X를 0으로     = line base 주소
X를 뗀 값 ÷64 = 몫이 tag, 나머지가 set
앞부분이 같으면 → 같은 라인
set이 같고 tag가 다르면 → conflict (way를 나눠 씀)</code></pre></li>
</ol>
<hr>
<h2 id="23-registered-sram--2-stage-파이프라인-⭐">2.3 registered SRAM + 2-stage 파이프라인 ⭐</h2>
<p>이 설계의 <strong>모든 것이 여기서 출발</strong>한다.</p>
<pre><code class="language-systemverilog">// rtl/sram_macro.sv:30
always_ff @(posedge clk) begin
    rdata &lt;= mem[raddr];      // 이 엣지의 write보다 &quot;먼저&quot; 샘플됨
    if (we) mem[waddr] &lt;= wdata;
end</code></pre>
<p>읽기 포트가 <strong>registered</strong> = N 사이클에 주소를 주면 N+1 사이클에 데이터가 나온다.
→ 파이프라인이 자연스럽게 2단이 된다:</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td><strong>S1</strong></td>
<td>CPU 요청 접수. set index 를 4개 way의 tag/data 배열에 <strong>동시</strong> 인가</td>
</tr>
<tr>
<td><strong>S2</strong></td>
<td>배열 데이터 도착 → 4-way tag 비교 → hit/way 결정 → way mux → rdata</td>
</tr>
</tbody></table>
<p><strong>read hit 연속 2건 타이밍</strong></p>
<table>
<thead>
<tr>
<th>cycle</th>
<th>S1</th>
<th>S2</th>
<th>출력</th>
</tr>
</thead>
<tbody><tr>
<td>N</td>
<td>A 접수, 배열 주소 인가</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>N+1</td>
<td>B 접수</td>
<td>A 판정 (hit)</td>
<td><code>rsp_valid</code>, A 데이터</td>
</tr>
<tr>
<td>N+2</td>
<td>C 접수</td>
<td>B 판정 (hit)</td>
<td><code>rsp_valid</code>, B 데이터</td>
</tr>
</tbody></table>
<p>→ <strong>hit throughput = 1 요청/사이클</strong>. latency는 2 사이클이지만 파이프라이닝되어 있다.</p>
<h3 id="왜-조합형-read면-안-되나-⭐-단골-질문">왜 조합형 read면 안 되나 (⭐ 단골 질문)</h3>
<p><code>assign rdata = mem[raddr]</code> 로 하면 배열 주소 mux가 <code>pipeline_stall</code> 로 선택되므로:</p>
<pre><code>pipeline_stall → tag_raddr → tag_rdata → cache_hit → pipeline_stall</code></pre><p><strong>닫힌 조합 루프</strong>가 된다. registered read면 <code>tag_rdata</code> 가 플롭 출력이라 루프가 끊긴다.
(README &quot;History&quot; 의 첫 번째 항목이 바로 이 버그다.)</p>
<h3 id="배열-주소-mux-l1_cache_coresv136149">배열 주소 mux (<code>l1_cache_core.sv:136~149</code>)</h3>
<p>세 가지 경우를 구분한다:</p>
<pre><code class="language-systemverilog">if (!pipeline_stall &amp;&amp; cpu_req_valid)  // 정상: S1 주소
else if (state == ST_WB_READ)          // victim 라인을 워드 단위로 훑는다
else                                    // stall 중 or S1 idle: S2 주소를 유지</code></pre>
<p>세 번째 분기가 중요하다. 드라이버는 idle일 때 버스에 <strong>X를 park</strong>하므로
(<code>l1_cache_cpu_driver.sv::drive_idle</code>), 그대로 인덱싱하면 배열이 X로 접근된다.</p>
<hr>
<h2 id="24-read-during-write-포워딩-⭐">2.4 read-during-write 포워딩 ⭐</h2>
<p>registered read의 대가. 배열 read 포트는 <strong>같은 엣지의 write를 못 본다.</strong></p>
<blockquote>
<p>사이클 N: <code>write(addr=A, data=D)</code> 와 <code>read(addr=A)</code> 가 동시에 걸림
→ 사이클 N+1 에 나오는 <code>rdata</code> 는 <strong>D 이전의 옛날 값</strong></p>
</blockquote>
<p>그래서 코어가 배열 바깥에서 우회시킨다 (<code>l1_cache_core.sv:175~210</code>):</p>
<pre><code class="language-systemverilog">// 직전 사이클의 write 주소 vs 직전 사이클에 &quot;실제로 발행한&quot; read 주소
safe_data_rdata[w][b*8 +: 8] =
    (prev_data_we[w] &amp;&amp; (prev_data_waddr == prev_data_raddr) &amp;&amp; prev_data_wbe[b])
    ? prev_data_wdata[b*8 +: 8]
    : data_rdata[w][b*8 +: 8];</code></pre>
<p><strong>꼭 짚어야 할 두 가지:</strong></p>
<ol>
<li><strong>비교 대상이 <code>s2_*</code> 가 아니라 <code>prev_*_raddr</code></strong> — <code>ST_WB_READ</code> 중에는 read 주소가
라인을 훑고 있어서 <code>s2_word</code> 가 아니다. &quot;지난 사이클에 실제로 뭘 읽으라고 했나&quot;가 정답.</li>
<li><strong>data는 byte 단위 포워딩</strong> — store가 일부 lane만 썼을 수 있다. 워드 통째로 포워딩하면
안 건드린 lane까지 덮어쓴다. → 이걸 잡는 게 <code>l1_cache_be_seq</code> + <code>l1_cache_be_test</code>
(idle gap 0 으로 돌려서 부분 write 바로 다음 사이클에 read가 오게 만든다).</li>
<li><strong>1 사이클 깊이면 충분</strong> — 그 다음 사이클부터는 배열 자체가 새 값을 갖고 있다.</li>
</ol>
<h3 id="✍️-연습-3">✍️ 연습 3</h3>
<p><code>0x100</code> 에 <code>be=4&#39;b0011</code> 로 <code>0xAAAA_BBBB</code> 를 쓰고, <strong>바로 다음 사이클</strong> 같은 주소를 읽는다.
원래 값이 <code>0x1111_2222</code> 였다면 정답은? 포워딩이 워드 단위였다면 뭐가 나오나?</p>
<details><summary>정답</summary>
정답 `0x1111_BBBB` (하위 2바이트만 교체).
워드 단위 포워딩이면 `0xAAAA_BBBB` 가 나와서 상위 2바이트가 오염된다.
byte enable을 무시하는 데이터 배열도 같은 증상이라, `be_seq` 는 두 버그를 동시에 잡는다.
</details>

<hr>
<h2 id="25-miss-fsm--사이클을-세어라">2.5 miss FSM — 사이클을 세어라</h2>
<pre><code>ST_IDLE ──miss, clean victim───────────────────────────▶ ST_FILL_REQ
   │                                                          │
   └─miss, dirty victim─▶ ST_WB_READ ─▶ ST_WB_SEND ───────────┘
                          (버퍼로 복사)   (메모리로 전송)        │
                                                               ▼
   ST_IDLE ◀──────────────────────── ST_COMPLETE ◀────── ST_FILL_RCV</code></pre><p>각 상태는 <strong>정확히 하나의 일만</strong> 한다. 상태별 사이클 수:</p>
<table>
<thead>
<tr>
<th>상태</th>
<th>사이클</th>
<th>근거</th>
</tr>
</thead>
<tbody><tr>
<td><code>ST_IDLE</code></td>
<td>1</td>
<td>miss 감지 + victim latch (<code>victim_way</code>, <code>victim_tag</code>)</td>
</tr>
<tr>
<td><code>ST_WB_READ</code></td>
<td><strong>5</strong></td>
<td><code>wb_cnt</code> 0→4. 주소는 N에, 데이터는 N+1에 오므로 캡처가 한 박자 늦다</td>
</tr>
<tr>
<td><code>ST_WB_SEND</code></td>
<td>1 + 4</td>
<td>req 수락 1 + 4 beat (backpressure 있으면 더)</td>
</tr>
<tr>
<td><code>ST_FILL_REQ</code></td>
<td>≥1</td>
<td><code>mem_req_ready</code> 대기</td>
</tr>
<tr>
<td><code>ST_FILL_RCV</code></td>
<td>latency + 4</td>
<td>responder가 랜덤 latency 후 4 beat</td>
</tr>
<tr>
<td><code>ST_COMPLETE</code></td>
<td>1</td>
<td>tag/valid/dirty/PLRU 커밋 + CPU 응답</td>
</tr>
</tbody></table>
<p><strong>ST_WB_READ가 왜 4가 아니라 5인가</strong> — <code>l1_cache_core.sv:307~312</code>:</p>
<table>
<thead>
<tr>
<th>사이클</th>
<th><code>wb_cnt</code></th>
<th>인가하는 주소</th>
<th>캡처하는 데이터</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>0</td>
<td>word 0</td>
<td>—</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>word 1</td>
<td><code>wb_buf[0]</code></td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td>word 2</td>
<td><code>wb_buf[1]</code></td>
</tr>
<tr>
<td>4</td>
<td>3</td>
<td>word 3</td>
<td><code>wb_buf[2]</code></td>
</tr>
<tr>
<td>5</td>
<td>4</td>
<td>— (전이)</td>
<td><code>wb_buf[3]</code></td>
</tr>
</tbody></table>
<p>registered read의 한 박자 지연이 <strong>정확히 한 사이클의 오버헤드</strong>로 드러난 자리다.</p>
<h3 id="✍️-연습-4--dirty-miss-총-사이클">✍️ 연습 4 — dirty miss 총 사이클</h3>
<p>memory latency = 3, backpressure 없음일 때 miss 감지부터 CPU 응답까지 몇 사이클?</p>
<details><summary>정답</summary>
1(IDLE) + 5(WB_READ) + 5(WB_SEND) + 1(FILL_REQ) + 7(FILL_RCV: 3+4) + 1(COMPLETE) = **20 사이클**
clean victim이면 WB 두 상태(10사이클)가 빠져서 **10 사이클**.
→ "dirty eviction이 miss 비용을 2배로 만든다"를 숫자로 말할 수 있게 된다.
그래서 "Not done yet"에 **write buffer** 가 들어가 있는 것 (evict를 fill 앞에 직렬화하지 않기).
</details>

<h3 id="st_complete-는-왜-있나-⭐-단골-질문"><code>ST_COMPLETE</code> 는 왜 있나 (⭐ 단골 질문)</h3>
<p>miss 당 1 사이클 손해지만, <strong>마지막 fill beat 의 배열 write</strong> 와 <strong>대기 중이던 store의
배열 write</strong> 가 같은 사이클에 충돌하는 구조적 해저드를 없앤다. 그리고 tag/valid/dirty/PLRU
커밋 지점이 한 곳으로 모인다.</p>
<p><strong>여기서 stall을 푸는 이유</strong>(<code>l1_cache_core.sv:432~433</code>)도 중요하다.
<code>ST_COMPLETE</code> 에서 <code>pipeline_stall=0</code> 이라야 이 사이클 끝에 파이프라인이 전진한다.
안 그러면 다음 사이클 <code>ST_IDLE</code> 에서 S2가 <strong>같은 요청을 그대로 들고 있어서 두 번 응답</strong>한다.</p>
<h3 id="응답-조건식-l1_cache_coresv262265">응답 조건식 (<code>l1_cache_core.sv:262~265</code>)</h3>
<pre><code class="language-systemverilog">assign cpu_rsp_valid = ((state == ST_IDLE)     &amp;&amp; s2_valid &amp;&amp; cache_hit &amp;&amp; !s2_rw)
                    || ((state == ST_COMPLETE) &amp;&amp; s2_valid &amp;&amp;              !s2_rw);
assign cpu_rsp_rdata = (state == ST_COMPLETE) ? fill_data : safe_data_rdata[hit_way];</code></pre>
<p>읽어낼 것 3개:</p>
<ul>
<li><strong>write는 응답이 없다.</strong> read만 <code>rsp_valid</code> 를 받는다 → 모니터도 read만 큐에 넣는다.</li>
<li>miss 응답은 배열이 아니라 <code>fill_data</code> 에서 나온다 (fill 중에 요청 워드를 따로 잡아둠,
<code>l1_cache_core.sv:325</code>).</li>
<li><code>ST_COMPLETE</code> 에는 <code>cache_hit</code> 조건이 없다 — 방금 채운 라인이니 당연히 맞다.</li>
</ul>
<hr>
<h2 id="26-tree-plru">2.6 tree-PLRU</h2>
<p>3 bit로 4-way를 근사한다 (<code>l1_cache_pkg.sv:99~121</code>).</p>
<pre><code>bit[0] : root       0 → {way0,way1} 쪽이 오래됨,  1 → {way2,way3} 쪽
bit[1] : 왼쪽 leaf  (way0 vs way1)
bit[2] : 오른쪽 leaf (way2 vs way3)</code></pre><pre><code class="language-systemverilog">victim = plru[0] ? (plru[2] ? 3 : 2)
                 : (plru[1] ? 1 : 0);

// 방금 쓴 way의 &quot;반대쪽&quot;을 가리키게 갱신
nxt[0] = ~way[1];
if (way[1] == 0) nxt[1] = ~way[0];
else             nxt[2] = ~way[0];</code></pre>
<p><strong>victim 선택 순서</strong>: invalid way가 있으면 그쪽 먼저(<code>l1_cache_core.sv:245~255</code>),
전부 valid일 때만 PLRU 트리를 따른다. 스코어보드도 <strong>똑같은 순서</strong>로 예측한다
(<code>l1_cache_scoreboard.sv:150~153</code>) — 안 맞으면 매 miss마다 way가 어긋난다.</p>
<h3 id="✍️-연습-5--set이-꽉-찬-상태에서-4번-연속-miss-victim-순서는">✍️ 연습 5 — set이 꽉 찬 상태에서 4번 연속 miss. victim 순서는?</h3>
<details><summary>정답</summary>

<table>
<thead>
<tr>
<th>plru</th>
<th>victim</th>
<th>갱신 후</th>
</tr>
</thead>
<tbody><tr>
<td><code>3&#39;b000</code></td>
<td>way <strong>0</strong></td>
<td><code>3&#39;b011</code></td>
</tr>
<tr>
<td><code>3&#39;b011</code></td>
<td>way <strong>2</strong></td>
<td><code>3&#39;b110</code></td>
</tr>
<tr>
<td><code>3&#39;b110</code></td>
<td>way <strong>1</strong></td>
<td><code>3&#39;b101</code></td>
</tr>
<tr>
<td><code>3&#39;b101</code></td>
<td>way <strong>3</strong></td>
<td><code>3&#39;b000</code></td>
</tr>
</tbody></table>
<p>→ <strong>0, 2, 1, 3</strong> 순환. 4개 way를 골고루 쓴다.
<code>l1_cache_eviction_seq</code> 는 한 set에 <strong>6개 tag</strong>(&gt; 4 way)를 돌려서 이 순환을 계속 강제하고,
커버리지 <code>cp_way</code> / <code>cp_victim_way</code> 가 4개 way를 다 봤는지 확인한다.
PLRU가 고장나서 항상 같은 way만 고르면 여기서 커버리지가 25%에서 멈춘다.</p>
</details>

<hr>
<h2 id="27-요청-하나의-일생">2.7 요청 하나의 일생</h2>
<pre><code>seq          item.randomize()  →  start_item / finish_item
  ↓ (TLM)
sequencer    ← driver.seq_item_port.get_next_item()
  ↓
driver       cb_cpu 로 valid/rw/addr/be/wdata 구동 → ready 대기 → item_done()
  ↓ (핀)
DUT          S1 (배열 주소 인가) → S2 (hit 판정) → hit면 즉시 응답 / miss면 FSM
  ↓ (핀)
monitor      cb_cpu_mon 으로 preponed 샘플
             ├─ req_ap : valid &amp;&amp; ready 인 순간 = 수락된 요청 (read+write, 발행 순서)
             └─ rsp_ap : rsp_valid 인 순간 = read 응답 + 실제 rdata
  ↓ (analysis port, 1→N)
scoreboard   write_req(): 골든 모델 갱신 + 기대값 스냅샷 + 기대 버스트 push
             write_rsp(): 스냅샷과 실제 rdata 비교
             write_mem(): 관측된 버스트를 기대 큐와 순서대로 대조
  ↓ cov_ap
coverage     hit/way/victim 이 채워진 item 을 샘플</code></pre><p><strong>드라이버의 진짜 back-to-back</strong> (<code>l1_cache_cpu_driver.sv</code>):
gap이 0이면 <code>item_done()</code> 직후 <strong>버스를 놓지 않고</strong> <code>try_next_item()</code> 으로 다음 아이템을
받아 그 자리에서 payload만 교체한다. 그래서 <code>cpu_req_valid</code> 가 accept 사이에 안 떨어진다.
매 아이템마다 VALID를 내리면 &quot;back-to-back 테스트&quot;가 이름만 back-to-back이 된다
(README History 에 실제로 있던 버그).</p>
<hr>
<h2 id="28-스코어보드--골든-캐시-모델-⭐⭐">2.8 스코어보드 = 골든 <strong>캐시</strong> 모델 ⭐⭐</h2>
<p>이 프로젝트의 심장. 메모리만 모델링하는 흔한 스코어보드와 다르다.</p>
<pre><code class="language-systemverilog">// l1_cache_scoreboard.sv:55~62 — DUT 내부를 통째로 미러링
tag_t      m_tag   [SETS][WAYS];
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];
bit [31:0] main_mem [int];    // 예측된 DRAM</code></pre>
<p><code>write_req()</code> 하나가 요청 1건에 대해 <strong>전부</strong> 예측한다:</p>
<table>
<thead>
<tr>
<th>예측 항목</th>
<th>코드</th>
</tr>
</thead>
<tbody><tr>
<td>hit/miss + <strong>어느 way</strong></td>
<td><code>:137~140</code></td>
</tr>
<tr>
<td>victim 종류 (cold/clean/dirty)</td>
<td><code>:155~157</code></td>
</tr>
<tr>
<td>dirty면 write-back 주소 + 4 beat</td>
<td><code>:160~171</code></td>
</tr>
<tr>
<td>뒤따르는 fill burst + 그 데이터</td>
<td><code>:174~181</code></td>
</tr>
<tr>
<td>read가 돌려줘야 할 값 (byte 정확)</td>
<td><code>:200~203</code></td>
</tr>
<tr>
<td>PLRU 갱신</td>
<td><code>:207</code></td>
</tr>
</tbody></table>
<p>그리고 <code>write_mem()</code> 이 관측된 버스트를 <code>exp_mem_q</code> 와 <strong>순서대로</strong> 대조한다.
→ 없어야 할 버스트, 있어야 하는데 없는 버스트, 주소·방향·길이·데이터 오류가 전부 잡힌다.</p>
<p><strong>메모리 모델이 두 벌인 이유</strong>: <code>mem_driver.main_mem</code>(실제 응답값)과
<code>scoreboard.main_mem</code>(예측값)이 따로 있다. 둘이 같은 소스를 공유하면 서로를 검증하지 못한다.
&quot;아직 아무도 안 쓴 주소&quot;의 기본값만 <code>tb_classes.svh</code> 의 <code>L1_MEM_DEFAULT(a)</code> 매크로로 공유하는데,
<strong>주소 의존적인 값</strong>(<code>32&#39;hDEAD_0000 | addr[15:0]</code>)인 게 포인트다. 상수였다면 fill burst의
4 beat가 전부 똑같아서 <strong>워드 선택 버그가 안 보인다.</strong></p>
<p><strong><code>check_phase</code></strong> (<code>:308~317</code>) — 조용한 PASS 방지장치:
트래픽 0건 / 응답 못 받은 read / 예측했는데 안 일어난 버스트 / mismatch 누적 → 전부 실패.</p>
<hr>
<h2 id="29-순서-문제-analysis-port가-왜-2개인가-⭐">2.9 순서 문제: analysis port가 왜 2개인가 ⭐</h2>
<p><strong>증상이었던 것</strong>: DUT가 파이프라인이라 <strong>read miss가 retire 되는 바로 그 사이클에
다음 요청이 수락</strong>될 수 있다. 모니터를 &quot;write는 accept 때, read는 응답 때&quot; 한 포트로
보내면, 이 겹침에서 write가 스코어보드에 먼저 도착해 <strong>이미 전진해버린 모델</strong>과
read를 비교한다 → 매 런마다 가짜 실패 2건.</p>
<p><strong>해결</strong>:</p>
<table>
<thead>
<tr>
<th>포트</th>
<th>언제</th>
<th>무엇</th>
</tr>
</thead>
<tbody><tr>
<td><code>req_ap</code></td>
<td><code>valid &amp;&amp; ready</code> 인 사이클</td>
<td><strong>모든</strong> 수락된 요청 (read + write), <strong>발행 순서 그대로</strong></td>
</tr>
<tr>
<td><code>rsp_ap</code></td>
<td><code>rsp_valid</code> 인 사이클</td>
<td>read 응답 + DUT가 실제 낸 <code>rdata</code></td>
</tr>
</tbody></table>
<p>read의 <strong>기대값은 발행 시점에 스냅샷</strong>(<code>rd_q</code> 에 push)해두고, 응답이 오면 그 스냅샷과 비교한다.
모든 요청이 발행 순서로 <code>req_ap</code> 를 지나가므로, 시뮬레이터가 같은 타임스텝 안에서
두 모니터 스레드를 어떤 순서로 스케줄하든 <strong>프로그램 순서가 보존</strong>된다.</p>
<blockquote>
<p>이 구조는 <strong>in-order 응답</strong>을 전제로 한다. 그래서 README &quot;Not done yet&quot; 의 MSHR
(hit-under-miss)이 &quot;RTL만이 아니라 TB 재설계&quot;라고 적혀 있는 것 — 응답이 순서를
벗어나면 큐 매칭 자체가 성립하지 않고 response ID가 필요해진다.
<strong>&quot;왜 MSHR 안 했나&quot;에 이렇게 답하면 만점이다.</strong></p>
</blockquote>
<hr>
<h2 id="210-메모리-responder--reactive-agent">2.10 메모리 responder — reactive agent</h2>
<p><code>l1_cache_mem_driver</code> 는 stimulus를 만드는 게 아니라 <strong>DUT의 요청에 반응</strong>한다.
시퀀스(<code>l1_cache_mem_rsp_seq</code>)는 데이터가 아니라 <strong>타이밍</strong>을 공급한다:</p>
<table>
<thead>
<tr>
<th>노브</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>latency</code></td>
<td>요청 수락 → 첫 fill beat 까지</td>
</tr>
<tr>
<td><code>ready_stall</code></td>
<td>request 채널 backpressure (몇 % 의 버스트에, 최대 몇 사이클)</td>
</tr>
<tr>
<td><code>beat_gap</code></td>
<td>beat 사이 idle 삽입</td>
</tr>
</tbody></table>
<p><code>l1_cache_stress_test</code> 는 latency 10~50, 60% 의 버스트에 최대 6사이클 backpressure,
양쪽 채널 모두에 gap — 이 조건에서 통과해야 handshake를 진짜로 지킨 것이다.</p>
<h3 id="disable-fork-를-안-쓴-이유-⭐"><code>disable fork</code> 를 안 쓴 이유 ⭐</h3>
<p>리셋으로 전송을 끊어야 하는데, <code>get_next_item()</code> 과 <code>item_done()</code> 사이에서
<code>disable fork</code> 를 쓰면 <strong>시퀀서가 내부적으로 spawn한 프로세스까지</strong> 죽는다.
→ 나중에 &quot;Get_next_item called twice&quot; / &quot;item_done() with no outstanding requests&quot; 로 터진다.</p>
<p>대신 모든 대기 지점을 <code>tick()</code> 한 곳으로 모으고 <code>aborted</code> 플래그를 검사한다:</p>
<pre><code class="language-systemverilog">virtual task tick();
    @(vif.cb_mem);
    if (vif.rst_n !== 1&#39;b1) aborted = 1&#39;b1;
endtask</code></pre>
<p>이렇게 하면 리셋이 어느 지점에서 끊든 <strong>get/item_done 쌍은 항상 균형</strong>을 유지한다.</p>
<hr>
<h2 id="211-리셋-검증과-poisoning">2.11 리셋 검증과 poisoning</h2>
<p>리셋 에이전트가 <code>rst_n</code> 을 <strong>소유</strong>한다 (tb_top의 initial 블록이 아니라).
그래야 테스트가 트래픽 중간에 리셋을 다시 걸 수 있다.</p>
<p><strong>동기화 포인트</strong>: 리셋 드라이버는 <code>vif.assert_reset()</code> 을 부르기 <strong>직전</strong>에
analysis port로 스코어보드에 알린다 (<code>l1_cache_reset_agent.sv</code>). 스코어보드가 모델을
DUT와 <strong>같은 순간</strong>에 flush 하도록.</p>
<table>
<thead>
<tr>
<th>플레이버</th>
<th>동작</th>
<th>검사</th>
</tr>
</thead>
<tbody><tr>
<td><code>l1_cache_reset_test</code> (quiesced)</td>
<td>메모리 버스가 idle 될 때까지 기다렸다 인가</td>
<td><strong>전부 살아있음</strong></td>
</tr>
<tr>
<td><code>l1_cache_reset_async_test</code></td>
<td>버스트 중간이든 상관없이 인가</td>
<td>write-back이 반쪽만 나간 <strong>그 라인만</strong> 데이터 검사 제외</td>
</tr>
</tbody></table>
<p><strong>poisoning</strong> (<code>l1_cache_scoreboard.sv:101~102</code>): 리셋 시점에 <code>exp_mem_q</code> 에 남아있던
write-back 예측의 주소를 <code>poisoned[]</code> 에 표시한다. 그 라인의 DRAM 내용은 모델이 재구성할
수 없기 때문. <strong>데이터 비교만</strong> 건너뛰고 프로토콜·순서·burst 길이 검사는 전부 유지된다.
리셋당 최대 1라인, 개수는 report에 찍힌다.</p>
<blockquote>
<p>&quot;검사를 껐다&quot;가 아니라 <strong>&quot;모델이 원리적으로 알 수 없는 부분만, 범위를 한정해서,
개수를 보고하면서 껐다&quot;</strong> — 이 구분을 설명할 수 있으면 검증 엔지니어답게 들린다.</p>
</blockquote>
<hr>
<h2 id="212-커버리지-2종">2.12 커버리지 2종</h2>
<table>
<thead>
<tr>
<th></th>
<th><code>cache_ops</code></th>
<th><code>cache_fsm</code></th>
</tr>
</thead>
<tbody><tr>
<td>어디</td>
<td><code>tb/env/l1_cache_coverage.sv</code> (subscriber)</td>
<td><code>tb/env/l1_cache_fsm_cov.sv</code> (<strong><code>bind</code> into core</strong>)</td>
</tr>
<tr>
<td>소스</td>
<td><strong>스코어보드</strong> 의 annotated item</td>
<td>코어 <strong>내부 신호</strong></td>
</tr>
<tr>
<td>무엇</td>
<td>rw · hit · evict · victim종류 · way · word · set · be + cross 6종</td>
<td>state · 합법 전이 7종 · <code>illegal_bins</code> 12종 · stall 길이 · backpressure · fill beat · victim/hit way</td>
</tr>
</tbody></table>
<p><strong>왜 스코어보드에서 샘플하나</strong> — hit/miss, 어느 way, victim이 dirty였는지는 <strong>핀에 없다.</strong>
골든 모델 안에만 존재한다. 모니터에서 샘플하려면 커버리지 코드가 캐시를 또 모델링해야 한다.</p>
<p><strong>왜 <code>bind</code> 인가</strong> — FSM state, <code>victim_way</code>, <code>pipeline_stall</code> 도 핀에 없다.
<code>bind</code> 는 RTL을 손대지 않고 검증 모듈을 계층 안으로 주입한다 (<code>tb_top.sv:87</code>).
합성 대상 코드에 검증용 코드가 섞이지 않는다.</p>
<p><strong><code>illegal_bins</code> vs <code>ignore_bins</code></strong>:</p>
<ul>
<li><code>illegal_bins</code> = &quot;일어나면 <strong>에러</strong>&quot; — FSM의 불법 전이 12종</li>
<li><code>ignore_bins</code> = &quot;<strong>도달 불가</strong>하니 분모에서 빼자&quot; — 예: <code>x_rw_evict</code> 에서 <code>no_evict</code> 쪽</li>
</ul>
<p><strong>커버리지 100%의 의미</strong>: &quot;그 시나리오를 <strong>자극했다</strong>&quot;이지 &quot;<strong>맞다</strong>&quot;가 아니다.
정답 판정은 스코어보드와 SVA가 한다. 그리고 커버리지 모델 자체가 빠뜨린 구멍은
커버리지로 못 찾는다.</p>
<p><strong>per-test 수치가 낮은 건 정상</strong> — 각 테스트가 좁은 시나리오를 노린다.
<code>regress.bat</code> 가 11테스트 × 4시드를 돌려 <code>xcrg</code> 로 <strong>머지</strong>한 값이 100%다.</p>
<hr>
<h2 id="213-sva--스코어보드와-역할이-다르다">2.13 SVA — 스코어보드와 역할이 다르다</h2>
<p><code>tb/l1_cache_if.sv:83~241</code> 에 어서션 15개 (파일에 1~13번으로 번호가 붙은 것 + burst 회계 2개).</p>
<table>
<thead>
<tr>
<th>그룹</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>CPU</td>
<td>리셋 중 VALID 금지 / 수락 전 VALID hold / payload stable / X·Z 금지 / write는 be≠0 / 응답 X·Z 금지</td>
</tr>
<tr>
<td>MEM req</td>
<td>수락 전 hold + payload stable / X·Z 금지 / <strong>라인 정렬</strong></td>
</tr>
<tr>
<td>MEM data</td>
<td>write·read 데이터 hold, X·Z 금지</td>
</tr>
<tr>
<td><strong>burst 회계</strong></td>
<td>카운터를 돌려서 <strong>모든 버스트가 정확히 <code>len+1</code> beat 를 내고 마지막에 LAST</strong></td>
</tr>
</tbody></table>
<p><strong>역할 분담</strong>:</p>
<ul>
<li><strong>SVA</strong> = 프로토콜. &quot;언제/어떻게&quot; 신호가 움직이는가. 매 사이클, 국소적.</li>
<li><strong>스코어보드</strong> = 데이터·의미. &quot;무엇이&quot; 맞는가. 트랜잭션 단위, 전역 상태 필요.</li>
</ul>
<p><code>cover property</code> 4개도 있지만 <strong>xsim은 무시한다</strong>(<code>XSIM 43-4127</code>). 문법은 유효하고
Questa에서는 수집된다.</p>
<hr>
<h1 id="part-3-로그로-확인하는-체크리스트">Part 3. 로그로 확인하는 체크리스트</h1>
<pre><code class="language-bat">cd sim\vivado
run.bat l1_cache_eviction_test 42</code></pre>
<p><code>run.bat</code> 이 마지막에 뽑아주는 줄들을 <strong>하나씩 해석</strong>할 수 있어야 한다:</p>
<table>
<thead>
<tr>
<th>확인할 것</th>
<th>어디서</th>
<th>무엇을 봐야 하나</th>
</tr>
</thead>
<tbody><tr>
<td>✅ <code>UVM_ERROR : 0</code> / <code>UVM_FATAL : 0</code></td>
<td>로그 끝</td>
<td>0이 아니면 무조건 실패</td>
</tr>
<tr>
<td>✅ <code>SCOREBOARD SUMMARY: MATCH=… MISMATCH=0</code></td>
<td>SCB report</td>
<td>MATCH가 <strong>충분히 큰가</strong> (0에 가까우면 검사가 안 돈 것)</td>
</tr>
<tr>
<td>✅ <code>Cache hits / misses … (hit rate xx%)</code></td>
<td>SCB report</td>
<td>시퀀스 의도와 맞나? thrash는 hit rate가 바닥, mix는 높아야 함</td>
</tr>
<tr>
<td>✅ <code>Dirty evictions : N</code></td>
<td>SCB report</td>
<td>eviction 테스트인데 <strong>0이면 테스트가 죽은 것</strong> (실제로 있던 버그!)</td>
</tr>
<tr>
<td>✅ <code>Way utilisation : w0=… w1=… w2=… w3=…</code></td>
<td>SCB report</td>
<td>네 개가 비슷해야 PLRU가 살아있다. 한쪽만 크면 교체정책 고장</td>
</tr>
<tr>
<td>✅ <code>Set reach : xx% (n/64)</code></td>
<td>SCB report</td>
<td>주소 분포</td>
</tr>
<tr>
<td>✅ <code>cache_ops covergroup : xx%</code></td>
<td>COV report</td>
<td>시나리오 도달률</td>
</tr>
<tr>
<td>✅ <code>[FSM COV] cache_fsm covergroup: xx%</code></td>
<td><code>final</code> 블록</td>
<td>상태·전이 도달률</td>
</tr>
<tr>
<td>✅ <code>Resets / poisoned : …</code></td>
<td>reset 테스트에서만</td>
<td>poisoned 라인 수가 리셋 수 이하인가</td>
</tr>
</tbody></table>
<p><strong>직접 부숴보기</strong> (가장 빠른 학습법):
<code>l1_cache_core.sv</code> 의 <code>plru_update</code> 호출을 지워보면 → <code>Way utilisation</code> 이 한쪽으로 쏠리고
<code>cp_way</code> 커버리지가 25% 근처에서 멈춘다. <strong>어느 체크가 먼저 우는지</strong> 관찰하면
각 장치의 담당 범위가 몸으로 이해된다. (실험 후 반드시 되돌릴 것.)</p>
<hr>
<h1 id="part-4-반드시-외울-버그-스토리-3개-⭐">Part 4. 반드시 외울 버그 스토리 3개 ⭐</h1>
<p>면접관은 문법보다 <strong>검증적 사고</strong>를 본다. 각각 30초 안에 설명할 수 있어야 한다.</p>
<h3 id="1-조합형-sram-read-모델이-파이프라인과-모순이었다--설계-감각">1. 조합형 SRAM read 모델이 파이프라인과 모순이었다 — <em>설계 감각</em></h3>
<blockquote>
<p>배열을 <code>assign rdata = mem[raddr]</code> 로 모델링해놨는데, 파이프라인은 <strong>S1에서 주소를 주고
S2에서 쓰는</strong> 구조였다. allocate→hit 전환 시점에 코어가 <strong>다음 요청의 index</strong>를 읽어
엉뚱한 라인 데이터를 돌려줬다 → read mismatch <strong>675건</strong>.
게다가 <code>stall → raddr → rdata → hit → stall</code> 이 <strong>닫힌 조합 루프</strong>였다.
읽기 포트를 registered로 바꿔 둘 다 해결하고, 그 대가로 생긴 read-during-write 해저드는
<strong>byte 단위 포워딩 경로</strong>를 추가해 막았다.</p>
<p><strong>교훈: 메모리 모델은 &quot;동작하는 코드&quot;가 아니라 타이밍 계약이다. 계약이 틀리면 RTL 전체가 틀린다.</strong></p>
</blockquote>
<h3 id="2-sva-실패가-회귀에-안-잡히고-있었다--검증-환경도-검증-대상">2. SVA 실패가 회귀에 안 잡히고 있었다 — <em>검증 환경도 검증 대상</em></h3>
<blockquote>
<p>어서션이 <code>$error</code> 를 쓰고 있었는데, 이건 <strong><code>UVM_ERROR</code> 를 증가시키지 않는다.</strong>
어서션이 줄줄이 깨지는 런도 <code>UVM_ERROR : 0</code> 으로 <strong>PASS</strong> 로 보고됐다.
<code>uvm_report_error</code> 로 바꾸고, <strong>일부러 깨지는 어서션을 넣어</strong> 685개 에러가 찍히고
런이 FAIL로 분류되는 걸 확인해서 증명했다.</p>
<p><strong>교훈: 어서션이 &quot;통과&quot;하는 것과 어서션이 &quot;존재&quot;하는 것은 다르다. 체커는 발화시켜서 증명해야 한다.</strong></p>
</blockquote>
<h3 id="3-통과하는-테스트가-아무것도-검증하지-않고-있었다--pass는-증거가-아니다">3. 통과하는 테스트가 아무것도 검증하지 않고 있었다 — <em>PASS는 증거가 아니다</em></h3>
<blockquote>
<p>두 가지가 겹쳐 있었다.
(a) <code>for</code> 루프 <strong>본문의 선언 초기화</strong>(<code>bit [31:0] base = f(i);</code>)를 xsim이 <strong>한 번만</strong> 평가해서,
24개의 서로 다른 캐시 라인이 조용히 <strong>1개</strong>로 붕괴했다 — passive 테스트가
191 hit / 1 miss 를 찍고 &quot;PASS&quot; 였다.
(b) 주소 헬퍼가 <code>get_index(a)</code> 처럼 <strong>한 글자 인자명</strong>을 써서, 호출부에 같은 이름의 지역변수가
있으면 xsim이 실인자를 <strong>callee 스코프에서</strong> 평가해 조용히 0을 넘겼다 —
<code>eviction_test</code> 가 eviction을 <strong>0건</strong> 만들면서 PASS 였다.
둘 다 로그의 <code>Dirty evictions</code> / <code>Way utilisation</code> 통계를 보고서야 발견했다.</p>
<p><strong>교훈: 테스트가 통과했다는 건 자극이 도달했다는 뜻이 아니다. 커버리지와 통계로 &quot;자극이 실제로
갔는가&quot;를 따로 확인해야 한다.</strong></p>
</blockquote>
<p><strong>보너스</strong> — <code>disable fork</code> 가 시퀀서 핸드셰이크를 깨뜨린 건 (Part 2.10) UVM 내부 동작을
아는지 보여주는 소재. 여유 있으면 네 번째로 준비.</p>
<hr>
<h1 id="part-5-능동-학습-연습">Part 5. 능동 학습 연습</h1>
<ol>
<li><strong>백지 구조도</strong> — <a href="OVERVIEW.svg">OVERVIEW.svg</a> 를 보지 않고 4블록 + 화살표 그리기</li>
<li><strong>주소 분해 3회</strong> — 연습 1·2를 다른 주소로 (<code>0xDEAD_BEEF</code> 추천)</li>
<li><strong>PLRU 손계산</strong> — 연습 5의 표를 백지에서 재현</li>
<li><strong>사이클 세기</strong> — dirty miss / clean miss / read hit 각각 (연습 4)</li>
<li><strong>코드 덮고 재현</strong> — 포워딩 로직(<code>l1_cache_core.sv:196~210</code>) 을 백지에 써보기.
막히는 곳이 정확히 약점이다</li>
<li><strong>일부러 부수기</strong> — Part 3의 실험. <code>plru_update</code> 제거 / byte enable 무시 / 포워딩 제거,
각각 <strong>어느 체크가 먼저 우는지</strong> 기록</li>
<li><strong>버그 3개 소리 내어 설명</strong> — 각각 30초, 타이머 켜고</li>
<li><strong>README &quot;History&quot; 표 읽기</strong> — 코드만 봐선 안 보이는 <strong>의사결정 기록</strong>이다.
&quot;왜 이렇게 했나&quot; 질문의 답이 여기 다 있다</li>
</ol>
<hr>
<h1 id="part-6-추천-일정-하루-12시간">Part 6. 추천 일정 (하루 1~2시간)</h1>
<table>
<thead>
<tr>
<th>일차</th>
<th>목표</th>
</tr>
</thead>
<tbody><tr>
<td>1일</td>
<td>Layer A. <code>l1_cache_pkg.sv</code> 정독 + Part 2.1~2.2 + 연습 1·2. smoke 테스트 로그 해석</td>
</tr>
<tr>
<td>2일</td>
<td>Part 2.3~2.4 (registered SRAM, 포워딩) + 연습 3. <strong>여기가 제일 어렵다, 시간 더 써라</strong></td>
</tr>
<tr>
<td>3일</td>
<td>Part 2.5~2.6 (FSM, PLRU) + 연습 4·5. 사이클 표 백지 재현</td>
</tr>
<tr>
<td>4일</td>
<td>Part 2.7~2.9 (요청의 일생, 스코어보드, 순서 문제). 스코어보드 <code>write_req()</code> 정독</td>
</tr>
<tr>
<td>5일</td>
<td>Part 2.10~2.13 (mem responder, reset, coverage, SVA)</td>
</tr>
<tr>
<td>6일</td>
<td>Part 3 체크리스트로 테스트 3개 돌려 로그 해석 + Part 5의 &quot;일부러 부수기&quot;</td>
</tr>
<tr>
<td>7일</td>
<td>버그 3개 스토리 + Part 7 질문 소리 내어 답하기</td>
</tr>
</tbody></table>
<hr>
<h1 id="part-7-예상-질문">Part 7. 예상 질문</h1>
<h3 id="캐시-기본">캐시 기본</h3>
<ul>
<li>direct-mapped / set-assoc / full-assoc 차이와 트레이드오프는?</li>
<li>write-back vs write-through, write-allocate vs no-write-allocate?</li>
<li>3C miss는? 이 프로젝트에서 각각 어떻게 구분해서 커버리지로 잡았나?</li>
<li>라인 크기를 32B로 키우면 뭐가 좋아지고 뭐가 나빠지나?</li>
<li>true LRU 대신 tree-PLRU를 쓴 이유는? 4-way에서 몇 bit 차이인가?</li>
</ul>
<h3 id="이-설계-고유-⭐">이 설계 고유 ⭐</h3>
<ul>
<li>SRAM read를 왜 registered로 만들었나? 조합형이면 뭐가 터지나?</li>
<li>read-during-write 해저드를 어떻게 처리했나? 왜 <strong>byte 단위</strong>인가?</li>
<li>포워딩 비교 대상이 왜 <code>s2_*</code> 가 아니라 <code>prev_*_raddr</code> 인가?</li>
<li><code>ST_COMPLETE</code> 는 왜 있나? 한 사이클 손해 아닌가? 여기서 stall을 푸는 이유는?</li>
<li><code>ST_WB_READ</code> 가 왜 4가 아니라 5 사이클인가?</li>
<li>valid/dirty/PLRU를 SRAM이 아니라 플롭에 둔 이유는?</li>
<li>hit throughput은? latency는? 둘의 차이는?</li>
<li>back-to-back에서 VALID를 안 내린다는 게 무슨 뜻인가? 어떻게 구현했나?</li>
</ul>
<h3 id="검증-방법론-⭐">검증 방법론 ⭐</h3>
<ul>
<li>스코어보드가 정답을 어떻게 아는가? 왜 메모리 모델이 아니라 <strong>캐시 모델</strong>인가?</li>
<li>파이프라인 DUT에서 응답 순서 문제를 어떻게 풀었나? 왜 analysis port가 2개인가?</li>
<li>커버리지를 왜 모니터가 아니라 스코어보드에서 샘플하나?</li>
<li><code>bind</code> 는 뭐고 왜 썼나?</li>
<li><code>illegal_bins</code> 와 <code>ignore_bins</code> 의 차이는? 후자를 남용하면?</li>
<li>커버리지 100%면 검증이 끝난 건가?</li>
<li>시드를 여러 개 도는 이유는? 커버리지는 어떻게 합치나?</li>
<li>리셋 검증은 어떻게 했나? poisoning은 뭐고 왜 그 범위인가?</li>
<li>passive agent는 왜 만들었고 어떻게 <strong>검증</strong>했나?</li>
<li>RAL은 왜 안 썼나? <em>(함정 — DUT에 memory-mapped register가 없다. CSR 블록을 먼저
설계해야 RAL이 의미를 갖는다. 기법 과시용으로 레지스터를 만들어내지는 않았다)</em></li>
<li>SVA와 스코어보드의 역할 차이는?</li>
</ul>
<h3 id="systemverilog--uvm">SystemVerilog / UVM</h3>
<ul>
<li>clocking block을 왜 쓰나? <code>@(posedge clk); #1ps;</code> 와 뭐가 다른가?</li>
<li><code>uvm_analysis_imp_decl</code> 은 왜 필요한가?</li>
<li><code>soft</code> constraint 는 언제 쓰나?</li>
<li>objection과 drain time은?</li>
<li><code>disable fork</code> 가 왜 위험한가? 이 프로젝트에서 뭘 깨뜨렸나?</li>
<li>procedural 블록 안의 선언 초기화가 왜 함정인가? <em>(static — 시간 0에 한 번만)</em></li>
</ul>
<h3 id="약점-방어">약점 방어</h3>
<ul>
<li>non-blocking miss(MSHR)는 왜 없나? <em>(TB 재설계가 필요 — Part 2.9 참고. 반쯤
하다 마느니 미룬 것)</em></li>
<li>critical-word-first는? write buffer는? 파라미터화는?</li>
<li>code coverage / X-propagation / gate-level / formal 은?</li>
</ul>
<hr>
<h2 id="가장-중요한-조언">가장 중요한 조언</h2>
<p>이 프로젝트의 진짜 가치는 &quot;동작하는 캐시 컨트롤러&quot;가 아니라
<strong>&quot;통과하는데 아무것도 검증하지 않던 테스트벤치&quot;를 찾아내고 고친 과정</strong>이다.</p>
<p>코드 100줄 외우는 것보다 <strong>Part 4의 세 문장을 자기 말로 설명</strong>하는 게 훨씬 강력하고,
그 앞에 <strong>Part 2.3~2.5의 사이클 계산</strong>을 손으로 할 수 있으면 &quot;AI가 만들어준 프로젝트&quot;와
&quot;내가 이해한 프로젝트&quot;의 차이가 명확해진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (9) — base_test ]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-9-basetest</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-9-basetest</guid>
            <pubDate>Thu, 30 Jul 2026 16:25:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: ... · tb_top · agent_cfg/agent/env · <strong>base_test</strong></p>
</blockquote>
<hr>
<h2 id="test란">Test란</h2>
<pre><code>&quot;어떤 환경을 만들고, 어떤 자극(sequence)을 흘려보낼지&quot; 결정하는 최고 지휘자
run_test()가 부르는 바로 그 클래스</code></pre><p>지금까지 흐름의 <strong>시작점</strong>이다. tb_top의 <code>run_test(&quot;axi4_base_test&quot;)</code>가 이 파일을 깨운다.</p>
<hr>
<h2 id="전체-구조">전체 구조</h2>
<pre><code>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를 선택</code></pre><p>핵심 설계: <strong>부모가 뼈대를 다 만들고, 자식은 <code>get_seq()</code> 하나만 바꿔서 다른 테스트가 됨</strong></p>
<hr>
<h1 id="part-1-axi4_base_test--뼈대">Part 1: axi4_base_test — 뼈대</h1>
<h2 id="build_phase--환경-조립">build_phase — 환경 조립</h2>
<pre><code class="language-systemverilog">function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    cfg = axi4_agent_cfg::type_id::create(&quot;cfg&quot;);
    if (!uvm_config_db#(virtual axi4_if)::get(this, &quot;&quot;, &quot;vif&quot;, cfg.vif))
        `uvm_fatal(&quot;NOVIF&quot;, &quot;virtual interface &#39;vif&#39; was not set by tb_top&quot;)
    cfg.is_active = UVM_ACTIVE;

    uvm_config_db#(axi4_agent_cfg)::set(this, &quot;env&quot;, &quot;cfg&quot;, cfg);
    env = axi4_env::type_id::create(&quot;env&quot;, this);
endfunction</code></pre>
<h3 id="단계별">단계별</h3>
<pre><code>1. cfg 객체 생성 (아직 비어있음)

2. tb_top이 넣어둔 vif를 꺼내서 cfg에 담기
   ⭐ 여기가 하드웨어-UVM 연결의 완성점!
   tb_top: config_db::set(null, &quot;*&quot;, &quot;vif&quot;, axi)   ← 넣음
   test:   config_db::get(this, &quot;&quot;, &quot;vif&quot;, cfg.vif) ← 꺼냄
   → axi 인터페이스가 cfg.vif에 담김
   → cfg를 내려보내면 driver/monitor가 하드웨어 접근 가능

3. active 모드 설정 (sequencer+driver 생성)

4. cfg를 env에 전달 + env 생성
   cfg 전달 체인 시작: test → env → agent → driver/monitor</code></pre><p><img src="https://velog.velcdn.com/images/houston_guy2/post/b97d1e26-188a-4f66-b3e0-2f8d51cd3cbc/image.png" alt=""></p>
<h2 id="end_of_elaboration_phase--계층-출력">end_of_elaboration_phase — 계층 출력</h2>
<pre><code class="language-systemverilog">function void end_of_elaboration_phase(uvm_phase phase);
    super.end_of_elaboration_phase(phase);
    uvm_top.print_topology();
endfunction</code></pre>
<pre><code>모든 컴포넌트가 만들어진 후 계층 구조를 로그에 출력

print_topology() 출력 예:
  uvm_test_top
    env
      agent
        sqr
        drv
        mon
      sb
      cov

→ &quot;내가 만든 환경이 제대로 조립됐나&quot; 확인용 (디버깅에 유용)
end_of_elaboration_phase = build/connect 다음 (모든 게 생성+연결된 후)</code></pre><h2 id="get_seq--자극-선택-override-포인트">get_seq — 자극 선택 (override 포인트)</h2>
<pre><code class="language-systemverilog">virtual function axi4_base_seq get_seq();
    axi4_wr_rd_seq s = axi4_wr_rd_seq::type_id::create(&quot;seq&quot;);
    return s;
endfunction</code></pre>
<pre><code>virtual 함수 = 자식이 오버라이드할 수 있음!

기본값: axi4_wr_rd_seq (쓰기+읽기 시퀀스)

이게 핵심 설계:
  부모는 &quot;어떤 sequence든 실행&quot;하는 뼈대만 제공
  자식은 get_seq만 바꿔서 다른 sequence 선택
  → polymorphism + factory 패턴!

반환 타입이 axi4_base_seq (부모 타입):
  실제로는 자식 시퀀스를 반환해도 됨 (polymorphism)
  → run_phase는 axi4_base_seq로 받아서 실행 → 어떤 시퀀스든 동일 처리</code></pre><h2 id="run_phase--실행">run_phase — 실행</h2>
<pre><code class="language-systemverilog">task run_phase(uvm_phase phase);
    axi4_base_seq seq = get_seq();
    phase.raise_objection(this, &quot;stimulus running&quot;);
    `uvm_info(&quot;TEST&quot;, $sformatf(&quot;starting sequence &#39;%s&#39; (num=%0d)&quot;,
                                seq.get_type_name(), seq.num), UVM_LOW)
    seq.start(env.agent.sqr);
    drain();
    phase.drop_objection(this, &quot;stimulus done&quot;);
endtask</code></pre>
<h3 id="objection-메커니즘">objection 메커니즘</h3>
<pre><code>objection = &quot;나 아직 할 일 있어, 끝내지 마&quot; 신호

raise_objection → &quot;시뮬레이션 끝내지 마&quot;
drop_objection  → &quot;이제 끝내도 돼&quot;

모든 objection이 drop되면 → run_phase 종료 → 시뮬레이션 끝

왜 필요? 이거 없으면 run_phase가 바로 끝나버림
        → 시퀀스 실행 전에 시뮬레이션 종료</code></pre><h3 id="sequence-실행">sequence 실행</h3>
<pre><code class="language-systemverilog">seq.start(env.agent.sqr);</code></pre>
<pre><code>get_seq()로 만든 시퀀스를 시퀀서에서 실행!

env.agent.sqr = agent가 만든 시퀀서
→ 시퀀스가 여기에 아이템을 밀어넣음
→ 드라이버가 seq_item_port로 받아서 구동

전체 연결:
  seq.start(sqr) → sqr → drv.seq_item_port → 버스 구동</code></pre><h2 id="drain--파이프라인-배출-매우-중요">drain — 파이프라인 배출 (매우 중요)</h2>
<pre><code class="language-systemverilog">protected task drain();
    int unsigned guard = 0;
    while (!env.agent.drv.is_idle() &amp;&amp; guard &lt; 10000) begin
        #100ns;
        guard++;
    end
    if (guard &gt;= 10000)
        `uvm_error(&quot;DRAIN&quot;, &quot;driver did not go idle - transactions stuck?&quot;)
    #200ns;   // let the final response propagate through the monitor
endtask</code></pre>
<p>주석이 왜 필요한지 설명한다:</p>
<pre><code>// The driver is pipelined: item_done() returns as soon as a burst is
// queued, not when it finishes on the bus.</code></pre><h3 id="왜-drain이-필요한가">왜 drain이 필요한가</h3>
<pre><code>파이프라인 드라이버의 함정:
  item_done()이 큐에 넣자마자 반환
  → 시퀀스가 &quot;다 보냈다&quot; 생각하고 끝남
  → 근데 버스엔 아직 버스트가 날아다니는 중!

drain 없이 끝내면:
  마지막 버스트들이 응답 못 받고 잘림
  → 스코어보드가 미완료 상태로 종료 → 검증 누락!</code></pre><h3 id="drain-동작">drain 동작</h3>
<pre><code>while (!env.agent.drv.is_idle() &amp;&amp; guard &lt; 10000) begin ... end
  드라이버가 idle 될 때까지 대기
  is_idle() = 큐 비었고 + inflight 0  (driver에서 배운 함수!)

guard = 무한루프 방지 안전장치
  10000번(100ns × 10000 = 1ms) 넘으면 강제 탈출 + 에러
  → 트랜잭션이 멈춰서 영원히 안 끝나는 상황 방지

#200ns = 마지막 응답이 모니터 → 스코어보드까지 도달할 시간 확보</code></pre><p><img src="https://velog.velcdn.com/images/houston_guy2/post/5bb8d5f2-f551-4f76-8f69-317fba2c9156/image.png" alt=""></p>
<hr>
<h1 id="part-2-파생-테스트들--get_seq만-바꿈">Part 2: 파생 테스트들 — get_seq만 바꿈</h1>
<h2 id="단일-방향-테스트-3종">단일 방향 테스트 3종</h2>
<pre><code class="language-systemverilog">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(&quot;seq&quot;);
        return s;
    endfunction
endclass</code></pre>
<pre><code>write_test → 쓰기만
read_test  → 읽기만
rand_test  → 랜덤

세 클래스 전부 get_seq() 하나만 다름!
나머지(build/run/drain)는 부모 것 그대로 상속
→ 코드 중복 최소화, 뼈대 재사용</code></pre><p>polymorphism의 실전 활용:</p>
<pre><code>부모 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</code></pre><h2 id="directed_test--코너-케이스">directed_test — 코너 케이스</h2>
<pre><code>directed = 특정 코너를 직접 겨냥
  single beat (len=0), max burst (len=15), 4KB 경계 근처,
  narrow transfer (size &lt; 버스폭), FIXED 버스트

→ &quot;랜덤이 plateau에서 못 가는 코너를 directed로 채움&quot;</code></pre><h2 id="cov_test--커버리지-채우기">cov_test — 커버리지 채우기</h2>
<pre><code class="language-systemverilog">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(&quot;seq&quot;);
        s.num = 40;              // extra random traffic after the sweep
        return s;
    endfunction
endclass</code></pre>
<pre><code>커버리지 bin을 채우기 위한 넓은 자극
s.num = 40 → sweep 후 추가 랜덤 40개

주석: &quot;May report WRAP mismatches - those are true positives&quot;
  → WRAP 버그로 인한 미스매치가 나올 수 있고 그건 진짜 버그(true positive)

get_seq에서 시퀀스 필드(num)를 조정하는 것도 가능!</code></pre><hr>
<h1 id="part-3-특수-테스트들-구조가-다름">Part 3: 특수 테스트들 (구조가 다름)</h1>
<h2 id="outstanding_test--파이프라인-검증">outstanding_test — 파이프라인 검증</h2>
<p><code>get_seq</code>가 아니라 <strong>build_phase와 run_phase를 직접 오버라이드</strong>한다.</p>
<pre><code class="language-systemverilog">class axi4_outstanding_test extends axi4_base_test;

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        cfg.max_outstanding = 4;    // ⭐ 파이프라인 깊이 설정
    endfunction</code></pre>
<h3 id="max_outstanding-설정-타이밍이-핵심">max_outstanding 설정 타이밍이 핵심</h3>
<pre><code>super.build_phase(phase);       // 부모가 cfg 만듦
cfg.max_outstanding = 4;        // 그 cfg를 수정

주석: &quot;The driver&#39;s build_phase runs after ours, so it picks this up.&quot;

순서 (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를 받음 ✅</code></pre><h3 id="run_phase--쓰기읽기-분리-pass">run_phase — 쓰기/읽기 분리 pass</h3>
<pre><code class="language-systemverilog">task run_phase(uvm_phase phase);
    axi4_os_write_seq wr = ...;
    axi4_os_read_seq  rd = ...;
    phase.raise_objection(this, &quot;outstanding stimulus&quot;);

    wr.start(env.agent.sqr);
    drain();                     // 모든 쓰기가 끝나야 읽기 시작

    rd.start(env.agent.sqr);
    drain();

    phase.drop_objection(this, &quot;done&quot;);
endtask</code></pre>
<pre><code>주석: &quot;read must not overtake a write that has not completed yet&quot;

왜 분리?
  AXI 읽기/쓰기 채널은 독립적
  → 파이프라인으로 섞으면 읽기가 쓰기를 추월할 수 있음
  → &quot;아직 안 쓴 데이터를 읽는&quot; 상황 → 잘못된 검증

해결:
  phase 1: 쓰기 전부 → drain (다 완료 대기)
  phase 2: 읽기 전부 → drain
  → 쓰기가 확실히 끝난 후 읽기 → 순서 보장

목적: peak 카운터로 파이프라인 동작 증명
  axi_ram(v1)    → peak 1 (한 번에 하나)
  axi_ram_v2     → peak &gt;1 (outstanding 지원)</code></pre><h2 id="wrap_test--일부러-실패하는-테스트">wrap_test — 일부러 실패하는 테스트</h2>
<pre><code class="language-systemverilog">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(&quot;seq&quot;);
        return s;
    endfunction

    function void start_of_simulation_phase(uvm_phase phase);
        super.start_of_simulation_phase(phase);
        `uvm_info(&quot;TEST&quot;,
            &quot;axi4_wrap_test: UVM_ERRORs below are EXPECTED - they demonstrate the DUT&#39;s WRAP-burst bug&quot;,
            UVM_LOW)
    endfunction
endclass</code></pre>
<pre><code>이 테스트는 &quot;일부러 실패&quot;함!

목적:
  DUT의 WRAP 버그를 문서화하고 재현
  → 회귀 테스트에는 포함 안 됨 (clean regression 아님)

start_of_simulation_phase:
  시뮬 시작 시 &quot;아래 에러들은 예상된 것&quot;이라고 미리 알림
  → 로그 보는 사람이 &quot;진짜 실패&quot;로 오해 안 하게

negative testing의 일부:
  &quot;버그가 있으면 정말 FAIL 하는가&quot;를 증명하는 테스트</code></pre><hr>
<h1 id="전체-테스트-계층">전체 테스트 계층</h1>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/69eed951-e54a-4a95-a409-5196155859ea/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>테스트</th>
<th>방식</th>
<th>목적</th>
</tr>
</thead>
<tbody><tr>
<td>base</td>
<td>뼈대</td>
<td>wr_rd 기본 자극</td>
</tr>
<tr>
<td>write/read/rand</td>
<td>get_seq만</td>
<td>단일 방향/랜덤</td>
</tr>
<tr>
<td>directed</td>
<td>get_seq만</td>
<td>코너 케이스</td>
</tr>
<tr>
<td>cov</td>
<td>get_seq + num</td>
<td>커버리지 채우기</td>
</tr>
<tr>
<td>outstanding</td>
<td>build+run</td>
<td>파이프라인 검증</td>
</tr>
<tr>
<td>wrap</td>
<td>get_seq + 알림</td>
<td>버그 재현 (FAIL 예상)</td>
</tr>
</tbody></table>
<hr>
<h2 id="테스트를-어떻게-고르나--커맨드라인">테스트를 어떻게 고르나 — 커맨드라인</h2>
<p>test를 바꾸는 건 코드 수정이 아니라 실행 옵션이다.</p>
<pre><code class="language-bash"># 기본 (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</code></pre>
<pre><code>한 번 실행 = test 하나 = 시퀀스 하나
여러 개 다 돌리기 = regression 스크립트가 여러 번 자동 실행</code></pre><hr>
<h2 id="전체-흐름-완성">전체 흐름 완성</h2>
<pre><code>tb_top
  → config_db::set(vif)
  → run_test(&quot;axi4_base_test&quot;)
        ↓
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</code></pre><hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<pre><code>Test = UVM 계층 최상단, &quot;환경 + 자극&quot;을 결정하는 지휘자

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으로 런타임에 선택</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (8) — agent_cfg · agent · env ]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-8-agentcfg-agent-env</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-8-agentcfg-agent-env</guid>
            <pubDate>Thu, 30 Jul 2026 16:22:23 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: ... · tb_top · <strong>agent_cfg/agent/env</strong> · base_test</p>
</blockquote>
<hr>
<h2 id="전체-그림--uvm-계층의-조립">전체 그림 — UVM 계층의 조립</h2>
<p>이 세 파일은 <strong>UVM 컴포넌트들을 계층으로 묶는 골격</strong>이다. 개별 부품(driver, monitor, scoreboard)이 여기서 조립된다.</p>
<pre><code>                    Test (다음 편)
                       │ 생성
                       ↓
        ┌──────────── env ────────────┐
        │                             │
     agent                    scoreboard + coverage
        │                             ↑
  ┌─────┼─────┐                       │
  sqr  drv   mon ───── ap.connect ────┘
  │     │     │
  └─sequencer │
     driver   monitor

cfg (설정 객체) → 모든 컴포넌트에 전달되어 &quot;어떻게 동작할지&quot; 지시</code></pre><hr>
<h1 id="part-1-axi4_agent_cfg--설정-객체">Part 1: axi4_agent_cfg — 설정 객체</h1>
<h2 id="역할">역할</h2>
<pre><code>&quot;이 agent가 어떻게 동작해야 하는지&quot;를 담은 작은 설정 상자
부품에 붙이는 &quot;사용 설명서&quot; 같은 것</code></pre><h2 id="코드">코드</h2>
<pre><code class="language-systemverilog">class axi4_agent_cfg extends uvm_object;

    virtual axi4_if         vif;
    uvm_active_passive_enum is_active = UVM_ACTIVE;
    int unsigned max_outstanding = 1;

    `uvm_object_utils(axi4_agent_cfg)

    function new(string name = &quot;axi4_agent_cfg&quot;);
        super.new(name);
    endfunction
endclass</code></pre>
<h2 id="3개의-설정값knob">3개의 설정값(knob)</h2>
<h3 id="1-vif--하드웨어-핸들">1. vif — 하드웨어 핸들</h3>
<pre><code class="language-systemverilog">virtual axi4_if vif;</code></pre>
<pre><code>DUT 핀에 접근하는 virtual interface 핸들

tb_top의 config_db::set으로 넘어온 axi 인터페이스가 결국 여기 vif에 담김
→ driver가 cfg.vif로 꺼내서 신호 구동
→ monitor가 cfg.vif로 꺼내서 관찰</code></pre><h3 id="2-is_active--능동수동-선택">2. is_active — 능동/수동 선택</h3>
<pre><code class="language-systemverilog">uvm_active_passive_enum is_active = UVM_ACTIVE;</code></pre>
<pre><code>UVM_ACTIVE  → sequencer + driver + monitor 생성 (버스 구동 + 관찰)
UVM_PASSIVE → monitor만 생성 (관찰만)

언제 PASSIVE?
  이미 다른 마스터가 버스를 구동하고 있고 우리는 지켜보기만 할 때
  (예: 실제 CPU가 구동하는 버스를 모니터링)

기본값 UVM_ACTIVE = 우리가 직접 구동</code></pre><h3 id="3-max_outstanding--파이프라인-깊이">3. max_outstanding — 파이프라인 깊이</h3>
<pre><code class="language-systemverilog">int unsigned max_outstanding = 1;</code></pre>
<pre><code>드라이버에서 봤던 그 값!

1  → 직렬 (한 번에 하나씩) = 안전한 기본값
     write-then-read-back 순서 보장
&gt;1 → 파이프라인 (여러 개 동시)
     axi_ram_v2의 outstanding 지원을 검증하려면 필요

driver의 item_thread가 이 값으로 throttle:
  while (n_inflight &gt;= cfg.max_outstanding) @(vif.master_cb);</code></pre><h2 id="왜-cfg-객체로-묶나">왜 cfg 객체로 묶나</h2>
<pre><code>// Passing knobs via a cfg object (instead of many config_db entries)
// is the standard, scalable UVM style.</code></pre><pre><code>❌ 나쁜 방법: 설정마다 config_db 따로
  config_db::set(..., &quot;vif&quot;, axi)
  config_db::set(..., &quot;is_active&quot;, ...)
  config_db::set(..., &quot;max_outstanding&quot;, ...)
  → set/get 호출 폭발, 관리 어려움

✅ 좋은 방법: cfg 객체 하나에 다 담아서 전달
  cfg.vif = ...
  cfg.is_active = ...
  cfg.max_outstanding = ...
  config_db::set(..., &quot;cfg&quot;, cfg)  ← 한 번만!
  → 설정 추가해도 cfg에 필드만 늘리면 됨 (확장성)</code></pre><hr>
<h1 id="part-2-axi4_agent--컴포넌트-컨테이너">Part 2: axi4_agent — 컴포넌트 컨테이너</h1>
<h2 id="역할-1">역할</h2>
<pre><code>하나의 AXI 인터페이스를 담당하는 UVM 컴포넌트 묶음
sequencer + driver + monitor를 한 상자에 담고 서로 연결</code></pre><h2 id="typedef--시퀀서-정의">typedef — 시퀀서 정의</h2>
<pre><code class="language-systemverilog">typedef uvm_sequencer #(axi4_seq_item) axi4_sequencer;</code></pre>
<pre><code>시퀀서는 특별할 게 없음 → uvm_sequencer를 그대로 사용
우리 아이템 타입(axi4_seq_item)만 지정하면 끝

typedef = 짧은 별명 붙이기 → 따로 클래스 작성 불필요, 한 줄로 해결

왜 driver/monitor는 직접 만드는데 sequencer는 안 만드나?
  driver/monitor: 프로토콜별 로직 필요 → 직접 구현
  sequencer: 아이템을 driver로 전달만 함 → 기본 기능으로 충분</code></pre><h2 id="멤버">멤버</h2>
<pre><code class="language-systemverilog">class axi4_agent extends uvm_agent;
    axi4_agent_cfg cfg;
    axi4_sequencer sqr;
    axi4_driver    drv;
    axi4_monitor   mon;</code></pre>
<h2 id="build_phase--자식-생성">build_phase — 자식 생성</h2>
<pre><code class="language-systemverilog">function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(axi4_agent_cfg)::get(this, &quot;&quot;, &quot;cfg&quot;, cfg))
        `uvm_fatal(&quot;NOCFG&quot;, &quot;axi4_agent_cfg not found for agent&quot;)

    // 자식에게 cfg 전달
    uvm_config_db#(axi4_agent_cfg)::set(this, &quot;drv&quot;, &quot;cfg&quot;, cfg);
    uvm_config_db#(axi4_agent_cfg)::set(this, &quot;mon&quot;, &quot;cfg&quot;, cfg);

    // 모니터는 항상, 시퀀서/드라이버는 active일 때만
    mon = axi4_monitor::type_id::create(&quot;mon&quot;, this);
    if (cfg.is_active == UVM_ACTIVE) begin
        sqr = axi4_sequencer::type_id::create(&quot;sqr&quot;, this);
        drv = axi4_driver::type_id::create(&quot;drv&quot;, this);
    end
endfunction</code></pre>
<h3 id="단계별">단계별</h3>
<pre><code>1. cfg 받기 (env가 넣어준 것)
   없으면 fatal (설정 없이 동작 불가)

2. 자식(drv, mon)에게 cfg 다시 전달
   &quot;drv&quot; → 이 agent 밑의 drv에게만
   &quot;mon&quot; → 이 agent 밑의 mon에게만
   → driver/monitor의 build_phase에서 get으로 받는 그 cfg!

3. 컴포넌트 생성
   모니터: 항상 (관찰은 언제나 필요)
   시퀀서+드라이버: UVM_ACTIVE일 때만 (구동할 때만)
   → PASSIVE 모드면 모니터만 존재 → 순수 관찰</code></pre><p><img src="https://velog.velcdn.com/images/houston_guy2/post/8f2a2341-822e-406d-a115-3658a38ccc88/image.png" alt=""></p>
<h2 id="connect_phase--드라이버↔시퀀서-연결">connect_phase — 드라이버↔시퀀서 연결</h2>
<pre><code class="language-systemverilog">function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (cfg.is_active == UVM_ACTIVE)
        drv.seq_item_port.connect(sqr.seq_item_export);
endfunction</code></pre>
<pre><code>드라이버와 시퀀서를 TLM 포트로 연결

drv.seq_item_port  → 드라이버의 &quot;아이템 달라&quot; 요청 포트
sqr.seq_item_export → 시퀀서의 &quot;아이템 줄게&quot; 제공 포트

connect로 이 둘을 연결
→ driver의 seq_item_port.get_next_item()이 동작하는 근거!

연결 그림:
  Sequence → Sequencer ──seq_item_export──┐
                                           │ connect
  Driver ──seq_item_port──────────────────┘
    get_next_item() → 시퀀서에서 아이템 받음
    item_done()     → 완료 알림

왜 connect_phase에서?
  build_phase: 컴포넌트 생성 / connect_phase: 생성된 것들을 연결
  → UVM phase 순서 (build 다음 connect)</code></pre><hr>
<h1 id="part-3-axi4_env--최상위-환경">Part 3: axi4_env — 최상위 환경</h1>
<h2 id="역할-2">역할</h2>
<pre><code>agent + scoreboard + coverage를 담는 최상위 컨테이너
모니터가 관찰한 걸 scoreboard와 coverage에 뿌리는 연결까지 담당</code></pre><h2 id="멤버-1">멤버</h2>
<pre><code class="language-systemverilog">class axi4_env extends uvm_env;
    axi4_agent      agent;
    axi4_scoreboard sb;
    axi4_coverage   cov;
    axi4_agent_cfg  cfg;</code></pre>
<h2 id="build_phase--자식-생성--cfg-전달">build_phase — 자식 생성 &amp; cfg 전달</h2>
<pre><code class="language-systemverilog">function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(axi4_agent_cfg)::get(this, &quot;&quot;, &quot;cfg&quot;, cfg))
        `uvm_fatal(&quot;NOCFG&quot;, &quot;axi4_agent_cfg not found for env&quot;)

    uvm_config_db#(axi4_agent_cfg)::set(this, &quot;agent&quot;, &quot;cfg&quot;, cfg);
    agent = axi4_agent::type_id::create(&quot;agent&quot;, this);
    sb    = axi4_scoreboard::type_id::create(&quot;sb&quot;, this);
    cov   = axi4_coverage::type_id::create(&quot;cov&quot;, this);
endfunction</code></pre>
<pre><code>1. test가 넣어준 cfg 받기
2. agent에게 cfg 전달 (set with &quot;agent&quot; 경로)
3. agent, scoreboard, coverage 생성

cfg 전달 체인:
  test → env → agent → driver/monitor (계층을 따라 내려가며 전달)</code></pre><h2 id="connect_phase--브로드캐스트-연결-핵심">connect_phase — 브로드캐스트 연결 (핵심)</h2>
<pre><code class="language-systemverilog">function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    agent.mon.ap.connect(sb.ap_imp);
    agent.mon.ap.connect(cov.analysis_export);
endfunction</code></pre>
<p>monitor의 <code>ap.write(tr)</code>가 어디로 가는지의 답이 여기다.</p>
<pre><code>모니터의 analysis port(ap)를 두 곳에 연결:

agent.mon.ap → sb.ap_imp              (스코어보드)
agent.mon.ap → cov.analysis_export    (커버리지)

→ 모니터가 ap.write(tr) 하면 스코어보드와 커버리지 둘 다 동시에 받음! (브로드캐스트)

주석 설명:
  checker (is it correct?)  → 스코어보드: 맞는지 검사
  coverage (did we exercise it?) → 커버리지: 뭘 실행했나 기록
  한 트랜잭션 → 두 관점으로 동시 분석</code></pre><p><img src="https://velog.velcdn.com/images/houston_guy2/post/afba225f-c6ae-4041-9444-13bc1ade78ae/image.png" alt=""></p>
<hr>
<h1 id="전체-계층--cfg-흐름">전체 계층 &amp; cfg 흐름</h1>
<h2 id="컴포넌트-계층">컴포넌트 계층</h2>
<pre><code>env
├── agent
│   ├── sequencer (active only)
│   ├── driver    (active only)
│   └── monitor   (always)
├── scoreboard
└── coverage</code></pre><h2 id="cfg-전달-체인">cfg 전달 체인</h2>
<pre><code>tb_top: config_db::set(vif=axi)
   ↓
test: cfg 생성, cfg.vif 채움, config_db::set(cfg)
   ↓
env: get(cfg) → set(agent, cfg)
   ↓
agent: get(cfg) → set(drv, cfg), set(mon, cfg)
   ↓
driver/monitor: get(cfg) → cfg.vif로 하드웨어 접근!</code></pre><h2 id="두-방향의-흐름">두 방향의 흐름</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/77c52b8a-5b4f-4420-8166-00615cd9dcd4/image.png" alt=""></p>
<pre><code>cfg는 위→아래로 전달 (test→env→agent→driver/monitor)
데이터는 아래→위로 흐름 (monitor→ap→scoreboard/coverage)</code></pre><hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<pre><code>axi4_agent_cfg = &quot;어떻게 동작할지&quot; 담은 설정 상자
                 (vif, is_active, max_outstanding)

axi4_agent = sqr+drv+mon 묶음
             cfg를 자식에 전달, driver↔sequencer 연결
             active면 전부, passive면 monitor만

axi4_env = agent+scoreboard+coverage 묶음
           monitor의 ap를 SB와 cov에 브로드캐스트 연결

핵심 흐름:
  cfg는 위→아래로 전달 (test→env→agent→driver/monitor)
  데이터는 아래→위로 흐름 (monitor→ap→scoreboard/coverage)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (7) — tb_top ]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-7-tbtop</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-7-tbtop</guid>
            <pubDate>Thu, 30 Jul 2026 16:18:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: ... · ref_model/scoreboard · <strong>tb_top</strong> · agent_cfg · agent · env · base_test</p>
</blockquote>
<hr>
<h2 id="tb_top이란">tb_top이란</h2>
<pre><code>지금까지 본 모든 부품(인터페이스, DUT, UVM 환경)을
실제로 연결하고 클록을 돌려서 시뮬레이션을 시작하는 &quot;조립 공장&quot;</code></pre><p>driver, monitor, scoreboard, ref_model이 <strong>어디서 시작되는지</strong>가 여기다.</p>
<hr>
<h2 id="전체-구조-한눈에">전체 구조 한눈에</h2>
<pre><code>┌─────────────────────── tb_top ────────────────────────┐
│                                                       │
│  ① 클록/리셋 생성                                        │ 
│     clk (100MHz), rst (10사이클 유지)                    │
│                                                       │
│  ② 인터페이스 인스턴스                                     │
│     axi4_if axi(.clk, .rst)                           │
│         ↕ (신호 연결)                                   │
│  ③ DUT 인스턴스                                         │
│     axi_ram dut(.s_axi_awid(axi.awid), ...)           │
│                                                       │
│  ④ UVM에 인터페이스 전달 + 테스트 시작                       │
│     config_db::set(vif) → run_test()                  │
│                                                       │
│  ⑤ 파형 덤프                                            │
└───────────────────────────────────────────────────────┘</code></pre><hr>
<h2 id="0-헤더--dut-선택-스위치">0. 헤더 — DUT 선택 스위치</h2>
<pre><code class="language-systemverilog">`ifdef DUT_V2
    `define DUT_MODULE axi_ram_v2
`else
    `define DUT_MODULE axi_ram
`endif</code></pre>
<p>이게 <strong>&quot;v1에서 fail, v2에서 pass&quot;</strong>를 가능하게 하는 핵심이다.</p>
<pre><code>컴파일 옵션에 따라 DUT가 바뀜:

기본:            axi_ram     (WRAP 버그 있는 원본)
+define+DUT_V2:  axi_ram_v2  (WRAP 수정 + outstanding 지원)

두 DUT의 포트 목록이 완전히 동일 → 모듈 이름만 바뀜
→ 같은 테스트벤치로 두 버전 검증 가능!

v1에 돌리면 → 스코어보드가 WRAP 미스매치 잡음 → FAIL
v2에 돌리면 → 수정됐으니 → PASS
→ &quot;버그가 있으면 FAIL, 없으면 PASS&quot; 증명 (테스트 신뢰성)</code></pre><hr>
<h2 id="1-패키지-import">1. 패키지 import</h2>
<pre><code class="language-systemverilog">module tb_top;
    import uvm_pkg::*;
    `include &quot;uvm_macros.svh&quot;
    import axi4_pkg::*;</code></pre>
<pre><code>uvm_pkg          → UVM 클래스들 (uvm_test, uvm_config_db 등)
uvm_macros.svh   → `uvm_info, `uvm_error 같은 매크로
axi4_pkg         → 우리가 만든 것들
                   (axi4_seq_item, AXI_DATA_WIDTH, axi_burst_e 등)</code></pre><hr>
<h2 id="2-파라미터--버스-폭">2. 파라미터 — 버스 폭</h2>
<pre><code class="language-systemverilog">localparam int DATA_WIDTH = 32;
localparam int ADDR_WIDTH = 16;
localparam int STRB_WIDTH = DATA_WIDTH/8;   // = 4
localparam int ID_WIDTH   = 8;</code></pre>
<pre><code>주석: &quot;must match axi4_pkg + the DUT instance below&quot;
  → 이 값이 패키지, 인터페이스, DUT 세 곳에서 일치해야 함
  → 안 맞으면 신호 폭이 어긋나서 데이터 손실!

localparam = 지역 상수 (외부에서 못 바꿈) → 실수 방지</code></pre><hr>
<h2 id="3-클록--리셋-생성">3. 클록 &amp; 리셋 생성</h2>
<h3 id="클록">클록</h3>
<pre><code class="language-systemverilog">logic clk;
initial clk = 0;
always #5 clk = ~clk;                 // 100 MHz</code></pre>
<pre><code>initial clk = 0;      → 시작할 때 0
always #5 clk = ~clk; → 5ns마다 반전(toggle)

5ns HIGH + 5ns LOW = 10ns 주기 = 100MHz
1초 / 10ns = 100,000,000 = 100MHz

     ┌──┐  ┌──┐  ┌──┐
─────┘  └──┘  └──┘  └──
     5ns  10ns 15ns 20ns</code></pre><h3 id="리셋">리셋</h3>
<pre><code class="language-systemverilog">logic rst;
initial begin
    rst = 1&#39;b1;                    // 리셋 ON
    repeat (10) @(posedge clk);    // 10 클록 유지
    rst = 1&#39;b0;                    // 리셋 OFF (해제)
end</code></pre>
<pre><code>  ┌────────────────┐
──┘                └──────────
  0    10클록(100ns)

왜 10클록? DUT 내부 모든 FF/상태가 확실히 초기화되도록</code></pre><p>이게 driver/monitor에서 봤던 <code>@(negedge vif.rst)</code>가 기다리던 그 신호다. 100ns 시점에 rst가 1→0으로 떨어지면 드라이버와 모니터가 깨어나 동작 시작.</p>
<hr>
<h2 id="4-인터페이스-인스턴스">4. 인터페이스 인스턴스</h2>
<pre><code class="language-systemverilog">axi4_if #(
    .DATA_WIDTH(DATA_WIDTH),
    .ADDR_WIDTH(ADDR_WIDTH),
    .STRB_WIDTH(STRB_WIDTH),
    .ID_WIDTH(ID_WIDTH)
) axi (
    .clk(clk),
    .rst(rst)
);</code></pre>
<pre><code>axi4_if를 &quot;axi&quot;라는 이름으로 실체화
.clk(clk), .rst(rst) → 위에서 만든 클록/리셋 주입

→ 인터페이스가 &quot;clk, rst를 포트로 받는다&quot;던 부분이 여기서 실제 주입됨!
→ 이제 axi.awvalid, axi.wdata 등 모든 AXI 신호가 존재
  드라이버가 vif.master_cb로 흔들고, 모니터가 vif.monitor_cb로 관찰할 대상</code></pre><hr>
<h2 id="5-dut-인스턴스--신호-연결">5. DUT 인스턴스 — 신호 연결</h2>
<pre><code class="language-systemverilog">`DUT_MODULE #(
    .DATA_WIDTH(DATA_WIDTH), ...
) dut (
    .clk(clk),
    .rst(rst),
    .s_axi_awid(axi.awid),
    .s_axi_awaddr(axi.awaddr),
    ...
);</code></pre>
<pre><code>`DUT_MODULE → axi_ram (또는 axi_ram_v2)로 컴파일 시점에 치환

신호 연결:
.s_axi_awid(axi.awid)
//  ↑ DUT 포트       ↑ 인터페이스 신호</code></pre><p>DUT는 plain Verilog라 개별 신호로 직접 연결한다 (interface를 못 받는 순수 모듈).</p>
<pre><code>     인터페이스 axi                    DUT
   ┌──────────────┐              ┌──────────────┐
   │ axi.awvalid  │─────────────→│ s_axi_awvalid│
   │ axi.awaddr   │─────────────→│ s_axi_awaddr │
   │ axi.awready  │←─────────────│ s_axi_awready│
   │ axi.rdata    │←─────────────│ s_axi_rdata  │
   └──────────────┘              └──────────────┘

방향은 인터페이스 신호가 결정:
  드라이버가 axi.awvalid 구동 → DUT의 s_axi_awvalid로 전달
  DUT가 s_axi_awready 구동 → axi.awready로 → 드라이버가 읽음</code></pre><hr>
<h2 id="6-uvm에-인터페이스-전달--테스트-시작-핵심">6. UVM에 인터페이스 전달 + 테스트 시작 (핵심)</h2>
<pre><code class="language-systemverilog">initial begin
    uvm_config_db#(virtual axi4_if)::set(null, &quot;*&quot;, &quot;vif&quot;, axi);
    run_test(&quot;axi4_base_test&quot;);
end</code></pre>
<p>이 두 줄이 <strong>하드웨어 세계와 UVM 세계를 연결</strong>하는 다리다.</p>
<h3 id="config_dbset--인터페이스-넘기기">config_db::set — 인터페이스 넘기기</h3>
<pre><code class="language-systemverilog">uvm_config_db#(virtual axi4_if)::set(null, &quot;*&quot;, &quot;vif&quot;, axi);
//                                    │    │    │      │
//                                    │    │    │      └ 넘길 값 (실제 인터페이스)
//                                    │    │    └ 키 이름 &quot;vif&quot;
//                                    │    └ 경로 &quot;*&quot; (모든 컴포넌트)
//                                    └ 시작점 null (전역)</code></pre>
<pre><code>&quot;axi 인터페이스를 &#39;vif&#39;라는 이름으로 UVM 전체에 등록&quot;

→ driver/monitor의 build_phase에서:
    uvm_config_db#(...)::get(this, &quot;&quot;, &quot;vif&quot;, ...)
  가 여기서 set한 걸 꺼내는 것!

set(여기서 넣음) ─────→ get(driver/monitor에서 꺼냄)</code></pre><h3 id="run_test--테스트-시작">run_test — 테스트 시작</h3>
<pre><code class="language-systemverilog">run_test(&quot;axi4_base_test&quot;);</code></pre>
<pre><code>UVM 테스트를 시작하는 함수

&quot;axi4_base_test&quot; → 기본 테스트 이름
  → 이 테스트가 env를 만들고 → agent → driver/monitor/sequencer
  → 전체 UVM 계층이 여기서 생성됨!

override 방법:
  +UVM_TESTNAME=axi4_wrap_test (커맨드라인)
  → 코드 수정 없이 다른 테스트 실행

run_test가 하는 일:
  1. +UVM_TESTNAME 확인 (없으면 인자값 사용)
  2. 그 이름의 test 클래스를 factory로 생성
  3. build → connect → run → ... phase 순서로 실행
  4. 모든 phase 끝나면 시뮬레이션 종료</code></pre><hr>
<h2 id="7-파형-덤프">7. 파형 덤프</h2>
<pre><code class="language-systemverilog">initial begin
    $dumpfile(&quot;waves.vcd&quot;);
    $dumpvars;
end</code></pre>
<pre><code>$dumpfile → 파형 저장할 파일 이름
$dumpvars → 모든 신호를 기록
→ waves.vcd 생성 → GTKWave 등으로 파형 디버깅</code></pre><hr>
<h2 id="전체-연결-관계">전체 연결 관계</h2>
<pre><code class="language-mermaid">flowchart TB
    subgraph TB[&quot;tb_top (최상단)&quot;]
        CLK[&quot;clk 생성&lt;br/&gt;#5 반전 = 100MHz&quot;]
        RST[&quot;rst 생성&lt;br/&gt;10클록 유지 후 해제&quot;]
        IF[&quot;axi4_if axi&lt;br/&gt;인터페이스 인스턴스&quot;]
        DUT[&quot;DUT 인스턴스&lt;br/&gt;axi_ram / axi_ram_v2&quot;]
        CFG[&quot;config_db::set(vif, axi)&quot;]
        RUN[&quot;run_test()&quot;]
    end

    UVM[&quot;UVM 환경&lt;br/&gt;test→env→agent→&lt;br/&gt;driver/monitor/sequencer&quot;]

    CLK --&gt; IF
    RST --&gt; IF
    CLK --&gt; DUT
    RST --&gt; DUT
    IF &lt;--&gt;|s_axi_* 개별 연결| DUT
    IF -.config_db.-&gt; CFG
    CFG -.vif 전달.-&gt; UVM
    RUN --&gt;|테스트 시작| UVM
    UVM -.vif.get.-&gt; IF</code></pre>
<hr>
<h2 id="지금까지-배운-것과-연결">지금까지 배운 것과 연결</h2>
<pre><code>tb_top이 만든 것 → 어디서 쓰이나

clk, rst        → driver/monitor의 @(negedge vif.rst)가 기다림
axi (인터페이스) → driver가 master_cb로 구동, monitor가 monitor_cb로 관찰
config_db::set  → driver/monitor의 build_phase에서 get으로 꺼냄
run_test        → test→env→agent→driver/monitor/sequencer/scoreboard 생성
DUT_V2 스위치   → v1(버그)/v2(수정) 전환 → 회귀로 버그 증명</code></pre><hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<pre><code>tb_top = 모든 부품을 조립하고 시뮬레이션을 시작하는 최상단

① 클록/리셋 생성 (100MHz, 10클록 리셋)
② 인터페이스 실체화 (axi)
③ DUT 연결 (s_axi_* 개별 신호 매핑)
④ config_db로 인터페이스를 UVM에 전달 + run_test로 테스트 시작
⑤ 파형 덤프

핵심 다리:
  config_db::set(vif) → driver/monitor가 get으로 받아 하드웨어 접근
  DUT_V2 define → 같은 TB로 버그/수정 버전 둘 다 검증</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (6) — ref_model & scoreboard]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-6-refmodel-scoreboard</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-6-refmodel-scoreboard</guid>
            <pubDate>Thu, 30 Jul 2026 05:01:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: 아이템 · 인터페이스 · 드라이버 · 모니터 · 관계 · <strong>ref_model/scoreboard</strong></p>
</blockquote>
<hr>
<h2 id="전체-그림--검증의-심판대">전체 그림 — 검증의 심판대</h2>
<p>모니터가 관찰한 트랜잭션이 최종적으로 &quot;맞는지 틀린지&quot; 판정되는 곳이다.</p>
<pre><code>Monitor가 관찰한 트랜잭션
        │ ap.write(tr)
        ↓
   ┌─────────────────────────────────────┐
   │          Scoreboard (심판)           │
   │                                     │
   │  WRITE → ref_model.apply_write()    │  &quot;이렇게 됐어야 해&quot;
   │  READ  → check_read()               │  &quot;실제로 맞나 비교&quot;
   │              │                      │
   │              ↓                      │
   │  ┌────────────────────────┐         │
   │  │  Reference Model        │         │
   │  │  (골든 모델 = 정답지)    │         │
   │  │  byte 단위 sparse memory │         │
   │  └────────────────────────┘         │
   └─────────────────────────────────────┘</code></pre><p><strong>핵심 분업:</strong></p>
<ul>
<li><strong>Reference Model</strong> = &quot;메모리가 이렇게 됐어야 한다&quot;를 계산하는 <strong>정답지</strong></li>
<li><strong>Scoreboard</strong> = 정답지와 DUT 실제 결과를 <strong>비교하는 심판</strong></li>
</ul>
<hr>
<h1 id="part-1-axi4_ref_model--골든-메모리-모델">Part 1: axi4_ref_model — 골든 메모리 모델</h1>
<h2 id="설계-철학-주석이-핵심">설계 철학 (주석이 핵심)</h2>
<pre><code>// Deliberately independent of the DUT&#39;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.</code></pre><p>이 주석이 <strong>&quot;골든 모델 독립 구현&quot;</strong> 원칙의 실제 구현이다.</p>
<pre><code>DUT 코드를 보고 만들면:
  DUT가 WRAP을 INCR처럼 처리 → 골든 모델도 똑같이 처리
  → 둘 다 틀렸는데 일치 → PASS → 버그 놓침 ❌

스펙만 보고 독립 구현:
  골든 모델은 beat_addr()로 &quot;올바른&quot; WRAP 주소 계산
  DUT는 &quot;틀린&quot; 선형 증가
  → 둘이 불일치 → 버그 노출 ✅

→ 이 파일이 WRAP 버그를 잡을 수 있었던 근본 이유</code></pre><hr>
<h2 id="핵심-오해-먼저-풀기">핵심 오해 먼저 풀기</h2>
<pre><code>❌ 오해: ref_model이 스펙 문서를 읽어서 미리 정답을 다 적어둔다
✅ 진실: ref_model은 모니터가 관찰한 &quot;실제 트랜잭션&quot;을 받아서
        그때그때 계산한다</code></pre><p>&quot;스펙 문서로 보고 만든다&quot;는 건 <strong>코드를 짤 때</strong> 얘기다. 실행 중에 문서를 읽는 게 아니라, 개발자가 스펙대로 <code>beat_addr()</code> 같은 <strong>계산 규칙</strong>을 코드에 박아둔 것이다.</p>
<p>정리하면:</p>
<ul>
<li>정답의 <strong>값</strong>은 관찰한 쓰기에서 온다 (0xAB를 쓰는 걸 보면 0xAB를 기억)</li>
<li>정답의 <strong>위치(주소)</strong>만 스펙을 따른다 (beat_addr가 WRAP/INCR를 스펙대로 계산)</li>
</ul>
<hr>
<h2 id="클래스-선언--메모리-구조">클래스 선언 &amp; 메모리 구조</h2>
<pre><code class="language-systemverilog">class axi4_ref_model extends uvm_object;
    `uvm_object_utils(axi4_ref_model)

    protected bit [7:0] mem [bit [AXI_ADDR_WIDTH-1:0]];</code></pre>
<h3 id="왜-uvm_component가-아니라-uvm_object인가">왜 uvm_component가 아니라 uvm_object인가</h3>
<pre><code>uvm_object 상속:
  → 계층 구조(hierarchy)에 안 들어감
  → phase(build/run/report)가 없음
  → 그냥 &quot;데이터를 담는 객체&quot;

이유:
  ref_model은 스코어보드가 소유하고 호출하는 도구
  독립적으로 phase를 돌 필요 없음 → 가벼운 uvm_object가 적합</code></pre><h3 id="mem--associative-array-연관-배열">mem — Associative Array (연관 배열)</h3>
<pre><code class="language-systemverilog">protected bit [7:0] mem [bit [AXI_ADDR_WIDTH-1:0]];
//         ↑ 값: 1바이트   ↑ 키: 주소</code></pre>
<p>이게 <strong>&quot;byte-granular sparse memory&quot;</strong>다.</p>
<pre><code>byte-granular (바이트 단위):
  값이 bit[7:0] = 1바이트 → 주소 하나당 1바이트 저장
  → write strobe를 정확히 모델링 가능 (특정 바이트만 쓰는 걸 그대로 반영)

sparse (희소):
  associative array = 실제로 쓴 주소만 저장
  → 64KB 전체를 배열로 잡지 않음 (메모리 절약)
  → mem[0x1000] 쓰면 그 주소만 생김</code></pre><pre><code>일반 배열 vs 연관 배열:

bit [7:0] mem [65536];        ← 64KB 전부 할당 (안 써도)
bit [7:0] mem [bit[15:0]];    ← 쓴 주소만 (sparse)</code></pre><hr>
<h2 id="함수-3개만-이해하면-끝">함수 3개만 이해하면 끝</h2>
<h3 id="1-read_byte--이-주소-값-뭐야">1. read_byte — &quot;이 주소 값 뭐야?&quot;</h3>
<pre><code class="language-systemverilog">function bit [7:0] read_byte(bit [AXI_ADDR_WIDTH-1:0] a);
    return mem.exists(a) ? mem[a] : 8&#39;h00;
endfunction</code></pre>
<pre><code>mem.exists(a): 이 주소에 값이 있나?
  있으면 → mem[a] 반환
  없으면 → 8&#39;h00 반환

왜 없으면 0?
  DUT가 시작 시 메모리를 0으로 초기화 → 안 쓴 주소는 0이어야 함
  → 모델도 안 쓴 주소를 0으로 예측 → DUT와 일치</code></pre><h3 id="2-apply_write--쓰기를-정답지에-반영-핵심">2. apply_write — &quot;쓰기를 정답지에 반영&quot; (핵심)</h3>
<p>DUT가 쓰기를 하면, 정답지에도 똑같이 써놓는 함수.</p>
<pre><code class="language-systemverilog">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 &lt; AXI_STRB_WIDTH; lane++)
            if (tr.strb[i][lane])
                mem[word_base + lane] = tr.data[i][8*lane +: 8];
    end
endfunction</code></pre>
<p>구체적 숫자로 따라가보면:</p>
<pre><code>상황: 0x1000에 데이터 0xDDCCBBAA 쓰기, strb = 4&#39;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는 안 씀 → 사전에 안 생김)</code></pre><p>이게 <strong>&quot;write strobe를 정확히 반영&quot;</strong>한다는 뜻이다. DUT가 특정 바이트만 썼으면, 정답지도 그 바이트만 쓴다.</p>
<h4 id="beat_addri가-버그를-잡는-지점"><code>beat_addr(i)</code>가 버그를 잡는 지점</h4>
<pre><code>a = tr.beat_addr(i);   ← 여기가 핵심!

beat_addr는 seq_item의 함수로 WRAP/INCR/FIXED 주소를 &quot;올바르게&quot; 계산

DUT는 WRAP을 틀리게 계산하는데
ref_model은 여기서 올바르게 계산
→ 나중에 둘이 안 맞아서 버그 노출</code></pre><h4 id="word_base--a---a--axi_strb_width--버스-폭-정렬"><code>word_base = a - (a % AXI_STRB_WIDTH)</code> — 버스 폭 정렬</h4>
<pre><code>주소 a를 버스 폭(4바이트) 배수로 내림 = &quot;a가 속한 워드의 시작 주소&quot;

a = 0x1006:
  0x1006 % 4 = 2
  word_base = 0x1006 - 2 = 0x1004

DUT는 메모리를 워드(4바이트) 단위로 다룸 (mem[addr&gt;&gt;2])
→ ref_model도 워드 시작을 알아야 바이트 레인을 정확히 매핑</code></pre><h4 id="8lane--8-문법"><code>[8*lane +: 8]</code> 문법</h4>
<pre><code>+: = &quot;이 위치부터 N비트&quot; (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이 변수라 에러
           +: 는 시작점이 변수여도 됨 (폭은 상수)</code></pre><h3 id="3-expected_beat--읽으면-뭐가-나와야-해">3. expected_beat — &quot;읽으면 뭐가 나와야 해?&quot;</h3>
<p>apply_write의 역방향. 읽기 검증 시 &quot;정답&quot;을 알려준다.</p>
<pre><code class="language-systemverilog">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 &lt; AXI_STRB_WIDTH; lane++)
        expected_beat[8*lane +: 8] = read_byte(word_base + lane);
endfunction</code></pre>
<pre><code>apply_write:   data → mem (쓰기)
expected_beat: mem → 예상 data (읽기)

한 beat에 대해:
  1. beat_addr(i)로 올바른 주소 계산
  2. word_base로 정렬
  3. 4개 레인의 바이트를 mem에서 읽어 32비트 워드 조립</code></pre><p>왜 전체 워드를 조립하나:</p>
<pre><code>DUT는 읽기 시 32비트 워드 전체를 반환 (narrow transfer라도 워드 전체)
→ 모델도 워드 전체를 조립
→ 그중 유효한 레인만 스코어보드가 mask로 비교</code></pre><hr>
<h2 id="세-함수의-관계">세 함수의 관계</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/83498930-97b2-402e-bddc-0d3678439b1f/image.png" alt=""></p>
<pre><code>쓸 때:   apply_write로 mem 채움
읽을 때: expected_beat로 &quot;정답&quot; 만들어서 스코어보드에 줌
        → 스코어보드가 DUT 실제 값과 비교</code></pre><hr>
<h1 id="part-2-axi4_scoreboard--심판">Part 2: axi4_scoreboard — 심판</h1>
<h2 id="역할">역할</h2>
<pre><code>&quot;DUT가 준 답&quot;과 &quot;정답지(ref_model)&quot;를 비교해서
맞았는지 틀렸는지 판정하는 채점관</code></pre><hr>
<h2 id="클래스-선언--analysis-port">클래스 선언 &amp; analysis port</h2>
<pre><code class="language-systemverilog">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;</code></pre>
<h3 id="uvm_analysis_imp--모니터의-반대편">uvm_analysis_imp — 모니터의 반대편</h3>
<pre><code>Monitor:     uvm_analysis_port  #(...)  ap;      ← 보내는 쪽
Scoreboard:  uvm_analysis_imp   #(...)  ap_imp;  ← 받는 쪽

imp = implementation → analysis port로 들어온 데이터를 받는 포트</code></pre><h3 id="axi4_seq_item-axi4_scoreboard-파라미터"><code>#(axi4_seq_item, axi4_scoreboard)</code> 파라미터</h3>
<pre><code>uvm_analysis_imp #(T, IMP)
  T   = axi4_seq_item     ← 받을 데이터 타입
  IMP = axi4_scoreboard   ← write() 함수를 가진 클래스 (나 자신!)

imp는 데이터를 받으면 IMP의 write() 함수를 자동 호출
→ 그래서 스코어보드에 write() 함수를 반드시 구현해야 함</code></pre><h3 id="통계-카운터">통계 카운터</h3>
<pre><code>n_writes    : 쓰기 버스트 개수
n_reads     : 읽기 버스트 개수
n_beats     : 검사한 beat 개수
n_mismatch  : 바이트 미스매치 개수 ← 이게 0이 아니면 버그!</code></pre><hr>
<h2 id="new--build_phase">new() &amp; build_phase()</h2>
<pre><code class="language-systemverilog">function new(string name, uvm_component parent);
    super.new(name, parent);
    ap_imp = new(&quot;ap_imp&quot;, this);           // analysis imp 생성
endfunction

function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ref_model = axi4_ref_model::type_id::create(&quot;ref_model&quot;);
endfunction</code></pre>
<pre><code>ap_imp = new(...)          → new()에서 (TLM 포트라 Factory 아님)
ref_model = create(...)    → build_phase에서 (Factory로, override 가능)

포트는 계층 연결에 필요 → 생성자에서 즉시
ref_model은 factory override 대상 → build_phase에서</code></pre><hr>
<h2 id="write--진입점">write() — 진입점</h2>
<pre><code class="language-systemverilog">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</code></pre>
<pre><code>모니터가 ap.write(tr) 하면 → 이 함수가 자동 호출됨

WRITE 버스트: &quot;메모리가 이렇게 됐어야 해&quot; → 모델에 반영 (정답지 갱신)
READ 버스트:  &quot;실제로 맞나?&quot; → check_read로 비교 (채점)</code></pre><h3 id="순서가-중요--write-먼저-read-나중">순서가 중요 — write 먼저, read 나중</h3>
<pre><code>시퀀스: 0x1000에 쓰고 → 0x1000 읽기
  1. write(0x1000) → apply_write → 모델 갱신
  2. write(0x1000) → check_read → 모델과 비교

→ 쓰기가 먼저 반영돼야 읽기 비교가 정확
→ 모니터가 in-order로 보내는 게 전제</code></pre><hr>
<h2 id="check_read--실제-비교-핵심">check_read() — 실제 비교 (핵심)</h2>
<pre><code class="language-systemverilog">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 &lt; 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(&quot;SB_MISMATCH&quot;, $sformatf(
                    &quot;READ mismatch @beat %0d lane %0d: addr=0x%04h burst=%s size=%0d len=%0d | expected 0x%02h, got 0x%02h&quot;,
                    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</code></pre>
<h3 id="단계별-흐름">단계별 흐름</h3>
<pre><code>exp  = ref_model.expected_beat(...)  → 정답지에게 &quot;이 주소 읽으면 뭐가 나와야 해?&quot;
mask = tr.lane_mask(i)               → &quot;이 beat에서 실제로 볼 바이트가 어디야?&quot;</code></pre><h3 id="mask로-유효-레인만-비교-매우-중요">mask로 유효 레인만 비교 (매우 중요)</h3>
<pre><code>// Only the byte lanes that the beat actually addresses are compared:
// for a narrow transfer the other lanes are don&#39;t-care in AXI, so
// checking them would produce false failures on a different slave.</code></pre><pre><code>narrow transfer(부분 전송)에서:
  버스는 32비트인데 실제로는 일부 레인만 유효
  나머지 레인은 AXI상 don&#39;t-care (아무 값이나 와도 됨)

모든 레인을 비교하면?
  → don&#39;t-care 레인의 쓰레기 값 때문에 false failure

→ mask[lane]이 1인 레인만 비교 → 유효한 데이터만 검사 → false failure 방지</code></pre><h3 id="-4-state-비교"><code>!==</code> 4-state 비교</h3>
<pre><code>!== : 4-state 비교 (X/Z까지 정확히)

왜 != 아니고 !== ?
  DUT가 X를 반환하면:
    != → 결과 X (불확실) → if가 애매하게 동작
    !== → 명확히 &quot;다름&quot; → 미스매치로 확실히 잡음

→ seq_item do_compare의 === 와 같은 원리, X 전파 버그를 놓치지 않음</code></pre><h3 id="에러-메시지--디버깅-정보-총동원">에러 메시지 — 디버깅 정보 총동원</h3>
<pre><code>미스매치 시 출력:
  @beat i, lane      → 어느 beat, 어느 바이트에서
  addr=beat_addr(i)  → 정확한 주소 (WRAP이면 계산된 주소!)
  burst=WRAP         → 어떤 버스트 타입에서
  size, len          → 전송 크기/길이
  expected vs got    → 기대값 vs 실제값

→ WRAP 버그 발생 시:
  addr가 &quot;올바른 WRAP 주소&quot;로 찍히고, DUT는 그 주소에 다른 값을 넣음
  → 버그 위치 특정 가능</code></pre><hr>
<h2 id="report_phase--최종-성적표">report_phase() — 최종 성적표</h2>
<pre><code class="language-systemverilog">function void report_phase(uvm_phase phase);
    super.report_phase(phase);
    `uvm_info(&quot;SB&quot;, $sformatf(
        &quot;scoreboard: %0d write bursts, %0d read bursts, %0d beats checked, %0d byte mismatches&quot;,
        n_writes, n_reads, n_beats, n_mismatch), UVM_LOW)
endfunction</code></pre>
<pre><code>시뮬레이션 끝에 출력:
  scoreboard: 42 write bursts, 40 read bursts, 269 beats checked, 0 byte mismatches

n_mismatch = 0 → 모든 읽기가 예상과 일치 ✅
n_mismatch &gt; 0 → 버그! (uvm_error가 이미 났을 것)

이력서의 &quot;24 byte-level mismatches&quot;가 바로 이 n_mismatch
→ WRAP 버그 있을 때 이 숫자가 24로 나왔던 것</code></pre><hr>
<h1 id="part-3-전체-데이터-흐름">Part 3: 전체 데이터 흐름</h1>
<h2 id="write-경로--read-경로-버그-검출">Write 경로 &amp; Read 경로 (버그 검출)</h2>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/2196a351-a81f-4643-a1ef-3c8ad6939dff/image.png" alt=""></p>
<h2 id="왜-이-구조가-wrap-버그를-잡았나">왜 이 구조가 WRAP 버그를 잡았나</h2>
<pre><code>1. ref_model이 beat_addr()로 &quot;올바른&quot; WRAP 주소 사용
   → DUT의 &quot;틀린&quot; 선형 주소와 다름

2. 비대칭(WRAP write + INCR read):
   ref_model: WRAP write를 올바른 주소에 반영
   DUT:       WRAP write를 틀린 주소에 씀
   INCR read: 양쪽 다 올바른 주소로 읽음
   → DUT가 틀린 곳에 썼으니 그 주소엔 원하는 값 없음
   → 미스매치 노출!</code></pre><hr>
<h1 id="전체-판정-흐름도">전체 판정 흐름도</h1>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/fe5b4d81-3773-455b-9442-8506e200b65c/image.png" alt=""></p>
<hr>
<h1 id="한눈에-정리">한눈에 정리</h1>
<table>
<thead>
<tr>
<th></th>
<th>Reference Model</th>
<th>Scoreboard</th>
</tr>
</thead>
<tbody><tr>
<td><strong>역할</strong></td>
<td>정답지 (메모리 예측)</td>
<td>심판 (비교/판정)</td>
</tr>
<tr>
<td><strong>베이스</strong></td>
<td>uvm_object (가벼움)</td>
<td>uvm_scoreboard</td>
</tr>
<tr>
<td><strong>메모리</strong></td>
<td>sparse byte array</td>
<td>없음 (모델에 위임)</td>
</tr>
<tr>
<td><strong>핵심 함수</strong></td>
<td>apply_write / expected_beat</td>
<td>write / check_read</td>
</tr>
<tr>
<td><strong>독립성</strong></td>
<td>DUT 안 보고 스펙만</td>
<td>—</td>
</tr>
<tr>
<td><strong>버그 검출</strong></td>
<td>beat_addr()로 올바른 주소</td>
<td>mask로 유효 레인만 비교</td>
</tr>
</tbody></table>
<pre><code>핵심:
  ref_model = &quot;관찰한 쓰기 값&quot;을 &quot;스펙대로의 주소&quot;에 기억해뒀다가
              읽기 검증 때 정답으로 제시하는 장부
              (값은 관찰에서, 주소 규칙은 스펙에서)

  scoreboard = DUT 답을 정답지와 비교하는 채점관
               자기가 직접 메모리 계산 안 함 → ref_model에게 물어보고 비교만</code></pre><hr>
<h2 id="인터뷰-대비-예상-질문">인터뷰 대비 예상 질문</h2>
<pre><code>Q. 왜 골든 모델을 byte 단위로 만들었나?
A. write strobe를 정확히 모델링하기 위해. AXI는 바이트 레인 단위로
   선택적 쓰기가 가능한데, 워드 단위 모델이면 이걸 표현 못 함.

Q. sparse memory를 쓴 이유는?
A. 64KB 전체를 배열로 잡으면 낭비. 연관 배열로 쓴 주소만 저장하고,
   안 쓴 주소는 DUT 초기값 0x00으로 예측.

Q. narrow transfer에서 왜 mask로 걸러 비교하나?
A. narrow transfer는 일부 레인만 유효하고 나머지는 AXI상 don&#39;t-care.
   전부 비교하면 don&#39;t-care 레인 때문에 false failure가 남.

Q. ref_model은 정답을 어디서 아나?
A. 미리 아는 게 아님. 모니터가 관찰한 쓰기 트랜잭션이 지나갈 때
   그 값을 저장. 주소만 스펙(beat_addr)대로 계산.

Q. 이 구조가 어떻게 WRAP 버그를 잡았나?
A. ref_model이 beat_addr()로 올바른 WRAP 주소를 쓰는데 DUT는 선형
   증가시킴. 비대칭 WRAP-write/INCR-read로 두 주소 계산이 갈라지게
   해서 미스매치를 노출.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (5) — 네 파일의 관계]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-5-%EB%84%A4-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-5-%EB%84%A4-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Thu, 30 Jul 2026 04:54:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈 마무리: 아이템 · 인터페이스 · 드라이버 · 모니터가 어떻게 맞물리나</p>
</blockquote>
<hr>
<h2 id="네-파일의-관계">네 파일의 관계</h2>
<pre><code>                    axi4_seq_item (아이템)
              &quot;AXI 버스트 하나&quot;의 공통 표현
         ┌──────────────────────────────────────┐
         │ Sequence가 randomize → Driver가 구동  │
         │ Monitor가 재조립 → Scoreboard가 비교  │
         └──────────────────────────────────────┘
                          ▲   ▲
          같은 객체 타입을 │   │ 공유
                          │   │
┌────────────────────────────────────────────────────────┐
│                     axi4_if.sv                          │
│   master_cb (드라이브)      monitor_cb (샘플)           │
│   #1step / #1               #1step only                 │
└──────────┬────────────────────────┬────────────────────┘
           │ vif.master_cb          │ vif.monitor_cb
    ┌──────▼──────┐          ┌──────▼─────┐
    │ axi4_driver │          │axi4_monitor│
    │             │          │            │
    │ 큐: 보낼 것 │          │ 큐: 본 것  │
    │ aw_q,w_q,   │          │ aw_q,      │
    │ ar_q        │          │ w_done_q,  │
    │             │          │ ar_q       │
    │ pop → 구동  │          │ [0]참조→채움│
    │ item_done() │          │ ap.write() │
    │  즉시!      │          │            │
    └──────┬──────┘          └─────┬──────┘
           │                        │
           │   axi4_seq_item        ▼
           └───────┬─────────→ Scoreboard
                              (do_compare로 판정)</code></pre><pre><code class="language-mermaid">flowchart LR
    ITEM[&quot;axi4_seq_item&lt;br/&gt;(아이템)&lt;br/&gt;버스트 1개 표현&quot;]

    SEQ[&quot;Sequence&quot;] --&gt;|randomize| ITEM
    ITEM --&gt;|beat_addr 공유| REF[&quot;Reference Model&quot;]
    ITEM --&gt; DRV[&quot;axi4_driver&quot;]
    IF[&quot;axi4_if&lt;br/&gt;(clocking block)&quot;] -.master_cb.-&gt; DRV
    IF -.monitor_cb.-&gt; MON[&quot;axi4_monitor&quot;]
    DRV --&gt;|핀 구동| BUS[&quot;AXI 버스&quot;]
    BUS --&gt;|핀 관찰| MON
    MON --&gt;|재조립한 아이템| SB[&quot;Scoreboard&quot;]
    REF --&gt;|예측한 아이템| SB
    SB --&gt;|do_compare| RESULT[&quot;pass / fail&quot;]</code></pre>
<h3 id="역할-대비표">역할 대비표</h3>
<table>
<thead>
<tr>
<th></th>
<th>axi4_seq_item</th>
<th>axi4_if</th>
<th>axi4_driver</th>
<th>axi4_monitor</th>
</tr>
</thead>
<tbody><tr>
<td><strong>역할</strong></td>
<td>버스트 1개 표현 + 합법성 규칙</td>
<td>신호 + 타이밍 규칙 정의</td>
<td>큐 → 버스 (구동)</td>
<td>버스 → 큐 (관찰)</td>
</tr>
<tr>
<td><strong>핵심 자산</strong></td>
<td>constraint, beat_addr()</td>
<td>master_cb, monitor_cb</td>
<td>6개 스레드</td>
<td>5개 스레드</td>
</tr>
<tr>
<td><strong>Clocking Block</strong></td>
<td>—</td>
<td>둘 다 제공</td>
<td>master_cb 사용</td>
<td>monitor_cb 사용</td>
</tr>
<tr>
<td><strong>큐/데이터</strong></td>
<td>동적 배열 data[], strb[]</td>
<td>—</td>
<td><code>pop_front()</code> 후 구동</td>
<td><code>[0]</code> 참조하며 채움</td>
</tr>
<tr>
<td><strong>핵심 트릭</strong></td>
<td>post_randomize 마스킹</td>
<td>#1step race 방지</td>
<td><code>item_done()</code> 즉시</td>
<td><code>[0]</code>을 pop 없이 참조</td>
</tr>
<tr>
<td><strong>검증 관여</strong></td>
<td>비교 기준 제공 (do_compare)</td>
<td>없음</td>
<td>없음 (구동만)</td>
<td>발행만 (판정은 SB)</td>
</tr>
</tbody></table>
<h3 id="파라미터-일관성">파라미터 일관성</h3>
<pre><code>아이템:     AXI_DATA_WIDTH, AXI_ADDR_WIDTH, AXI_STRB_WIDTH (패키지)
인터페이스: DATA_WIDTH=32, ADDR_WIDTH=16, STRB_WIDTH=4

이 둘이 다르면:
  seq_item.data는 64비트인데 vif.wdata는 32비트
  → 상위 비트 잘림 → 조용한 데이터 손실!

해결: package의 파라미터를 인터페이스 default로 사용해 단일 소스화</code></pre><h3 id="네-파일이-함께-만드는-이슈">네 파일이 함께 만드는 이슈</h3>
<pre><code>아이템 규칙: WRAP은 len {1,3,7,15}, 4KB 경계 준수 (constraint)
드라이버:    beat_addr()로 WRAP 주소 계산해 구동
      +
드라이버 이슈: W가 AW보다 먼저 나갈 수 있음 (채널 독립)
      +
모니터 가정: AW를 먼저 봤다고 가정 (aw_q[0]에 채움)
      ↓
특정 backpressure 패턴에서 모니터가 W beat를 놓침!

→ 둘 중 하나를 고쳐야 함
   (드라이버: AW 핸드셰이크 후 w_q push / 모니터: W 버퍼링)</code></pre><hr>
<h2 id="부록-레쥬메-표현-예시">부록: 레쥬메 표현 예시</h2>
<p><strong>Item (seq_item):</strong></p>
<blockquote>
<p>Encoded AXI4 protocol legality (4KB boundary, size/address alignment, WRAP beat-count
rules) directly into SystemVerilog constraints, and implemented burst-accurate address
prediction shared by driver and reference model — exposing a DUT bug where WRAP bursts
incremented linearly.</p>
</blockquote>
<p><strong>Interface:</strong></p>
<blockquote>
<p>Designed a parameterized AXI4 SystemVerilog interface with separate driver/monitor
clocking blocks (sample/drive skews) and direction-enforcing modports, eliminating
clock-edge race conditions at compile time.</p>
</blockquote>
<p><strong>Driver:</strong></p>
<blockquote>
<p>Built a pipelined AXI4 master driver with per-channel threads and configurable
outstanding depth, instrumented with peak-concurrency counters that quantitatively
prove multi-outstanding traffic reached the DUT — the same driver degrades to
strictly serialized behavior against single-outstanding slaves without code changes.</p>
</blockquote>
<p><strong>Monitor:</strong></p>
<blockquote>
<p>Architected an outstanding-aware, channel-parallel AXI4 monitor using per-phase FIFO
correlation, preventing transaction loss under multi-outstanding traffic.</p>
</blockquote>
<hr>
<h2 id="인터뷰-대비-예상-질문">인터뷰 대비 예상 질문</h2>
<ol>
<li><p><strong>왜 clocking block을 두 개 만들었나?</strong>
마스터는 드라이브+샘플이 섞여있고 모니터는 순수 샘플만 해야 하는데,
하나의 CB로는 두 역할의 방향을 동시에 표현할 수 없어서.</p>
</li>
<li><p><strong>blocking 드라이버와 pipelined 드라이버의 차이는?</strong>
<code>item_done()</code>을 언제 부르냐. 즉시 부르면 시퀀스가 버스보다 앞서 달려
Outstanding이 생김.</p>
</li>
<li><p><strong>wr_peak 카운터는 왜 필요한가?</strong>
Outstanding 미지원 슬레이브는 이 값을 1 이상 못 올림.
따라서 이 카운터가 &quot;파이프라인이 실제로 동작했다&quot;는 증거.</p>
</li>
<li><p><strong>모니터가 aw_q[0]을 pop하지 않고 참조만 하는 이유는?</strong>
Handle 복사로 큐 안 객체를 직접 채우기 위해. WLAST에서야 pop.</p>
</li>
<li><p><strong>W가 AW보다 먼저 나가면 무슨 일이 생기나?</strong>
AXI상 합법이지만 이 모니터는 AW를 먼저 봤다고 가정 → beat 유실 가능.</p>
</li>
<li><p><strong>시퀀스가 seq_item을 재사용하면 왜 위험한가?</strong>
파이프라인 드라이버는 큐에 Handle을 저장 → 원본 재랜덤화 시 큐 내용도 변경.
Deep copy 또는 매번 create로 방지.</p>
</li>
<li><p><strong>do_compare에서 == 대신 ===를 쓰는 이유는?</strong>
==는 2-state라 X/Z가 있으면 결과가 X(불확실). ===는 4-state로 X까지 정확히
비교 → DUT가 X를 뱉는 버그를 명확히 불일치로 잡음.</p>
</li>
<li><p><strong>WRAP 버스트를 어떻게 검증했나?</strong>
beat_addr()가 정확한 WRAP window 계산을 하고, DUT는 WRAP을 INCR처럼 선형
증가시키는 버그가 있어 스코어보드에서 미스매치로 포착됨.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (4) — axi4_monitor]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-4-axi4monitor</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-4-axi4monitor</guid>
            <pubDate>Thu, 30 Jul 2026 04:53:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: 아이템 · 인터페이스 · 드라이버 · <strong>모니터</strong></p>
</blockquote>
<hr>
<h2 id="axi4_monitor--채널-병렬-옵저버">axi4_monitor — 채널 병렬 옵저버</h2>
<h3 id="전체-구조">전체 구조</h3>
<pre><code>        AXI 버스 (5채널)
             │
   ┌─────┬───┼───┬─────┬─────┐
   ↓     ↓   ↓   ↓     ↓     ↓
mon_aw mon_w mon_b mon_ar mon_r   ← 5개 스레드 병렬
   │     │     │     │     │
   └──aw_q──w_done_q──┘     │
         (write 파이프)      │
                    ar_q ────┘
                          │
                          ↓
                    ap.write(tr) → Scoreboard / Coverage</code></pre><p><img src="https://velog.velcdn.com/images/houston_guy2/post/a2960f00-a26f-4e8c-a87f-caabb938134b/image.png" alt=""></p>
<p>핵심: <strong>채널마다 독립 스레드 + 큐(FIFO)로 phase를 이어붙임</strong>
(드라이버가 &quot;구동&quot;을 채널 병렬로 했듯, 모니터는 &quot;관찰&quot;을 채널 병렬로)</p>
<hr>
<h3 id="왜-이-구조인가">왜 이 구조인가</h3>
<pre><code>순차 &quot;AW → W → B&quot; 루프의 함정:

T=0: AW #1 잡음
T=1: AW #2 발생 → W beat 수집 중이라 못 봄! ❌
...

→ Outstanding 발생 순간 조용히 데이터 유실 (에러도 안 남)

채널 병렬:
  mon_aw는 오직 AW만 감시 → 절대 안 놓침
  큐로 phase 연결 → Outstanding depth 몇이든 캡처 ✅</code></pre><hr>
<h3 id="큐-3개--모니터의-심장">큐 3개 — 모니터의 심장</h3>
<pre><code class="language-systemverilog">protected axi4_seq_item aw_q[$];      // AW 봤음, W 아직 안 끝남
protected axi4_seq_item w_done_q[$];  // W 끝남, B 기다리는 중
protected axi4_seq_item ar_q[$];      // AR 봤음, R 아직 안 끝남</code></pre>
<pre><code>Write 생애:  AW핸드셰이크 → [aw_q] → (WLAST) → [w_done_q] → (B) → ap.write()
Read 생애:   AR핸드셰이크 → [ar_q] → (RLAST) → ap.write()
             (읽기는 R에 RRESP 같이 오므로 B 단계 불필요)</code></pre><hr>
<h3 id="run_phase--5개-스레드">run_phase() — 5개 스레드</h3>
<pre><code class="language-systemverilog">task run_phase(uvm_phase phase);
    @(negedge vif.rst);      // 리셋 해제 대기 (X 관찰 방지)
    fork
        mon_aw();  mon_w();  mon_b();  mon_ar();  mon_r();
    join                     // 전부 forever → 시뮬 끝까지 유지
endtask</code></pre>
<pre><code>join 선택 이유: 5개 태스크 전부 forever → 절대 안 끝남
              → join이 영원히 블록 → run_phase가 시뮬 끝까지 유지
              (join_none이면 phase 종료로 판단해 스레드가 죽을 수 있음)

모니터는 objection을 걸지 않음 (Driver/Sequence가 관리)</code></pre><hr>
<h3 id="mon_aw--쓰기-주소-관찰">mon_aw() — 쓰기 주소 관찰</h3>
<pre><code class="language-systemverilog">task mon_aw();
    axi4_seq_item tr;
    forever begin
        @(vif.monitor_cb);
        if (vif.monitor_cb.awvalid &amp;&amp; vif.monitor_cb.awready) begin  // 핸드셰이크
            tr       = axi4_seq_item::type_id::create(&quot;mon_wr&quot;);
            tr.dir   = AXI_WRITE;
            tr.id    = vif.monitor_cb.awid;
            tr.addr  = vif.monitor_cb.awaddr;
            tr.len   = vif.monitor_cb.awlen;
            tr.size  = vif.monitor_cb.awsize;
            tr.burst = axi_burst_e&#39;(vif.monitor_cb.awburst);   // enum cast
            tr.data  = new[tr.len + 1];      // 배열 미리 할당
            tr.strb  = new[tr.len + 1];
            aw_q.push_back(tr);
        end
    end
endtask</code></pre>
<pre><code>@(vif.monitor_cb): Clocking Block의 #1step 덕에 race-free 샘플링
awvalid &amp;&amp; awready: 골든 룰 4 → 핸드셰이크 성립 = 주소 전송 완료
enum cast: awburst(2&#39;b01) → AXI_INCR → 로그 가독성 &amp; 비교 편의
new[len+1]: AWLEN=3 → 4 beat → 배열 크기 4</code></pre><hr>
<h3 id="mon_w--쓰기-데이터-관찰-핵심">mon_w() — 쓰기 데이터 관찰 (핵심)</h3>
<pre><code class="language-systemverilog">task mon_w();
    axi4_seq_item tr;
    int unsigned  beat = 0;      // forever 반복 사이에 값 유지
    forever begin
        @(vif.monitor_cb);
        if (vif.monitor_cb.wvalid &amp;&amp; vif.monitor_cb.wready) begin
            if (aw_q.size() == 0)
                `uvm_warning(&quot;MON&quot;, &quot;W beat with no outstanding AW - ignored&quot;)
            else begin
                tr = aw_q[0];            // ⚠️ pop 안 함! 참조만!
                if (beat &lt;= tr.len) begin
                    tr.data[beat] = vif.monitor_cb.wdata;
                    tr.strb[beat] = vif.monitor_cb.wstrb;
                end
                if (vif.monitor_cb.wlast) begin
                    void&#39;(aw_q.pop_front());     // 이제 제거
                    w_done_q.push_back(tr);      // B 대기로 이동
                    beat = 0;
                end
                else beat++;
            end
        end
    end
endtask</code></pre>
<pre><code>핵심 트릭: tr = aw_q[0] (pop 아님!)
  tr은 Handle 복사 → tr 수정 = 큐 안 객체 수정 (의도된 동작!)
     aw_q[0] ──┐
                ├──→ [같은 Object]
     tr ───────┘
  tr.data[beat]=... → 큐 안 객체가 직접 채워짐 ✅

범위 가드 (beat &lt;= tr.len): WLAST 안 오고 beat 계속 오면
  배열 범위 밖 접근 방지

void&#39;(): pop_front() 반환값 안 쓴다는 표시 (경고 억제)</code></pre><hr>
<h3 id="mon_b--쓰기-응답-관찰">mon_b() — 쓰기 응답 관찰</h3>
<pre><code class="language-systemverilog">task mon_b();
    forever begin
        @(vif.monitor_cb);
        if (vif.monitor_cb.bvalid &amp;&amp; vif.monitor_cb.bready) begin
            if (w_done_q.size() == 0)
                `uvm_warning(&quot;MON&quot;, &quot;B response with no completed write burst&quot;)
            else begin
                tr      = w_done_q.pop_front();
                tr.resp = axi_resp_e&#39;(vif.monitor_cb.bresp);
                ap.write(tr);        // Scoreboard/Coverage로 발행! 🎉
            end
        end
    end
endtask</code></pre>
<pre><code>B 핸드셰이크 = 쓰기 완료 → w_done_q에서 pop → resp 저장 → ap.write()

ap.write(tr)는 Handle 전달
→ 모니터는 매번 create()로 새 객체 → 재사용 안 함 → 안전
→ 단, Scoreboard가 저장할 땐 copy()로 독립 복사본 필수</code></pre><hr>
<h3 id="mon_ar--mon_r--읽기-채널">mon_ar() / mon_r() — 읽기 채널</h3>
<pre><code class="language-systemverilog">// mon_ar: mon_aw와 거의 동일, 유일한 차이:
foreach (tr.strb[j]) tr.strb[j] = &#39;1;   // 읽기는 STRB 없음 → 전부 1

// mon_r: mon_w와 구조 같지만 2가지 차이
tr.resp = axi_resp_e&#39;(vif.monitor_cb.rresp);   // 차이1: 응답이 R에 함께
if (vif.monitor_cb.rlast) begin                // 차이2: RLAST에서 바로 발행
    void&#39;(ar_q.pop_front());
    ap.write(tr);        // B 대기 없이 즉시!
    beat = 0;
end</code></pre>
<pre><code>차이 1 — 응답이 데이터 채널에 함께
  읽기는 별도 응답 채널 없음 → RLAST 시점에 바로 ap.write()

차이 2 — RRESP는 beat마다 옴 (⚠️ 이슈)
  tr.resp = ... 를 매 beat 덮어씀
  → 중간 beat의 SLVERR가 마지막 OKAY에 덮여 사라질 수 있음</code></pre><hr>
<h3 id="타임라인--outstanding-2개">타임라인 — Outstanding 2개</h3>
<pre><code>사이클  버스 이벤트     큐 상태
─────  ─────────────  ─────────────────────
 1     AW #1          aw_q=[tr1]
 2     AW #2          aw_q=[tr1, tr2]   ← 순차 구현이면 유실!
 3     W beat (tr1)   aw_q=[tr1, tr2]
 4     W WLAST (tr1)  aw_q=[tr2], w_done_q=[tr1]
 5     W beat (tr2)   aw_q=[tr2], w_done_q=[tr1]
 6     W WLAST (tr2)  aw_q=[], w_done_q=[tr1, tr2]
 7     B #1           w_done_q=[tr2]  → tr1 발행
 8     B #2           w_done_q=[]     → tr2 발행</code></pre><hr>
<h3 id="모니터-구조-다이어그램">모니터 구조 다이어그램</h3>
<h4 id="스레드-구조--데이터-흐름">스레드 구조 &amp; 데이터 흐름</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/a2960f00-a26f-4e8c-a87f-caabb938134b/image.png" alt=""></p>
<p>채널마다 독립 스레드로 관찰하고, 큐로 phase를 이어붙여 Outstanding 트랜잭션을 유실 없이 캡처.</p>
<h4 id="write-트랜잭션-생애-큐-이동">Write 트랜잭션 생애 (큐 이동)</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/667c20aa-d423-41a7-a42d-8c80943825ab/image.png" alt=""></p>
<pre><code>읽기는 더 단순 (B 단계 없음):
AR 핸드셰이크 → ar_q → R beats 채움 → RLAST에서 바로 ap.write()
(R 채널에 RRESP가 같이 오므로 별도 응답 채널 불필요)</code></pre><h4 id="mon_w-내부-로직-가장-복잡한-스레드">mon_w 내부 로직 (가장 복잡한 스레드)</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/36c4c6e1-ea1f-4af1-b9ba-7b7c424ae828/image.png" alt=""></p>
<pre><code>핵심 트릭: tr = aw_q[0] (pop 아님!)
  → Handle 복사라 tr 수정 = 큐 안 객체 직접 채움
  → WLAST에서야 pop_front()로 제거</code></pre><h4 id="드라이버-vs-모니터-큐-방향-대비">드라이버 vs 모니터 큐 방향 대비</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/8def850a-8963-4f5f-aae0-e14c840e88c4/image.png" alt=""></p>
<pre><code>드라이버: 큐 → 버스  (pop 후 구동)
모니터:   버스 → 큐  ([0] 참조하며 채우고 발행)
방향이 정확히 반대</code></pre><h4 id="전체-마인드맵">전체 마인드맵</h4>
<pre><code class="language-mermaid">mindmap
  root((axi4_monitor))
    설계철학
      채널 병렬 관찰
      순차 루프는 Outstanding 유실
      passive - monitor_cb만 사용
    5개 스레드
      mon_aw - 쓰기 주소
      mon_w - 쓰기 데이터
      mon_b - 쓰기 응답
      mon_ar - 읽기 주소
      mon_r - 읽기 데이터
    3개 상관 큐
      aw_q - AW봤음 W채우는중
      w_done_q - W끝남 B대기
      ar_q - AR봤음 R채우는중
    핵심 트릭
      &quot;[0] 참조 - pop 안 함&quot;
      Handle 복사로 직접 채움
      WLAST/RLAST에서 발행
    이슈
      같은 사이클 AW+W 레이스
      RRESP 덮어쓰기
      ID 재정렬 미지원</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (3) — axi4_driver]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-3-axi4driver</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-3-axi4driver</guid>
            <pubDate>Thu, 30 Jul 2026 04:47:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: 아이템 · 인터페이스 · <strong>드라이버</strong> · 모니터</p>
</blockquote>
<hr>
<h2 id="axi4_driver--파이프라인-마스터-드라이버">axi4_driver — 파이프라인 마스터 드라이버</h2>
<h3 id="전체-구조">전체 구조</h3>
<pre><code>              Sequencer
                  │ get_next_item()
                  ↓
         ┌── item_thread ──┐  ← item_done()을 &quot;즉시&quot; 호출
         │                 │     (파이프라인의 핵심!)
         ↓                 ↓
    [aw_q][w_q]         [ar_q]
       │    │              │
       ↓    ↓              ↓
   aw_thread w_thread   ar_thread    ← 각 채널 독립 드라이브
       │    │              │
       └────┴──── AXI 버스 ┴────┐
                  │             │
             b_thread      r_thread  ← 응답 수집 + 카운터 감소</code></pre><hr>
<h3 id="왜-이-구조인가--blocking과-비교">왜 이 구조인가 — Blocking과 비교</h3>
<h4 id="❌-전형적인-blocking-드라이버">❌ 전형적인 Blocking 드라이버</h4>
<pre><code class="language-c">forever begin
    seq_item_port.get_next_item(req);
    drive_transaction(req);        // AW → W → B 전부 대기
    seq_item_port.item_done();     // 버스에서 끝난 후에야 호출
end</code></pre>
<pre><code>→ 버스가 놀아도 다음 트랜잭션이 안 들어옴
→ Outstanding 절대 발생 안 함
→ 슬레이브의 Outstanding 지원 로직이 검증 안 됨!</code></pre><h4 id="✅-이-드라이버">✅ 이 드라이버</h4>
<pre><code class="language-c">task item_thread();
    forever begin
        seq_item_port.get_next_item(req);
        aw_q.push_back(req);
        w_q.push_back(req);
        n_inflight++;
        seq_item_port.item_done();     // ← 즉시 호출!
    end
endtask</code></pre>
<pre><code>→ 시퀀스가 버스보다 앞서 달림 → 버스에 여러 버스트 동시 존재 ✅

item_done()을 &quot;언제&quot; 부르느냐가 blocking과 pipelined를 가르는 유일한 차이</code></pre><h4 id="헤더-주석의-설계-의도">헤더 주석의 설계 의도</h4>
<pre><code>max_outstanding = 1  → blocking 드라이버와 동일
max_outstanding = &gt;1 → pipelined
→ 같은 코드로 두 모드 커버, 회귀에서 둘 다 검증 가능

Outstanding 미지원 슬레이브:
  AWREADY를 낮게 유지 → aw_thread가 while에서 자동 블록
  → 드라이버 수정 없이 자동 적응 (graceful degradation)</code></pre><hr>
<h3 id="멤버-변수">멤버 변수</h3>
<pre><code class="language-c">class axi4_driver extends uvm_driver #(axi4_seq_item);</code></pre>
<pre><code>uvm_driver #(T) 상속 시 자동 생성:
  T req;                              ← 요청 아이템 (built-in)
  uvm_seq_item_pull_port #(T) seq_item_port;  ← 시퀀서 연결</code></pre><h4 id="채널별-작업-큐">채널별 작업 큐</h4>
<pre><code class="language-c">protected axi4_seq_item aw_q[$];   // AW 발행 대기
protected axi4_seq_item w_q[$];    // W beat 전송 대기
protected axi4_seq_item ar_q[$];   // AR 발행 대기</code></pre>
<pre><code class="language-c">모니터 큐와 방향 정반대:
  모니터: 관찰한 것을 쌓아 완성되면 발행
  드라이버: 보낼 것을 쌓아 하나씩 꺼내 구동

같은 트랜잭션이 aw_q, w_q에 동시에 들어감 (Handle 2개, Object 1개)
→ aw_thread는 주소만, w_thread는 데이터만 읽어 충돌 없음</code></pre>
<h4 id="inflight--계측-카운터">Inflight &amp; 계측 카운터</h4>
<pre><code class="language-c">protected int unsigned n_inflight;          // 시퀀서 기준 진행 중
int unsigned wr_outstanding, wr_peak;       // 버스 기준 진행 중
int unsigned rd_outstanding, rd_peak;</code></pre>
<pre><code>n_inflight     = &quot;시퀀서에서 받은 것&quot; (큐 포함)
wr_outstanding = &quot;버스에서 진행 중&quot; (AW 수락 ~ B 응답)

차이가 중요:
  큐에 10개 쌓여도 슬레이브가 AWREADY 안 올리면
  n_inflight=10, wr_outstanding=1

wr_peak &gt;= 2 → &quot;진짜 파이프라인 동작&quot; 증거!
→ 검증 환경 자체를 검증하는 계측 (인터뷰 어필 포인트)</code></pre><hr>
<h3 id="is_idle--drain-판정">is_idle() — Drain 판정</h3>
<pre><code class="language-c">function bit is_idle();
    return (aw_q.size() == 0) &amp;&amp; (w_q.size() == 0) &amp;&amp;
           (ar_q.size() == 0) &amp;&amp; (n_inflight == 0);
endfunction</code></pre>
<pre><code>문제: 파이프라인 드라이버는 마지막 item_done() 후에도
      버스에 버스트가 남아있음 → 그냥 끝내면 응답 유실!

해결 (테스트에서):
  seq.start(sequencer);
  wait (env.agent.driver.is_idle());   // 드레인 대기
  phase.drop_objection(this);

4개 조건 전부 체크:
  큐(3개) == 0     → 대기 중인 게 없음
  n_inflight == 0  → 버스에서 진행 중인 게 없음</code></pre><hr>
<h3 id="run_phase--reset_signals">run_phase() &amp; reset_signals()</h3>
<pre><code class="language-c">task run_phase(uvm_phase phase);
    reset_signals();
    @(negedge vif.rst);
    @(vif.master_cb);       // 한 클록 더 대기
    fork
        item_thread();  aw_thread();  w_thread();
        b_thread();     ar_thread();  r_thread();
    join
endtask</code></pre>
<table>
<thead>
<tr>
<th>단계</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><code>reset_signals()</code></td>
<td>리셋 중 신호를 안전한 값으로</td>
</tr>
<tr>
<td><code>@(negedge vif.rst)</code></td>
<td>리셋 해제 대기</td>
</tr>
<tr>
<td><code>@(vif.master_cb)</code></td>
<td>리셋 해제가 클록과 무관한 시점일 수 있어 클록에 정렬</td>
</tr>
<tr>
<td><code>fork ... join</code></td>
<td>6개 스레드 기동</td>
</tr>
</tbody></table>
<h4 id="reset_signals--페이로드가-리셋-안-되는-이슈">reset_signals() — 페이로드가 리셋 안 되는 이슈</h4>
<pre><code>리셋되는 것:  awvalid/wvalid/arvalid (VALID 계열),
              bready/rready (READY 계열),
              awlock/awcache/awprot 등 사이드밴드

리셋 안 되는 것: awid/awaddr/awlen/awsize/awburst,
                 wdata/wstrb, arid/araddr/... → 첫 트랜잭션 전까지 X!

AXI상 합법 (VALID=0일 때 페이로드 don&#39;t-care)
하지만 실무: 파형 X 가득 → 디버깅 어려움, X 전파 위험
→ &quot;X-state propagation guard&quot;와 같은 맥락
→ 개선: awaddr &lt;= &#39;0; wdata &lt;= &#39;0; 등 명시적 초기화</code></pre><hr>
<h3 id="item_thread--파이프라인의-심장">item_thread() — 파이프라인의 심장</h3>
<pre><code class="language-c">task item_thread();
    forever begin
        seq_item_port.get_next_item(req);
        while (n_inflight &gt;= cfg.max_outstanding) @(vif.master_cb);  // throttle
        if (req.dir == AXI_WRITE) begin
            aw_q.push_back(req);
            w_q.push_back(req);      // same order -&gt; AXI write-data order
        end
        else ar_q.push_back(req);
        n_inflight++;
        seq_item_port.item_done();   // 즉시!
    end
endtask</code></pre>
<pre><code class="language-c">while (n_inflight &gt;= cfg.max_outstanding) @(vif.master_cb);  // throttle</code></pre>
<p>진행 중인 트랜잭션(In-flight) 개수가 허용치(Max Outstanding)에 도달하면, 빈자리가 날 때까지 클락을 기다려라</p>
<p>@(vif.master_cb);의 실제 의미</p>
<p>@ : 기다려라 (Wait for event)
vif : 저기 연결된 인터페이스를 타고 들어가서
.master_cb : 거기에 정의된 클락킹 블록을 참조해라.</p>
<p>// 하드웨어와 연결되는 실제 인터페이스 정의</p>
<pre><code class="language-c">interface axi_if(input logic clk, input logic rst_n);

    // Interface

    // 신호들 선언
    logic [31:0] awaddr;
    logic        awvalid;
    logic        awready;
    // ...

    // 클락킹 블록 선언: &quot;나는 clk의 상승 엣지(posedge)를 기준으로 동작하겠다&quot;
    clocking master_cb @(posedge clk);
        default input #1step output #1ns; // 셋업/홀드 타임 정의 (Race Condition 방지)
        output awaddr, awvalid;           // 내가(Master가) 쏠 신호들
        input  awready;                   // 내가(Master가) 읽을 신호들
    endclocking</code></pre>
<p>위의 인터페이스 코드에서 master_cb 옆에 @(posedge clk)라고 기준을 명시해 두었죠?
따라서 @(vif.master_cb);는 컴파일러가 볼 때 사실상 @(posedge clk);과 똑같이 동작하게 됩니다. 즉, &quot;이 클락킹 블록의 기준이 되는 클락 엣지(상승 엣지)가 한 번 칠 때까지 멈춰서 기다려라&quot;라는 의미가 됩니다.</p>
<p>그렇다면 왜 굳이 복잡하게 master_cb를 쓸까?
그냥 직관적으로 @(posedge vif.clk); 라고 쓰면 편할 텐데, 굳이 클락킹 블록을 참조하는 이유는 Race Condition(경쟁 상태)이라는 치명적인 버그를 막기 위해서입니다.</p>
<h4 id="throttle--outstanding-depth-제한">Throttle — Outstanding depth 제한</h4>
<pre><code>max_outstanding=4:
  n_inflight 0→1→2→3→4 통과, 4에서 블록 (4&gt;=4)
  B 응답 → n_inflight=3 → 통과

throttle이 get_next_item 뒤에 있는 이유:
  대기 중 item_done() 안 불림 → 시퀀스도 블록
  → 백프레셔가 시퀀스까지 자연스럽게 전달 ✅</code></pre><h4 id="aw_q와-w_q에-같은-순서로-push">aw_q와 w_q에 같은 순서로 push</h4>
<pre><code>AXI4에 WID 없음 → W beat는 AW와 같은 순서로 나가야 함
두 큐 같은 순서 채우고 둘 다 FIFO 소비 → 순서 보장 ✅</code></pre><h4 id="⚠️-숨은-함정--시퀀스가-객체-재사용-시">⚠️ 숨은 함정 — 시퀀스가 객체 재사용 시</h4>
<pre><code class="language-c">// 위험한 시퀀스
axi4_seq_item tr = create(&quot;tr&quot;);   // 한 번만 생성!
repeat(10) begin
    start_item(tr);
    tr.randomize();      // 같은 객체 다시 랜덤화!
    finish_item(tr);
end</code></pre>
<pre><code>드라이버 큐:
  1회차: aw_q=[tr]      tr.addr=0x1000
  2회차: tr.randomize() → tr.addr=0x2000  ← 큐 안의 것도 바뀜!
         aw_q=[tr,tr]   둘 다 0x2000!

→ &quot;Queue에 Object 넣고 원본 수정&quot; 문제 그대로
→ Blocking이면 안 생김, 파이프라인에서만 터지는 버그!

해결:
  - 시퀀스에서 매번 create (권장)
  - 또는 드라이버에서 req.clone()으로 deep copy</code></pre><hr>
<h3 id="aw_thread--ar_thread--주소-채널">aw_thread() / ar_thread() — 주소 채널</h3>
<pre><code class="language-c">task aw_thread();
    axi4_seq_item tr;
    forever begin
        while (aw_q.size() == 0) @(vif.master_cb);   // 폴링 대기
        tr = aw_q.pop_front();

        vif.master_cb.awid    &lt;= tr.id;
        vif.master_cb.awaddr  &lt;= tr.addr;
        vif.master_cb.awlen   &lt;= tr.len;
        vif.master_cb.awsize  &lt;= tr.size;
        vif.master_cb.awburst &lt;= tr.burst;
        vif.master_cb.awvalid &lt;= 1&#39;b1;              // ① VALID 올림
        @(vif.master_cb);                            // ② 한 클록
        while (!vif.master_cb.awready) @(...);        // ③ READY 대기 (VALID 유지)
        vif.master_cb.awvalid &lt;= 1&#39;b0;              // ④ 완료 후 내림
        wr_outstanding++;
        if (wr_outstanding &gt; wr_peak) wr_peak = wr_outstanding;
    end
endtask</code></pre>
<h4 id="골든-룰-준수-확인">골든 룰 준수 확인</h4>
<pre><code>룰 1 (Deadlock 방지): ①에서 awready 확인 없이 바로 VALID 올림 ✅
룰 2 (VALID 유지):    ③의 while 동안 awvalid=1 유지 ✅
룰 4 (전송 조건):     ③의 awready 감지 = 핸드셰이크 성립 ✅</code></pre><h4 id="연속-aw-발행-시-거동">연속 AW 발행 시 거동</h4>
<pre><code>핸드셰이크 후:
  awvalid &lt;= 0   (T=21ns 예약)
  loop → pop → awvalid &lt;= 1  (T=21ns 예약, 같은 시각!)
  → NBA 규칙: 나중 할당 승리 → awvalid=1 유지
  → back-to-back AW (AXI상 정상) ✅</code></pre><h4 id="카운터-시점">카운터 시점</h4>
<pre><code>wr_outstanding++ : AW 핸드셰이크 성립 후 (버스에 나갔을 때)
wr_outstanding-- : BVALID 수신 (b_thread)
n_inflight와 시점 다름 → 두 카운터 차이 = 큐 대기 개수</code></pre><blockquote>
<p>ar_thread는 aw_thread와 구조 완전 동일. 읽기는 데이터를 보낼 필요가 없어
(AR + R, 2채널) 쓰기(AW + W + B, 3채널)보다 단순.</p>
</blockquote>
<hr>
<h3 id="w_thread--쓰기-데이터-채널">w_thread() — 쓰기 데이터 채널</h3>
<pre><code class="language-c">task w_thread();
    axi4_seq_item tr;
    forever begin
        while (w_q.size() == 0) @(vif.master_cb);
        tr = w_q.pop_front();
        foreach (tr.data[i]) begin
            vif.master_cb.wdata  &lt;= tr.data[i];
            vif.master_cb.wstrb  &lt;= tr.strb[i];
            vif.master_cb.wlast  &lt;= (i == tr.len);   // 마지막 beat
            vif.master_cb.wvalid &lt;= 1&#39;b1;
            @(vif.master_cb);
            while (!vif.master_cb.wready) @(vif.master_cb);
            vif.master_cb.wvalid &lt;= 1&#39;b0;
            vif.master_cb.wlast  &lt;= 1&#39;b0;
        end
    end
endtask</code></pre>
<h4 id="wlast-생성">WLAST 생성</h4>
<pre><code class="language-c">vif.master_cb.wlast &lt;= (i == tr.len);</code></pre>
<pre><code>len=3: i=0→0, i=1→0, i=2→0, i=3→1  (마지막만 1) ✅

wlast는 매 beat 후 0으로 내림 → 안 내리면 다음 버스트 첫 beat에
wlast=1 → 프로토콜 위반!

⚠️ 방어 부족: data.size()가 len+1이 아니면 WLAST 위치 틀림
   더 안전: (i == tr.data.size()-1)</code></pre><h4 id="⚠️-큰-이슈--w가-aw보다-먼저-나갈-수-있음">⚠️ 큰 이슈 — W가 AW보다 먼저 나갈 수 있음</h4>
<pre><code>aw_thread와 w_thread가 완전 독립:

슬레이브가 AWREADY를 오래 낮게 유지 + WREADY는 높음
  → aw_thread: AW #1 대기 (블록)
  → w_thread:  W beats #1, #2 전부 전송!
  → AW #1도 안 나갔는데 W #2 데이터가 나감

AXI상 합법이지만 이 모니터와 충돌:
  mon_w: aw_q.size()==0 → &quot;W beat with no AW - ignored&quot; → beat 유실!

→ 개선: aw_thread에서 핸드셰이크 후 w_q에 push
        또는 모니터가 W-before-AW 버퍼링</code></pre><hr>
<h3 id="b_thread--r_thread--응답-채널">b_thread() / r_thread() — 응답 채널</h3>
<pre><code class="language-c">task b_thread();
    vif.master_cb.bready &lt;= 1&#39;b1;               // always ready
    forever begin
        @(vif.master_cb);
        if (vif.master_cb.bvalid) begin
            if (n_inflight &gt; 0)     n_inflight--;
            if (wr_outstanding &gt; 0) wr_outstanding--;
        end
    end
endtask

task r_thread();
    vif.master_cb.rready &lt;= 1&#39;b1;
    forever begin
        @(vif.master_cb);
        if (vif.master_cb.rvalid &amp;&amp; vif.master_cb.rlast) begin  // RLAST!
            if (n_inflight &gt; 0)     n_inflight--;
            if (rd_outstanding &gt; 0) rd_outstanding--;
        end
    end
endtask</code></pre>
<h4 id="b_thread-vs-r_thread-차이">b_thread vs r_thread 차이</h4>
<pre><code>쓰기: B 응답은 버스트당 1개 → bvalid만 체크
읽기: R beat는 여러 개 → RLAST로 버스트 끝 판단!

len=3 읽기: rlast=0,0,0,1 → 마지막에만 카운터--</code></pre><h4 id="설계-특징--이슈">설계 특징 &amp; 이슈</h4>
<pre><code>✅ always ready (bready/rready=1) → 응답 지연 안 시킴, 코드 단순
✅ &gt; 0 가드 → int unsigned 언더플로 방지 (0에서 -- → 거대한 수)

⚠️ BRESP/RDATA 안 읽음 → item_done() 이미 호출해서 시퀀스에 못 돌려줌
   → 검증은 Monitor+Scoreboard 담당 (일관된 철학)
⚠️ backpressure 미검증 → READY 항상 1이라 마스터 지연 시나리오 없음
   → 개선: 랜덤 bready/rready 지연 주입</code></pre><hr>
<h3 id="계측이-증명하는-것">계측이 증명하는 것</h3>
<pre><code class="language-c">function void report_phase(uvm_phase phase);
    `uvm_info(&quot;DRV&quot;, $sformatf(
        &quot;peak writes = %0d, peak reads = %0d (max_outstanding = %0d)&quot;,
        wr_peak, rd_peak, cfg.max_outstanding), UVM_LOW)
endfunction</code></pre>
<pre><code>출력: peak writes = 4 → 파이프라인 동작 ✅
      peak writes = 1 → 파이프라인 안 됨 (backpressure or 트래픽 부족)

개선: max_outstanding&gt;1인데 peak&lt;=1이면 uvm_warning 자동 발생</code></pre><hr>
<h3 id="inflight--max_outstanding--용어-심화">inflight &amp; max_outstanding — 용어 심화</h3>
<p>두 개념 모두 &quot;지금 처리 중인 트랜잭션&quot;과 관련되지만 미묘하게 다르다.</p>
<h4 id="inflight-뜻">inflight 뜻</h4>
<pre><code>in flight = &quot;비행 중&quot; = &quot;출발했지만 아직 착륙(완료) 안 한&quot;

트랜잭션이 &quot;출발했지만 아직 응답을 못 받은 상태&quot;

택배 비유:
  주문함 → [inflight: 배송 중!] → 도착 완료
  n_inflight = 지금 배송 트럭에 실려 오는 택배 개수</code></pre><pre><code class="language-c">protected int unsigned n_inflight;
// ++  : item_thread에서 큐에 넣을 때 (출발!)
// --  : b_thread(BVALID) 또는 r_thread(RLAST)에서 (도착!)</code></pre>
<pre><code>시퀀서 → [큐] → AXI 버스 → 응답
         └──── 이 구간에 있는 것들이 inflight ────┘

n_inflight = 3 → &quot;지금 3개 버스트가 처리 중 (응답 안 온 것 3개)&quot;</code></pre><h4 id="max_outstanding-뜻">max_outstanding 뜻</h4>
<pre><code>outstanding = &quot;미결/미처리된&quot; (금융의 &quot;미상환 대출&quot;과 같은 뉘앙스)
max_outstanding = &quot;동시에 진행 중일 수 있는 최대 개수&quot; (설정값)</code></pre><pre><code class="language-systemverilog">while (n_inflight &gt;= cfg.max_outstanding) @(vif.master_cb);
// cfg = agent config 객체, max_outstanding = 그 안의 설정값
// &quot;inflight가 max에 도달하면 더 안 보내고 대기&quot;</code></pre>
<h4 id="둘의-관계--주차장-비유">둘의 관계 — 주차장 비유</h4>
<pre><code>max_outstanding = 주차장 전체 자리 수 (예: 4칸, 고정)
n_inflight      = 지금 주차된 차 수 (0~4, 변동)

n_inflight ≤ max_outstanding  ← 항상 유지

4칸 다 차면 → 나가는 차(응답) 있을 때까지 새 차 대기

동작:
  n_inflight 0→1→2→3→4 통과, 4에서 블록 (4&gt;=4)
  B 응답 도착 → n_inflight=3 → 다시 통과</code></pre><h4 id="왜-제한을-두나">왜 제한을 두나</h4>
<pre><code>max_outstanding = 1  → 한 번에 하나씩 = blocking 드라이버
max_outstanding = 4  → 최대 4개 동시 = pipelined

→ 같은 드라이버 코드로 설정값만 바꿔
  &quot;직렬 모드&quot;와 &quot;파이프라인 모드&quot;를 둘 다 테스트!</code></pre><h4 id="⚠️-n_inflight-vs-wr_outstanding--시점이-다름">⚠️ n_inflight vs wr_outstanding — 시점이 다름</h4>
<pre><code>n_inflight     : 시퀀서에서 받는 순간 ++ (큐에 있을 때부터)
wr_outstanding : AW가 버스에서 수락된 순간 ++ (실제 버스에 나갔을 때)

큐에 5개 쌓였는데 슬레이브가 AWREADY를 안 올리면:
  n_inflight = 5      (받긴 다 받음)
  wr_outstanding = 1  (실제 버스엔 하나만)

→ 차이 = 큐에서 대기 중인 개수
→ wr_peak &gt;= 2 가 &quot;진짜 파이프라인 동작&quot; 증거</code></pre><hr>
<h3 id="드라이버-구조-다이어그램">드라이버 구조 다이어그램</h3>
<h4 id="스레드-구조--데이터-흐름">스레드 구조 &amp; 데이터 흐름</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/584300dd-8972-4e4a-a8e0-8d4ede378359/image.png" alt=""></p>
<p><code>item_thread</code>가 큐에 넣고 <code>item_done()</code>을 <strong>즉시</strong> 부르기 때문에,
시퀀스가 버스보다 앞서 달려서 여러 버스트가 동시에 진행된다.</p>
<h4 id="카운터-흐름-트랜잭션-생애">카운터 흐름 (트랜잭션 생애)</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/e97f59ac-b81a-4205-be84-0694a6669baa/image.png" alt=""></p>
<pre><code>n_inflight     : 큐에 있을 때부터 셈 (시퀀서 기준)
wr/rd_outstanding : 실제 버스에 나갔을 때부터 셈 (버스 기준)
차이 = 큐 대기 개수,  peak &gt;= 2 = 파이프라인 증거</code></pre><h4 id="throttle-메커니즘-max_outstanding-제한">Throttle 메커니즘 (max_outstanding 제한)</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/04020838-fdd0-42db-a582-493838fb07cd/image.png" alt=""></p>
<h4 id="aw_thread-핸드셰이크-상세-골든-룰">aw_thread 핸드셰이크 상세 (골든 룰)</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/1b6b3555-eadb-4ebc-864a-ed1239e81cba/image.png" alt=""></p>
<pre><code>룰 1 (Deadlock 방지) : ①에서 awready 확인 없이 바로 VALID 올림
룰 2 (VALID 유지)    : ③의 while 동안 awvalid=1 유지
룰 4 (전송 조건)     : ③의 awready 감지 = 핸드셰이크 성립</code></pre><h4 id="write-버스트-생명주기-시퀀스">Write 버스트 생명주기 시퀀스</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/45cc7e19-d0e5-4bd8-9da9-bc1588f403ca/image.png" alt=""></p>
<h4 id="전체-마인드맵">전체 마인드맵</h4>
<p><img src="https://velog.velcdn.com/images/houston_guy2/post/2bc7de63-61fc-4eb8-acab-25fe914c30a9/image.png" alt=""></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[AXI4 UVM (2) — axi4_if]]></title>
            <link>https://velog.io/@houston_guy2/AXI4-UVM-2-axi4if</link>
            <guid>https://velog.io/@houston_guy2/AXI4-UVM-2-axi4if</guid>
            <pubDate>Thu, 30 Jul 2026 04:39:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시리즈: 아이템 · <strong>인터페이스</strong> · 드라이버 · 모니터</p>
</blockquote>
<h2 id="axi4_if--인터페이스">axi4_if — 인터페이스</h2>
<h3 id="전체-구조">전체 구조</h3>
<pre><code>┌─────────────────────────────────────────────────────────┐
│                    interface axi4_if                     │
│                                                          │
│  파라미터: DATA_WIDTH, ADDR_WIDTH, STRB_WIDTH, ID_WIDTH │
│  포트:     clk, rst  (외부에서 주입)                     │
│                                                          │
│  ┌────────────────────────────────────────────────┐     │
│  │  raw 신호 5채널 (AW / W / B / AR / R)           │     │
│  └────────────────────────────────────────────────┘     │
│         ↑                          ↑                     │
│  ┌──────┴───────┐          ┌──────┴───────┐            │
│  │  master_cb   │          │  monitor_cb  │            │
│  │(드라이브+샘플)│          │  (샘플 전용) │            │
│  └──────┬───────┘          └──────┬───────┘            │
│         │                          │                     │
│   modport master            modport monitor             │
└─────────┼──────────────────────────┼────────────────────┘
          ↓                          ↓
    axi4_driver                axi4_monitor</code></pre><p>핵심: <strong>같은 신호를 두 개의 clocking block이 다른 방향/역할로 바라봄</strong></p>
<hr>
<h3 id="파라미터">파라미터</h3>
<pre><code class="language-systemverilog">interface axi4_if #(
    parameter int DATA_WIDTH = 32,
    parameter int ADDR_WIDTH = 16,
    parameter int STRB_WIDTH = (DATA_WIDTH/8),
    parameter int ID_WIDTH   = 8
) (
    input logic clk,
    input logic rst
);</code></pre>
<table>
<thead>
<tr>
<th>파라미터</th>
<th>값</th>
<th>의미</th>
<th>파생 결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>DATA_WIDTH</code></td>
<td>32</td>
<td>데이터 버스 폭</td>
<td>beat 하나 = 4바이트</td>
</tr>
<tr>
<td><code>ADDR_WIDTH</code></td>
<td>16</td>
<td>주소 폭</td>
<td><strong>64KB</strong> 주소 공간</td>
</tr>
<tr>
<td><code>STRB_WIDTH</code></td>
<td>4</td>
<td><code>DATA_WIDTH/8</code></td>
<td>바이트 레인 4개</td>
</tr>
<tr>
<td><code>ID_WIDTH</code></td>
<td>8</td>
<td>트랜잭션 ID 폭</td>
<td>최대 256개 ID</td>
</tr>
</tbody></table>
<h4 id="addr_width16과-4kb-규칙">ADDR_WIDTH=16과 4KB 규칙</h4>
<pre><code>2^16 = 64KB, 4KB 페이지 = 16개
0x0000~0x0FFF, 0x1000~0x1FFF, ..., 0xF000~0xFFFF

→ seq_item의 c_4k constraint가 이 16개 경계를 지키게 함
→ 주소 공간이 작아서 경계 위반 시나리오가 자주 발생 (좋은 검증 환경)</code></pre><h4 id="strb_width가-파생-파라미터인-이유">STRB_WIDTH가 파생 파라미터인 이유</h4>
<pre><code>DATA_WIDTH만 바꾸면 STRB_WIDTH가 자동으로 따라옴
DATA_WIDTH=32 → STRB=4,  DATA=64 → STRB=8

→ 두 개를 따로 지정하다 실수로 안 맞추는 버그 방지
→ 다만 localparam으로 선언하면 오버라이드까지 막을 수 있어 더 안전</code></pre><h4 id="clk-rst가-포트인-이유">clk, rst가 포트인 이유</h4>
<pre><code>인터페이스는 클록을 생성하지 않음! TB Top에서 만들어 주입.
인터페이스 안에서 만들면 인스턴스마다 클록이 생겨 동기화 문제 발생.

rst 극성: monitor의 @(negedge vif.rst) → Active-HIGH 리셋</code></pre><hr>
<h3 id="5채널-신호--axi-버전-정보가-드러나는-지점">5채널 신호 — AXI 버전 정보가 드러나는 지점</h3>
<h4 id="aw-채널-폭에서-읽는-axi4-시그니처">AW 채널 폭에서 읽는 AXI4 시그니처</h4>
<pre><code>awlen [7:0]  ← 8비트
  AXI3: AWLEN[3:0]  (최대 16 beats)
  AXI4: AWLEN[7:0]  (최대 256 beats)  ✅

awlock [0:0] ← 1비트
  AXI3: AWLOCK[1:0]  (normal/exclusive/locked)
  AXI4: AWLOCK       (locked 삭제)  ✅

→ 이 인터페이스는 정확히 AXI4 스펙</code></pre><h4 id="w-채널--wid가-없는-게-포인트">W 채널 — WID가 없는 게 포인트!</h4>
<pre><code>AXI3: WID 존재 → 쓰기 데이터 인터리빙 가능
AXI4: WID 제거! → 인터리빙 금지, W beat는 AW와 같은 순서로 연속

→ 그래서 monitor의 mon_w()가 aw_q[0](가장 오래된 것) 하나만
  보고 데이터를 채워도 안전한 것!
→ 드라이버가 aw_q와 w_q에 같은 순서로 push하는 것도 이 때문</code></pre><h4 id="쓰기-vs-읽기-비대칭">쓰기 vs 읽기 비대칭</h4>
<pre><code>        쓰기                       읽기
   ┌──────────┐              ┌──────────┐
   │ AW 채널  │              │ AR 채널  │
   └──────────┘              └──────────┘
   ┌──────────┐              ┌──────────────────┐
   │ W  채널  │              │ R 채널           │
   │ (데이터) │              │ (데이터 + rresp) │
   └──────────┘              └──────────────────┘
   ┌──────────┐                    ↑
   │ B  채널  │              별도 응답 채널 없음
   │ (응답)   │
   └──────────┘

쓰기: W(데이터) + B(응답) = 2채널
읽기: R(데이터+rresp)     = 1채널

→ rresp는 beat마다 존재 → monitor mon_r()의 덮어쓰기 이슈 원인</code></pre><h4 id="빠진-optional-신호">빠진 optional 신호</h4>
<pre><code>없는 것: awqos/arqos, awregion/arregion, awuser/aruser, wuser/buser/ruser
→ 스펙상 optional, 작은 환경에서는 생략 합리적
→ QoS 기반 아비터를 검증하려면 추가 필요</code></pre><hr>
<h3 id="master_cb--드라이버용-clocking-block">master_cb — 드라이버용 Clocking Block</h3>
<pre><code class="language-systemverilog">clocking master_cb @(posedge clk);
    default input #1step output #1;
    // AW
    output awid, awaddr, awlen, awsize, awburst,
           awlock, awcache, awprot, awvalid;
    input  awready;
    // W
    output wdata, wstrb, wlast, wvalid;
    input  wready;
    // B
    input  bid, bresp, bvalid;
    output bready;
    // AR
    output arid, araddr, arlen, arsize, arburst,
           arlock, arcache, arprot, arvalid;
    input  arready;
    // R
    input  rid, rdata, rresp, rlast, rvalid;
    output rready;
endclocking</code></pre>
<h4 id="skew-해석">skew 해석</h4>
<pre><code>input #1step  → 엣지 직전 안정된 값 읽기 (Preponed 영역)
output #1     → 엣지 후 1ns에 드라이브

        T=10ns (posedge)
             │
    ─────────┼──────┬──────
       ↑     │      │
    #1step   │    output #1
    (읽기)   │    (쓰기 T=11ns)</code></pre><blockquote>
<p>⚠️ <code>output #1</code>은 절대 시간(1ns). 클록 주기 10ns면 10%로 안전하지만,
2ns 클록이면 50%, 1ns 클록이면 충돌. 고속 클록에서는 재조정 필요.</p>
</blockquote>
<h4 id="방향-설계--axi-마스터-역할">방향 설계 — AXI 마스터 역할</h4>
<table>
<thead>
<tr>
<th>채널</th>
<th>master_cb output (드라이브)</th>
<th>master_cb input (샘플)</th>
</tr>
</thead>
<tbody><tr>
<td>AW</td>
<td>페이로드 + <code>awvalid</code></td>
<td><code>awready</code></td>
</tr>
<tr>
<td>W</td>
<td>페이로드 + <code>wvalid</code></td>
<td><code>wready</code></td>
</tr>
<tr>
<td>B</td>
<td><code>bready</code></td>
<td>페이로드 + <code>bvalid</code></td>
</tr>
<tr>
<td>AR</td>
<td>페이로드 + <code>arvalid</code></td>
<td><code>arready</code></td>
</tr>
<tr>
<td>R</td>
<td><code>rready</code></td>
<td>페이로드 + <code>rvalid</code></td>
</tr>
</tbody></table>
<pre><code>패턴:
  데이터 나가는 채널(AW,W,AR) → 마스터가 VALID 드라이브
  데이터 들어오는 채널(B,R)   → 마스터가 READY 드라이브

→ VALID는 보내는 쪽, READY는 받는 쪽이 올림 (골든 룰 구조)</code></pre><h4 id="컴파일-타임-안전장치">컴파일 타임 안전장치</h4>
<pre><code class="language-systemverilog">vif.master_cb.awready &lt;= 1&#39;b1;   // ❌ 컴파일 에러!
// awready는 input으로 선언됨 → 드라이브 불가</code></pre>
<hr>
<h3 id="monitor_cb--모니터용-clocking-block">monitor_cb — 모니터용 Clocking Block</h3>
<pre><code class="language-systemverilog">clocking monitor_cb @(posedge clk);
    default input #1step;
    input awid, awaddr, ..., awvalid, awready;
    input wdata, wstrb, wlast, wvalid, wready;
    input bid, bresp, bvalid, bready;
    input arid, araddr, ..., arvalid, arready;
    input rid, rdata, rresp, rlast, rvalid, rready;
endclocking</code></pre>
<h4 id="master_cb와의-차이-3가지">master_cb와의 차이 3가지</h4>
<pre><code>차이 1 — output이 하나도 없음
  default input #1step;  ← output skew조차 없음
  → 물리적으로 어떤 신호도 드라이브 불가 → 진짜 passive 보장 ✅
  vif.monitor_cb.awvalid &lt;= 1;  // ❌ 컴파일 에러

차이 2 — VALID와 READY를 둘 다 봄
  input awvalid, awready;  ← 둘 다!
  → 그래서 if(awvalid &amp;&amp; awready)로 핸드셰이크 감지 가능
  → 모니터가 트랜잭션을 감지하는 원리의 근거

차이 3 — 마스터/슬레이브 구분 없음
  모든 신호를 평등하게 input → 어디에 붙여도 동작 → 재사용성 ✅</code></pre><h4 id="두-cb가-같은-신호를-써도-괜찮은-이유">두 CB가 같은 신호를 써도 괜찮은 이유</h4>
<pre><code>awvalid: master_cb=output(드라이브), monitor_cb=input(샘플)
→ 드라이버 1개, 샘플러 1개 → 충돌 없음 ✅
(monitor_cb에 output이 없어서 multiple driver 문제 없음)</code></pre><hr>
<h3 id="modport">Modport</h3>
<pre><code class="language-systemverilog">modport master  (clocking master_cb,  input clk, rst);
modport monitor (clocking monitor_cb, input clk, rst);</code></pre>
<pre><code>각 modport가 노출하는 것:
  ✅ 해당 clocking block
  ✅ clk, rst (읽기 전용)
  ❌ raw 신호 직접 접근 불가!

목적: raw 신호로 clocking block을 우회하는 실수를 컴파일 타임에 차단
  vif.awvalid &lt;= 1;             // ❌ (스큐 우회 = race 위험)
  vif.master_cb.awvalid &lt;= 1;   // ✅ 이것만 허용</code></pre><blockquote>
<p>⚠️ 현재 UVM 클래스는 <code>virtual axi4_if vif;</code>로 modport 없는 순수 타입을 씀.
<code>virtual axi4_if.monitor vif;</code>처럼 modport를 명시하면 안전성이 올라감.</p>
</blockquote>
<blockquote>
<p>slave modport가 없는 이유: DUT가 plain Verilog라 개별 신호로 직접 연결.
UVM Slave Agent를 만들려면 <code>modport slave</code> 추가 필요.</p>
</blockquote>
<hr>
]]></description>
        </item>
    </channel>
</rss>