<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>young-0320.log</title>
        <link>https://velog.io/</link>
        <description>electronic engineering student</description>
        <lastBuildDate>Thu, 06 Aug 2026 03:56:40 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>young-0320.log</title>
            <url>https://velog.velcdn.com/images/young-0320/profile/e41a2585-780d-4c88-b2cc-05fa832d7407/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. young-0320.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/young-0320" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Zynq FIR IP 개발 #5 : 숫자로 쓰는 결산]]]></title>
            <link>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-5-%EC%88%AB%EC%9E%90%EB%A1%9C-%EC%93%B0%EB%8A%94-%EA%B2%B0%EC%82%B0</link>
            <guid>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-5-%EC%88%AB%EC%9E%90%EB%A1%9C-%EC%93%B0%EB%8A%94-%EA%B2%B0%EC%82%B0</guid>
            <pubDate>Thu, 06 Aug 2026 03:56:40 GMT</pubDate>
            <description><![CDATA[<p>지난 네 편은 전부 &quot;무엇이 안 됐고 왜 안 됐는가&quot;의 기록이었다. 블록 디자인을 처음 그리면서 헤맨 이야기, 그리고 세 편에 걸친 에러 트러블슈팅. </p>
<p>이번 편은 고친 것들이 모여서 결국 무엇이 되었는지만 다룬다. 자랑할 숫자와 자랑 못 할 숫자를 같이 놓는다. 마지막에는 이 프로젝트가 하지 못한 것도 정리한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>대상</td>
<td>N=43 anti-alias FIR + ↓2 데시메이터 IP (Zynq-7020)</td>
</tr>
<tr>
<td>기간</td>
<td>2026-03 ~ 2026-08 (6개월, 1인 수행)</td>
</tr>
<tr>
<td>결과</td>
<td>실보드 SNR 74.9dB PASS / Fmax 116→146MHz / 8192샘플 85.0µs / 보드 실측 2.21W</td>
</tr>
<tr>
<td>이 글에서 다루는 것</td>
<td>위 숫자들이 어디까지 믿을 만한가</td>
</tr>
</tbody></table>
<hr>
<h2 id="한-문단-요약--무엇을-만든-건가">한 문단 요약 — 무엇을 만든 건가</h2>
<p>100 MS/s 16-bit 입력을 안티에일리어싱 필터링과 함께 50 MS/s로 2:1 데시메이션하는 N=43 FIR IP다. 타겟은 Zynq-7020(Zybo Z7-20)이고, 데이터 경로는 이렇다. PC의 Python이 UART로 명령을 내리면 PS의 bare-metal C가 AXI DMA MM2S 채널로 입력 샘플을 PL의 FIR에 공급하고, 처리된 출력은 S2MM 채널로 DDR에 회수된 뒤 UART로 PC에 전달된다. PC는 이를 FFT로 시각화하는 동시에 Python 고정소수점 골든모델과 대조해 PASS/FAIL을 자동 판정한다.</p>
<pre><code class="language-text">PC(Python)
 ↓
UART
 ↓
PS bare-metal C
 ↓
AXI DMA MM2S
 ↓
PL FIR/decimator
 ↓
AXI DMA S2MM
 ↓
DDR
 ↓
UART
 ↓
PC FFT plot / 자동 판정</code></pre>
<p>설계 사양은 이렇다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>입력 / 출력 샘플레이트</td>
<td>100 MS/s → 50 MS/s (M=2)</td>
</tr>
<tr>
<td>탭 수 / 설계</td>
<td>N=43, Kaiser window lowpass (β ≈ 5.653)</td>
</tr>
<tr>
<td>통과대역 / 저지대역</td>
<td>fp 15 MHz / fs 25 MHz, 감쇠 ≥ 60 dB (worst-case 판정)</td>
</tr>
<tr>
<td>구조</td>
<td>Transposed form, 계수 Q1.15 / 내부 Q2.30, 1 sample/cycle</td>
</tr>
<tr>
<td>파이프라인</td>
<td>v1 3-stage → v2 4-stage</td>
</tr>
<tr>
<td>인터페이스</td>
<td>AXI-Stream (tvalid/tready/tlast)</td>
</tr>
</tbody></table>
<p>43은 처음부터 정한 숫자가 아니었다. Kaiser 차수 추정식에 이 사양(fp 15 MHz, fs 25 MHz, As 60 dB)을 넣으면 39탭이 나온다. 초기 작업 가정은 거기에 양자화 오차 여유를 +2 준 41탭이었다. 둘 다 결국 탈락했다. 
갈린 건 탭 수가 아니라 판정 기준을 어디에 두느냐였다. &quot;저지대역 감쇠 60 dB&quot;라고 쓸 때, 이걸 25 MHz 한 지점에서 재는 것과 f ≥ 25 MHz 전 구간의 worst-case로 재는 것은 다른 시험이다. 후자로 바꾸는 순간 순위가 뒤집혔다.</p>
<table>
<thead>
<tr>
<th>N</th>
<th align="right">25 MHz 감쇠</th>
<th align="right">저지대역 worst-case</th>
<th align="right">worst 발생 위치</th>
<th>판정</th>
</tr>
</thead>
<tbody><tr>
<td>39</td>
<td align="right">71.27 dB</td>
<td align="right">59.17 dB</td>
<td align="right">25.463 MHz</td>
<td>fail</td>
</tr>
<tr>
<td>41</td>
<td align="right">60.38 dB</td>
<td align="right">59.62 dB</td>
<td align="right">25.154 MHz</td>
<td>fail</td>
</tr>
<tr>
<td>43</td>
<td align="right">60.25 dB</td>
<td align="right">60.25 dB</td>
<td align="right">25.000 MHz</td>
<td>pass</td>
</tr>
</tbody></table>
<p>41탭이 함정이었다. 25 MHz 한 점만 보면 60.38 dB라 통과처럼 보인다. 그런데 조금 위인 25.154 MHz에서 59.62 dB까지 올라온다. 스펙에서 0.38 dB 모자란다. 사양서에 &quot;25 MHz에서 60 dB&quot;라고 적어놓고 그 한 점만 확인했다면 41로 확정하고 넘어갔을 것이다. 39탭은 더 직관적이지 않다. 25 MHz에서의 감쇠가 71.27 dB로 셋 중 제일 깊다. 그런데도 미달이다. 저지대역 안쪽 25.463 MHz에서 59.17 dB로 솟기 때문이다. Kaiser 윈도 설계의 저지대역은 평평한 벽이 아니라 리플이 있는 능선이라, 경계에서 깊다고 전 구간이 깊은 게 아니다.</p>
<p>43탭이 통과한 이유도 표에 그대로 있다. 25 MHz 감쇠(60.25 dB)와 저지대역 worst-case(60.25 dB)가 같은 값이고, 발생 위치가 정확히 25.000 MHz다. 저지대역 안쪽에 경계보다 나쁜 봉우리가 없다는 뜻 — 즉 필터가 설계 의도대로 저지대역 시작점을 최악점으로 갖는다. 41/39는 최악점이 경계 바깥으로 밀려나 있었다.</p>
<p>마지막으로 이 판정은 멀티톤 입력을 넣어서 한 게 아니라 계수의 주파수 응답 |H(f)|로 했다. 입력을 넣어 재면 &quot;이 입력에서 alias가 얼마나 남았나&quot;라는 시스템 질문의 답이 나오지, &quot;필터가 저지대역에서 60 dB를 주는가&quot;라는 필터 자체의 질문에는 답이 안 된다. 입력 스펙트럼을 바꾸면 결과가 따라 바뀌니까. Q1.15로 양자화한 계수로도 다시 확인해서 61.06 dB로 여전히 통과하는 걸 보고 43으로 확정했다(즉 이건 고정소수점 포맷 문제가 아니라 탭 수 문제였다).</p>
<p>추정식은 시작점이지 판정 기준이 아니다. 그리고 판정은 한 점이 아니라 구간에서 해야 한다.</p>
<h2 id="보드-출력-vs-골든모델">보드 출력 vs 골든모델</h2>
<p>아무리 빠르고 전력 적게 먹더라도 틀린 필터라면 아무 가치가 없다.</p>
<p>검증 방식은 보드가 뱉은 실제 출력을 Python 고정소수점 골든모델과 샘플 단위로 비교하는 것이다. 시뮬레이션 결과가 아니라 SD 부팅한 실보드에서 UART로 관측한 숫자다.</p>
<ul>
<li>보드 출력 4096 샘플 vs Python 고정소수점 골든모델</li>
</ul>
<table>
<thead>
<tr>
<th>지표</th>
<th align="right">시나리오 1-1 (5/20/30 MHz)</th>
<th align="right">시나리오 1-2 (7/15/25/45 MHz)</th>
</tr>
</thead>
<tbody><tr>
<td>판정</td>
<td align="right">PASS</td>
<td align="right">PASS</td>
</tr>
<tr>
<td>SNR</td>
<td align="right">74.863 dB</td>
<td align="right">72.216 dB</td>
</tr>
<tr>
<td>Max error</td>
<td align="right">6 LSB</td>
<td align="right">7 LSB</td>
</tr>
<tr>
<td>Mean error</td>
<td align="right">0.008 LSB</td>
<td align="right">0.002 LSB</td>
</tr>
<tr>
<td>RMSE</td>
<td align="right">1.403 LSB</td>
<td align="right">1.805 LSB</td>
</tr>
<tr>
<td>Correlation</td>
<td align="right">1.000000</td>
<td align="right">1.000000</td>
</tr>
<tr>
<td>보드 포화 발생</td>
<td align="right">0회</td>
<td align="right">0회</td>
</tr>
</tbody></table>
<p>톤별로 살펴보는 건 1-2만 한다. 1-1은 입력 30 MHz가 접히는 자리(20 MHz)에 입력 20 MHz 톤이 이미 있어서 두 성분이 같은 출력 위치에 겹친다. 그 자리의 피크가 어느 쪽 몫인지 분리할 수 없으니, 판정 스크립트도 두 톤을 PASS가 아니라 INFO로 리포트한다. 1-2는 7/15/25/45 MHz가 출력에서 7/15/25/5 MHz로 전부 다른 위치에 떨어져서 톤별 감쇠를 단독으로 읽을 수 있다. </p>
<ul>
<li>시나리오 1-2</li>
</ul>
<table>
<thead>
<tr>
<th>톤</th>
<th>대역</th>
<th align="right">보드 감쇠</th>
<th align="right">골든 감쇠</th>
<th align="right">보드−골든</th>
<th>판정</th>
</tr>
</thead>
<tbody><tr>
<td>7 MHz</td>
<td>통과대역</td>
<td align="right">−6.03 dB</td>
<td align="right">−6.03 dB</td>
<td align="right">−0.00 dB</td>
<td>PASS</td>
</tr>
<tr>
<td>15 MHz</td>
<td>통과대역 경계</td>
<td align="right">−6.03 dB</td>
<td align="right">−6.03 dB</td>
<td align="right">0.00 dB</td>
<td>PASS</td>
</tr>
<tr>
<td>25 MHz</td>
<td>전이대역</td>
<td align="right">−60.33 dB</td>
<td align="right">−60.25 dB</td>
<td align="right">−0.08 dB</td>
<td>INFO</td>
</tr>
<tr>
<td>45 MHz</td>
<td>저지대역 (→ 5 MHz alias)</td>
<td align="right">−64.51 dB</td>
<td align="right">−64.57 dB</td>
<td align="right">0.06 dB</td>
<td>PASS</td>
</tr>
</tbody></table>
<p><strong>통과대역 −6.03 dB는 감쇠가 아니다.</strong> 측정 스크립트의 정규화 때문에 나오는 값이다. <code>sw/fir_decimator_metrics.py</code>는 <code>np.abs(np.fft.rfft(sig))</code>로 정규화하지 않은 FFT 크기를 쓰고, 출력(4096샘플)의 dB도 입력(8192샘플)에서 잡은 같은 기준값(<code>input_ref</code>)으로 나눈다. 샘플 수가 절반이면 크기도 절반이 되니 −6.02 dB가 상수로 깔린다. 즉 저 숫자는 &quot;통과대역이 평탄하게 통과했다&quot;는 뜻이다. 7 MHz와 15 MHz가 소수점 둘째 자리까지 같은 값인 게 그 증거다.</p>
<p><strong>25 MHz −60.33 dB가 사양 그 자체다.</strong> &quot;저지대역 시작 25 MHz에서 60 dB 이상 감쇠&quot;라고 스펙에 써놓은 그 지점이 −60.33 dB로 나왔다.</p>
<p>여기서 방금 말한 −6.02 dB 오프셋이 왜 안 보이는지 짚어둘 필요가 있다. 25 MHz는 출력 샘플레이트 50 MS/s의 Nyquist 지점이다. rfft에서 Nyquist bin은 켤레 짝이 없어 크기가 일반 bin의 정확히 2배(+6.02 dB)로 잡히고, 이게 샘플 수 절반에서 오는 −6.02 dB와 상쇄된다. 그래서 <strong>이 한 지점에서만 표의 숫자가 필터의 실제 감쇠와 바로 같아진다.</strong> 계수 응답을 직접 계산하면 |H(25 MHz)| = 60.249 dB인데 골든이 −60.25 dB인 것이 그 결과다. 전이대역 경계라 하드 PASS 기준이 아닌 INFO로 리포트하지만, 사양을 아슬아슬하게 넘긴 게 아니라 설계한 대로 정확히 그 자리에 떨어졌다.</p>
<p><strong>45 MHz가 이 프로젝트의 존재 이유다.</strong> 50 MS/s로 데시메이션하면 45 MHz는 5 MHz로 접혀서(alias) 통과대역 한복판에 오염을 만든다. 안티에일리어싱 필터가 하는 일이 바로 이걸 막는 것이고, 출력 스펙트럼의 5 MHz 자리에 아무것도 없다.</p>
<p>숫자를 정확히 읽으면 이렇다. 계수 응답상 이 필터의 45 MHz 감쇠는 77.2 dB다. 그런데 표의 실측은 64.5 dB다. 12 dB 넘게 벌어진 건 필터가 거기까지만 눌러서가 아니라, 그 아래가 이 측정계의 바닥이기 때문이다. 8192 샘플 유한 레코드의 FFT로 재는 이상 −77 dB짜리 성분은 바닥에 묻힌다.</p>
<p>증거는 같은 표 안에 있다. 골든모델이 −64.57 dB로 0.06 dB 차이밖에 안 난다. 고정소수점을 빼고 float64로만 같은 경로를 돌려도 같은 자리에 떨어진다. 즉 이 바닥은 보드가 만든 것도 양자화가 만든 것도 아니라 <strong>측정 방법이 만든 것</strong>이다. 이 글의 판정에는 &quot;적어도 64.5 dB 이상 억제됐다&quot;로 충분했고, 더 내려가 보려면 측정 방식 자체를 바꿔야 한다.</p>
<p>그리고 보드−골든 차이가 전 톤에서 0.1 dB 이내다. 실리콘이 Python이 계산한 것과 같은 답을 냈다는 뜻이다.</p>
<h2 id="크리티컬-패스-분석--fmax-116-→-146-mhz">크리티컬 패스 분석 — Fmax 116 → 146 MHz</h2>
<p>여기서부터가 이번 편의 본론이다.</p>
<h3 id="주파수-스윕을-한-이유">주파수 스윕을 한 이유</h3>
<p>&quot;100 MHz에서 타이밍 통과&quot;는 사실 정보가 별로 없는 문장이다. 100 MHz가 한계인지, 200 MHz도 되는데 100으로 돌린 건지 알 수가 없으니까. 그래서 타이밍이 무너지는 지점을 찾기로 했다.</p>
<p>주파수 스윕을 할 때 중요한 부분은, PS7의 하드 PLL은 정수 분주만 된다는 것이다. 즉 117 MHz로 스윕하면 가장 가까운 값으로 스냅되어 버린다. 그러면 Fmax를 1 MHz 해상도로 잴 수가 없다. 그래서 Clocking Wizard(MMCM)로 PL 전체 클럭을 만들어 <strong>요청 = 실제</strong>가 되도록 하고, 1 MHz씩 올리면서 WNS(Worst Negative Slack)가 음수로 떨어지는 첫 지점을 찾았다.</p>
<p>이건 뒤늦게 고친 것이기도 하다. 처음 스윕은 PS7 클럭으로 했고 그때 나온 Fmax는 115 MHz였다. 라벨과 실제 주파수가 어긋나 있었다는 걸 방법론 재검증에서 발견하고 clk_wiz로 다시 잰 값이 아래 표다.</p>
<h3 id="v1-결과--fmax-116-mhz">v1 결과 — Fmax 116 MHz</h3>
<table>
<thead>
<tr>
<th>실제 주파수</th>
<th align="right">WNS</th>
<th>판정</th>
</tr>
</thead>
<tbody><tr>
<td>110 MHz</td>
<td align="right">+0.381 ns</td>
<td>pass</td>
</tr>
<tr>
<td>115 MHz</td>
<td align="right">+0.231 ns</td>
<td>pass</td>
</tr>
<tr>
<td>116 MHz</td>
<td align="right">+0.016 ns</td>
<td><strong>pass (마지막)</strong></td>
</tr>
<tr>
<td>117 MHz</td>
<td align="right">−0.071 ns</td>
<td>fail</td>
</tr>
<tr>
<td>118 MHz</td>
<td align="right">−0.044 ns</td>
<td>fail</td>
</tr>
<tr>
<td>119 MHz</td>
<td align="right">−0.205 ns</td>
<td>fail</td>
</tr>
<tr>
<td>120 MHz</td>
<td align="right">−0.098 ns</td>
<td>fail</td>
</tr>
</tbody></table>
<p><strong>v1 Fmax = 116 MHz.</strong></p>
<h3 id="크리티컬-패스-찾기">크리티컬 패스 찾기</h3>
<p>한계 주파수를 찾았으면 다음은 어느 경로가 그 한계를 만드는지가 중요하다. 정밀 스윕 이전에 120 MHz 빌드에서 이미 이 경로를 열어 본 적이 있었다.</p>
<pre><code class="language-text">Source:      u_fir_n43/z_reg[1][2]        (FIR 딜레이 레지스터)
Destination: u_fir_n43/round_reg_reg[45]  (라운딩 레지스터)
Data Path Delay: 8.664 ns   (요구 8.000 ns, 0.664 ns 초과)
  Logic  : 6.057 ns (69.9%) — CARRY4 19개 + LUT 4개, 총 23 로직 레벨
  Routing: 2.607 ns (30.1%)</code></pre>
<p>한 가지 밝혀둘 게 있다. 이건 clk_wiz 정밀 스윕 이전, 120 MHz 빌드의 리포트다. 정밀 스윕에서 117~120 MHz FAIL의 worst는 같은 데이터패스의 다른 시작점(<code>prod_reg_reg[0][2]_replica → round_reg_reg</code>)으로 잡혔다. <strong>시작점 이름은 배치마다 바뀌지만 도착점과 원인은 같다</strong> — 누산과 라운딩이 한 사이클에 겹친 그 경로다.</p>
<p>원인은 내 코드에 있었다. v1의 Stage 2는 누산과 라운딩을 한 사이클 안에서 같이 하고 있었다.</p>
<pre><code class="language-verilog">z[0] &lt;= prod_reg[0] + z[1];
// non-blocking이라 z[0]을 바로 못 읽어서, 같은 식을 한 번 더 계산
round_reg &lt;= round_q2_30_to_q1_15(prod_reg[0] + z[1]);</code></pre>
<p><code>round_q2_30_to_q1_15()</code> 내부는 <code>abs()</code> → <code>+ROUND_BIAS</code> → 부호 재적용의 3단 연쇄 48-bit 연산이다. 여기에 앞의 누산 덧셈까지 붙으면 한 클럭에 48-bit 덧셈이 최대 3회 직렬로 들어간다. 이게 CARRY4 19개짜리 캐리 체인으로 합성되어 8.664 ns를 만든 것이다.</p>
<p>핵심은 저 주석이다. non-blocking 할당이라 방금 만든 <code>z[0]</code>을 같은 사이클에서 읽을 수 없으니, 읽는 대신 <strong>다시 계산했다.</strong> 그 &quot;다시 계산&quot;이 그대로 크리티컬 패스가 됐다.</p>
<h3 id="v2--파이프라이닝을-통한-26-확보">v2 — 파이프라이닝을 통한 26% 확보</h3>
<p>고친 건 간단하다. 라운딩을 다음 사이클로 미뤄서, 이미 레지스터에 들어간 <code>z[0]</code>을 읽어서 쓰도록 했다. 재계산이 사라지니 누산 덧셈 1회분이 라운딩 경로에서 통째로 빠진다.</p>
<table>
<thead>
<tr>
<th>v1 (3-stage)</th>
<th>v2 (4-stage)</th>
</tr>
</thead>
<tbody><tr>
<td>Stage 1: 곱셈 → prod_reg</td>
<td>Stage 1: 곱셈 → prod_reg (동일)</td>
</tr>
<tr>
<td>Stage 2: 누산 + 라운딩 동시</td>
<td>Stage 2: 누산만</td>
</tr>
<tr>
<td>Stage 3: 포화 → 출력</td>
<td>Stage 3: round(z[0]) — 레지스터 값 참조</td>
</tr>
<tr>
<td></td>
<td>Stage 4: 포화 → 출력</td>
</tr>
</tbody></table>
<p>코어 latency는 3 → 4 사이클로 늘어난다(수락된 입력 기준, RTL 헤더 주석). 4편에서 &quot;v1=4 / v2=5 사이클&quot;이라고 쓴 것과 숫자가 다른데, 그건 래퍼에서 본 값이라 세는 구간이 하나 더 길다. 같은 설계의 다른 기준점이다.</p>
<p>어쨌든 늘어난 건 사실인데, 아래 모듈(decimator, AXI-Stream 래퍼)이 전부 valid 핸드셰이크 기반이라 고정 latency를 가정하는 곳이 없다. 그래서 RTL 수정은 이 파일 하나로 끝났다.</p>
<table>
<thead>
<tr>
<th>실제 주파수</th>
<th align="right">WNS</th>
<th>판정</th>
</tr>
</thead>
<tbody><tr>
<td>120 MHz</td>
<td align="right">+0.655 ns</td>
<td>pass</td>
</tr>
<tr>
<td>130 MHz</td>
<td align="right">+0.563 ns</td>
<td>pass</td>
</tr>
<tr>
<td>140 MHz</td>
<td align="right">+0.069 ns</td>
<td>pass</td>
</tr>
<tr>
<td>145 MHz</td>
<td align="right">+0.129 ns</td>
<td>pass</td>
</tr>
<tr>
<td>146 MHz</td>
<td align="right">+0.022 ns</td>
<td><strong>pass (마지막)</strong></td>
</tr>
<tr>
<td>147 MHz</td>
<td align="right">−0.102 ns</td>
<td>fail</td>
</tr>
<tr>
<td>148 MHz</td>
<td align="right">−0.021 ns</td>
<td>fail</td>
</tr>
<tr>
<td>150 MHz</td>
<td align="right">−0.012 ns</td>
<td>fail</td>
</tr>
</tbody></table>
<p><strong>v2 Fmax = 146 MHz.</strong> v1 대비 +30 MHz, +26%. 자원은 사실상 동급이다 — DSP48 16개 동일, 코어 기준 LUT 1,792 / FF 2,113으로 두 구조가 같고, 늘어난 건 파이프 FF 몇 개다. <strong>주파수 26%를 자원 증가 없이 구조 하나로 확보했다</strong>는 게 이 절의 핵심이다.</p>
<h3 id="스윕으로-관찰한-두-가지">스윕으로 관찰한 두 가지</h3>
<p><strong>(1) WNS는 주파수에 대해 단조롭게 줄지 않는다.</strong> 위 v2 표에서도 알 수 있다. 140 MHz의 WNS(+0.069)가 <strong>더 빠른</strong> 145 MHz(+0.129)보다 작다. FAIL 구간도 마찬가지여서 148 MHz(−0.021)가 147 MHz(−0.102)보다 낫다. v1 표에서도 118(−0.044)이 117(−0.071)보다 좋다.</p>
<p>제약이 빡빡해질수록 Vivado가 배치를 다르게 최적화하기 때문이다. 즉 <strong>WNS는 절대 주파수의 함수가 아니라 매 실행의 place&amp;route 결과다.</strong> 그래서 &quot;몇 MHz가 한계인가&quot;는 한 점만 보고 판단하면 안 되고, 구간으로 봐야 한다.</p>
<p><strong>(2) 그래서 배포는 Fmax로 안 한다.</strong> 146 MHz는 WNS +0.022 ns로 정적 마진이 거의 0이다. 온도·전압 변동에 간헐적으로 에러가 날 위험이 있다. 따라서 실보드 배포는 <strong>145 MHz</strong>로 정했다. 속도 손실은 0.7 MHz(0.7%)인데 타이밍 마진은 약 6배다. v1도 같은 이유로 116이 아니라 115 MHz를 배포본으로 쓴다.</p>
<h3 id="그리고-이제-병목은-fir-코어가-아니다">그리고, 이제 병목은 FIR 코어가 아니다</h3>
<p>v2의 147 MHz FAIL 경계에서 worst path를 열어 봤더니 이렇게 나왔다.</p>
<pre><code class="language-text">Source:      axi_dma_0/.../I_S2MM_REALIGNER/.../sig_max_first_increment_reg[1]
Destination: axi_dma_0/.../I_S2MM_REALIGNER/.../sig_btt_eq_0_reg</code></pre>
<p>Xilinx AXI DMA IP 내부(S2MM realigner의 byte-count 로직)다. 정확히 말하면 경계 부근에서는 이 DMA 경로와 <strong>AXIS 래퍼 경로가 비슷하게 임계</strong>여서, 배치에 따라 둘 중 하나가 worst가 된다. 래퍼는 남의 IP가 아니라 내 코드다. 그러니 &quot;이제 남의 IP 탓&quot;이라고 쓰면 틀리고, 정확하게 정리하자면 — <strong>어느 쪽도 FIR 코어의 곱셈·누산·라운딩 데이터패스가 아니다.</strong></p>
<p>이게 v3를 만들지 않은 이유다. round를 더 쪼개도 Fmax는 더 안 올라간다. &quot;더 최적화할 수 있는데 안 했다&quot;가 아니라 <strong>&quot;더 해도 소용없다는 걸 데이터로 확인하고 멈췄다&quot;</strong>이다. 어디서 멈춰야 하는지 아는 것도 설계의 일부라는 걸 여기서 배웠다.</p>
<h2 id="빠름이-아니라-일정함">빠름이 아니라 일정함</h2>
<p>&quot;FPGA로 만들었다&quot;는 것만으로는 아무 의미가 없다. &quot;노트북에서 numpy로 돌리면 되는 거 아닌가&quot;라는 질문에 숫자로 답할 수 있어야 한다. </p>
<p>같은 8192 샘플, 같은 43-tap FIR + 2:1 decimation을 두 곳에서 돌렸다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/e9c0d3c6-245b-48d1-a9ad-083c878f3414/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>비교 대상</th>
<th align="right">처리 시간</th>
</tr>
</thead>
<tbody><tr>
<td>CPU (i5-1340P, Windows, numpy)</td>
<td align="right">221.0 µs</td>
</tr>
<tr>
<td>CPU (Ryzen 7 9700X, Ubuntu, BLAS 최적화)</td>
<td align="right">58.6 µs</td>
</tr>
<tr>
<td>FPGA (v2 @ 145 MHz)</td>
<td align="right">85.0 µs</td>
</tr>
<tr>
<td>이론 한계 (8192 ÷ 2 ÷ 100 MHz)</td>
<td align="right">81.92 µs</td>
</tr>
</tbody></table>
<p>두 CPU 값은 머신·OS·BLAS가 전부 다르다. Windows와 Ubuntu의 차이가 아니라 서로 다른 컴퓨터의 차이다. UART 전송은 관측 경로라 양쪽 모두에서 제외했다. 115200 baud로 16,384바이트를 올려보내면 약 1.4초가 걸리는데, 이걸 넣으면 연산 비교가 아니라 시리얼 속도 측정이 된다. CPU는 <code>time.perf_counter()</code>로 연산 구간만, FPGA는 bare-metal C의 <code>XTime_GetTime()</code>으로 MM2S kick부터 S2MM IDLE까지 DMA 전송 구간만 잰다. FPGA 85.0 µs는 추정이 아니라 보드가 정수 µs로 UART에 올려보낸 실측값이다.</p>
<p>노트북 CPU 대비 약 2.6배, 이론 한계 대비 오차 3.8%. 그런데 이 표에서 정작 눈에 띄는 건 두 번째 줄이다. <strong>Ryzen CPU가 FPGA보다 빠르다.</strong> </p>
<p>그러니 이 절의 질문은 &quot;FPGA가 CPU보다 빠른가&quot;가 될 수 없다. 답이 경우에 따라 다르다로 이미 나와 있으니까. 물어야 할 건 다른 것이다.</p>
<p>같은 i5-1340P 환경에서 스크립트를 수십 번 돌렸는데, 판독값을 따로 적어둔 건 그중 여섯 번뿐이다.</p>
<pre><code class="language-text">181.2 / 223 / 208.8 / 174.4 / 174.8 / 175.9 (µs)</code></pre>
<p>기록하지 않은 실행까지 포함하면 대략 170 µs에서 300 µs 사이였다. 위 여섯 개는 그 구간에서 기록된 표본이지 최솟값도 최댓값도 아니다. </p>
<p>그럼 표의 221.0 µs는 어디서 온 값인가. 가장 흔한 값이라서 고른 게 아니다. 최종 비교 차트를 생성한 측정 라운드에서 나온 값이고, 그 차트가 이미 산출물로 확정돼 있어서 뒤늦게 요동을 관찰한 뒤에도 숫자를 갈아엎는 대신 확정값은 그대로 두고 요동 범위를 병기하는 쪽을 택했다.</p>
<p>같은 기간 FPGA는 전 세션 85 µs 고정이었다.</p>
<blockquote>
<p><strong>평균이 조금 빠른 게 아니라, 최악값이 평균값과 같다.</strong></p>
</blockquote>
<p>실시간 신호처리에서 의미 있는 건 평균 지연이 아니라 <strong>지연의 상한이 보장되는가</strong>다. 170~300 µs 사이 어딘가에서 끝나는 시스템과, 항상 85 µs에 끝나는 시스템은 평균이 비슷하더라도 다른 물건이다. FPGA의 본질적 이점은 절대 속도가 아니라 <strong>실행 환경과 무관한 결정론적 처리 시간</strong>이고, 이걸 데이터로 보인 게 이 절의 성과다.</p>
<h2 id="전력-실측">전력 실측</h2>
<p>Vivado는 전력을 추정해 준다. v2@145 MHz 빌드 기준 on-chip total <strong>1.705 W</strong>. 그런데 이건 툴이 계산한 값이다. 실제로 재보면 얼마가 나올까.</p>
<p>OWON HDS242를 이용하여 직접 측정하였다.</p>
<h3 id="측정-방식">측정 방식</h3>
<p>보드를 개조하여 전력을 측정하는건 보드가 망가질 위험이 크다고 생각하여 5V 배럴잭 입력 경로를 DC잭 피그테일로 끊고 그 절단점에 전류계를 직렬로 넣었다. GND는 전 과정에서 직결 유지시켰다.</p>
<pre><code>

[전류]  어댑터 ─[암]─ +5V ─▶ 적색[10A잭] ─ HDS242(DC A) ─ [COM]흑색 ─▶ +5V ─[수]─ 보드
                      GND ──────────────────── 직결 ──────────────────── GND

● 매듭 = 측정점
[전압]  어댑터 ─[암]─ +5V ──●(직결)●── +5V ─[수]─ 보드   
                      GND ──────────────────── ●(직결)●───────────────── GND
</code></pre><ul>
<li>전력 실측 이미지
<img src="https://velog.velcdn.com/images/young-0320/post/6bdb08da-a5c0-4f15-b83e-4e0488410f81/image.png" alt=""></li>
</ul>
<p>보드 전체 입력만 잴 수 있으니, <strong>상태의 차분</strong>으로 변수를 분리했다.</p>
<table>
<thead>
<tr>
<th>상태</th>
<th>조건</th>
<th align="right">I (중앙값)</th>
<th align="right">V</th>
<th align="right">P</th>
</tr>
</thead>
<tbody><tr>
<td>S0</td>
<td>SD 미삽입 — PL 미구성 baseline</td>
<td align="right">0.340 A</td>
<td align="right">5.047 V</td>
<td align="right">1.72 W</td>
</tr>
<tr>
<td>S1</td>
<td>SD boot 완료 — PL 구성 + 145 MHz, FIR idle</td>
<td align="right">0.443 A</td>
<td align="right">4.988 V</td>
<td align="right">2.21 W</td>
</tr>
<tr>
<td>S2</td>
<td><code>mode 1-1</code> 반복 실행 중</td>
<td align="right">0.435 A</td>
<td align="right">5.008 V</td>
<td align="right">2.18 W</td>
</tr>
</tbody></table>
<p>(각 상태 5회 판독의 중앙값. 전원 인가 직후 ~0.30 A 과도 전류가 흐르므로 안정화 후 판독)</p>
<h3 id="결과-셋">결과 셋</h3>
<p><strong>(1) S1 − S0 = +0.49 W.</strong> PL 비트스트림 구성 + 145 MHz MMCM 클럭 + bare-metal 앱 + DDR 활성이 전부 더해진 몫이다. PL 단독이 아니라는 건 측정 방식을 정할 때부터 알고 있던 한계다.</p>
<p><strong>(2) S2 ≈ S1.</strong> FIR을 돌려도 전력이 안 늘었다.</p>
<p>처음엔 측정 실수인 줄 알았는데, 계산해 보면 당연한 결과다. 8192 샘플 처리가 85 µs에 끝난다. 데모는 1~2초 주기로 돈다. <strong>duty가 0.01% 미만</strong>이다. 멀티미터는 판독 주기 동안 평균을 내서 표시하니 원리적으로 잡힐 수가 없다.</p>
<p>그래서 이건 실패한 측정이 아니라 <strong>결과 그 자체</strong>다. 연산 구간이 평균 전력에 흔적을 남기지 못할 만큼 짧다는 것이 처리 효율의 전력 측면 증거다.</p>
<p><strong>(3) 흥미로운 숫자들</strong></p>
<table>
<thead>
<tr>
<th>상태</th>
<th align="right">USB 미연결</th>
<th align="right">USB 연결</th>
<th align="right">차이</th>
</tr>
</thead>
<tbody><tr>
<td>S0</td>
<td align="right">0.293 A</td>
<td align="right">0.340 A</td>
<td align="right">+47 mA</td>
</tr>
<tr>
<td>S1</td>
<td align="right">0.398 A</td>
<td align="right">0.443 A</td>
<td align="right">+45 mA</td>
</tr>
</tbody></table>
<p>USB-UART(FTDI 브리지)를 PC에 꽂으면 <strong>0.22 W</strong>가 추가로 든다. FIR 연산이 평균 전력에 남긴 몫(측정 불가)보다 훨씬 크다.</p>
<blockquote>
<p><strong>연산 경로보다 관측 경로가 전력을 더 쓴다.</strong></p>
</blockquote>
<p>UART를 처음부터 &quot;데이터 경로가 아니라 관측 경로&quot;로 분리해 설계했는데, 그 구분이 전력에서도 그대로 나타난 셈이다. 덤으로, USB 유무 두 조건에서 S1−S0 델타가 0.105 A / 0.103 A로 재현돼서 차분 측정 방법론 자체도 검증됐다.</p>
<h3 id="추정-1705-w와-실측-221-w-05-w-차이는-어디로-갔나">추정 1.705 W와 실측 2.21 W, 0.5 W 차이는 어디로 갔나</h3>
<p>이걸 그냥 &quot;오차&quot;로 넘기면 측정한 의미가 없기에 분석해봤다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th align="right">값</th>
<th>근거</th>
</tr>
</thead>
<tbody><tr>
<td>보드 실측 S1 (USB 포함)</td>
<td align="right">2.21 W</td>
<td>실측</td>
</tr>
<tr>
<td>− USB-UART(FTDI) 몫</td>
<td align="right">−0.22 W</td>
<td>실측으로 분리</td>
</tr>
<tr>
<td>= S1 (USB 제외 환산)</td>
<td align="right">1.99 W</td>
<td>USB 미연결 실측 0.398 A와 일치</td>
</tr>
<tr>
<td>÷ 레귤레이터 효율 85~90% (가정)</td>
<td align="right">1.69 ~ 1.79 W</td>
<td>5V → 코어/IO 레일 변환 손실</td>
</tr>
<tr>
<td>Vivado on-chip 추정</td>
<td align="right">1.705 W</td>
<td><strong>위 구간 안 — 정합</strong></td>
</tr>
</tbody></table>
<p>USB 몫은 가정이 아니라 실측으로 뺐고, 남은 차이는 레귤레이터 손실과 추정 범위 밖 주변부(DDR3 칩, 이더넷 PHY 등)로 설명된다. 등식이 아니라 <strong>정합성 점검</strong>이고, 레귤레이터 효율 85~90%는 가정값이다. 레일별 부하 배분을 알 수 없어서 더 정밀하게는 못 잡았다.</p>
<h3 id="참고--그래서-fir-코어-자체는-얼마인가">참고 — 그래서 FIR 코어 자체는 얼마인가</h3>
<p>routed DCP에서 계층 스코프로 뽑으면 이렇다.</p>
<table>
<thead>
<tr>
<th>스코프</th>
<th align="right">LUT</th>
<th align="right">FF</th>
<th align="right">DSP48</th>
<th align="right">전력</th>
</tr>
</thead>
<tbody><tr>
<td><code>u_fir_n43_v2</code> 코어 단독</td>
<td align="right">1,792</td>
<td align="right">2,113</td>
<td align="right">16</td>
<td align="right">0.015 W</td>
</tr>
<tr>
<td>AXIS IP 전체 (래퍼+decimator+코어)</td>
<td align="right">1,948</td>
<td align="right">2,286</td>
<td align="right">16</td>
<td align="right">0.017 W</td>
</tr>
<tr>
<td>전체 비트스트림</td>
<td align="right">4,556</td>
<td align="right">—</td>
<td align="right">16</td>
<td align="right">1.705 W</td>
</tr>
</tbody></table>
<p>계층 분해로 보면 on-chip 총 전력 중 <strong>PS7이 1.407 W로 지배적</strong>이고, PL 전체 dynamic이 ~0.16 W, 그중 내가 만든 코어 몫이 0.015 W다. 칩 자원으로는 LUT 3.4% / FF 2.0% / DSP 7.3%.</p>
<p>(전력 계층 분해는 report_power를 다시 돌린 값이라 총합이 1.711 W로, 스윕 표의 1.705 W와 기본 설정 차이만큼 미세하게 다르다.)</p>
<p>즉 <strong>&quot;이 FIR이 1.7 W를 먹는다&quot;는 틀린 문장이다.</strong> 1.7 W는 Zynq 칩 한 장이 켜져 있는 값이고, 내 필터는 그 위에서 15 mW 정도를 쓴다.</p>
<h2 id="반전">반전</h2>
<p>여기가 이번 과정에서 제일 흥미로웠던 부분이다.</p>
<blockquote>
<p><strong>+26% → 0%</strong> — 최적화는 아키텍처가 아니라 타겟에 속한다 </p>
</blockquote>
<p>v1 → v2 파이프라인 분할로 FPGA Fmax를 26% 올렸다. 그러면 이건 좋은 설계일까? 같은 RTL을 다른 타겟에 올려도 26% 이득이 날까?</p>
<p>학교 서버의 Oasys-RTL로 동일한 v1/v2 소스를 250nm 표준셀에 합성해서 20000 ps부터 6000 ps까지 같은 제약으로 페어 스윕했다.</p>
<table>
<thead>
<tr>
<th></th>
<th align="right">FPGA (28nm, CARRY4)</th>
<th align="right">ASIC (250nm 표준셀 합성)</th>
</tr>
</thead>
<tbody><tr>
<td>v1 Fmax</td>
<td align="right">116 MHz</td>
<td align="right">≥ 166.7 MHz</td>
</tr>
<tr>
<td>v2 Fmax</td>
<td align="right">146 MHz</td>
<td align="right">≥ 166.7 MHz</td>
</tr>
<tr>
<td>v1 ↔ v2 격차</td>
<td align="right">+26%</td>
<td align="right"><strong>동률</strong> (전 구간 slack 차 ≤ period의 6%, 순위 요동)</td>
</tr>
</tbody></table>
<p>둘 다 166.7 MHz를 통과한다. 26%가 사라졌다. 그것도 <strong>250nm이, 28nm FPGA의 146 MHz를 넘겨서.</strong></p>
<h3 id="왜-이런-일이">왜 이런 일이</h3>
<p>같은 경로를 두 타겟이 어떻게 처리하는지 열어 봤다. v1의 그 문제의 경로(<code>z_reg[1] → round_reg</code>, 누산+라운딩 병합)다.</p>
<table>
<thead>
<tr>
<th>타겟</th>
<th align="right">이 경로의 지연</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>FPGA CARRY4 (고정 캐리 체인)</td>
<td align="right">8.664 ns</td>
<td>타이밍 fail → v2 분할로 해결</td>
</tr>
<tr>
<td>ASIC 표준셀 (carry-save 재구조화)</td>
<td align="right">5.719 ns</td>
<td>166.7 MHz에서도 pass — 분할 불필요</td>
</tr>
</tbody></table>
<p>FPGA의 CARRY4는 하드와이어된 물리 구조다. 덧셈 2회가 병합된 식을 넣으면 그 순서 그대로 캐리 체인을 타야 한다. 반면 표준셀 합성기는 multi-operand 덧셈을 carry-save 구조로 재구조화해서 병합해 버린다. 그러니 v1도 v2와 같은 한계까지 조여진다.</p>
<p><strong>내가 손으로 한 최적화를, ASIC 합성기는 알아서 했다.</strong></p>
<h3 id="결론이-뒤집힌-게-아니라-정확해졌다">결론이 뒤집힌 게 아니라 정확해졌다</h3>
<p>이 결과를 처음 봤을 때는 &quot;그럼 v2 만든 게 헛수고였나&quot; 싶었는데, 아래와 같이 결론을 정리했다.</p>
<blockquote>
<p><strong>최적화는 아키텍처에 속하지 않는다. 타겟에 속한다.</strong></p>
</blockquote>
<p>v2 분할은 FPGA CARRY4라는 특정 물리 구조를 정확히 겨냥한 <strong>타겟 특화 최적화</strong>다. FPGA에서는 26%의 가치가 있고, ASIC에서는 0의 가치가 있다. 둘 다 참이다.</p>
<p>그리고 이건 v2를 버릴 이유가 아니라 <strong>v2를 쓸 이유</strong>다. 내 타겟은 Zynq니까. v1은 폐기하지 않고 &quot;타겟 특화 최적화의 비교 사례&quot;로 남겨뒀다 — 이 절이 존재할 수 있는 게 v1을 지운 덕이 아니라 남긴 덕이다.</p>
<h3 id="스윕-중간에-헷갈린-것들">스윕 중간에 헷갈린 것들</h3>
<p>처음 20000 ps(느슨한 제약)에서는 v1 ≈ v2로 보였고, 15000 ps에서는 갑자기 <strong>v2의 slack이 5배</strong> 좋아 보였다(+4680.8 ps vs +946.6 ps). 12000 ps 이하로 조이니 다시 수렴했다.</p>
<p>이유는 이렇다. 제약이 느슨하면 합성기가 면적 최소인 ripple 매핑을 고르기 때문에 탭 누산 경로(depth 85)가 지배적이고, v2가 분할한 경로는 아예 병목이 아니다. 중간 구간에서는 합성기가 탭 가산기를 재구조화하면서(depth 85→50) v1의 병합 경로가 병목으로 부상하는데, 아직 그 경로까지는 덜 조인 상태라 차이가 크게 보인다. 끝까지 조이면 합성기가 그 경로까지 재구조화해서 결국 붙는다.</p>
<p><strong>합성 결과 한 점만 보고 결론 내리면 안 된다</strong>는 걸 여기서 확실히 배웠다. Fmax 스윕을 해야 하는 이유와 정확히 같은 이유다.</p>
<h3 id="자원·전력-수치-정리">자원·전력 수치 정리</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>FPGA v2 코어 @145MHz</th>
<th align="right">ASIC v1 @166.7MHz</th>
<th align="right">ASIC v2 @166.7MHz</th>
</tr>
</thead>
<tbody><tr>
<td>로직</td>
<td>LUT 1,792 / FF 2,113 / DSP 16</td>
<td align="right">28,615 cells</td>
<td align="right">27,506 cells</td>
</tr>
<tr>
<td>면적</td>
<td>자원 점유율 LUT 3.4% / DSP 7.3%</td>
<td align="right">1.42 mm²</td>
<td align="right">1.41 mm²</td>
</tr>
<tr>
<td>전력</td>
<td>0.015 W</td>
<td align="right">1.331 W</td>
<td align="right">1.342 W</td>
</tr>
</tbody></table>
<p>이 표는 <strong>절대 비교용이 아니다.</strong> FPGA 0.015 W는 Vivado vectorless 추정이고 ASIC 1.33 W는 Oasys 기본 toggle-rate 가정 기반 250nm 추정이다. 산정 방식이 다르다. DSP48 하드블록과 표준셀 곱셈기라 &quot;로직 규모&quot;의 단위도 대응이 안 된다. FPGA는 기성 칩 자원을 점유하는 방식이라 mm² 자체가 정의되지 않는다.</p>
<p>여기서 유효한 건 <strong>같은 타겟 안에서의 v1 ↔ v2 상대 비교뿐</strong>이고(ASIC 내 Δ≈1%), 그게 이 절의 목적이었다.</p>
<h2 id="최종-스코어보드">최종 스코어보드</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>실보드 기능 검증</td>
<td>4096 샘플 vs 골든 — SNR 74.9 dB, max error 6 LSB, correlation 1.000000, PASS</td>
</tr>
<tr>
<td>저지대역 억제</td>
<td>25 MHz −60.3 dB (사양 60 dB), 45 MHz alias −64.5 dB</td>
</tr>
<tr>
<td>처리 시간</td>
<td>8192 샘플 85.0 µs (이론 81.92 µs 대비 3.8%), CPU 221.0 µs 대비 2.6배</td>
</tr>
<tr>
<td>시간 결정성</td>
<td>FPGA 전 세션 85 µs 고정 vs CPU 170~300 µs 요동</td>
</tr>
<tr>
<td>Fmax</td>
<td>v1 116 MHz → v2 146 MHz (+26%, 자원 증가 없음), 배포 145 MHz (마진 6배)</td>
</tr>
<tr>
<td>자원 (코어 단독)</td>
<td>LUT 1,792 / FF 2,113 / DSP48 16 / BRAM 0</td>
</tr>
<tr>
<td>전력</td>
<td>보드 5V 실측 2.21 W (코어 몫 0.015 W), Vivado 추정 1.705 W와 정합</td>
</tr>
<tr>
<td>ASIC 교차 검증</td>
<td>250nm 합성 시 v1 ≈ v2 동률 (≥166.7 MHz) — 26%는 FPGA 종속</td>
</tr>
<tr>
<td>검증 자산</td>
<td>pytest 163 케이스 + RTL TB 스위트 + 보드 자동 판정, 개발 로그 50편</td>
</tr>
</tbody></table>
<h2 id="한계점">한계점</h2>
<p><strong>Fmax를 오실로스코프로 정밀 측정하지 못했다.</strong> 116 / 146 MHz는 Vivado 정적 타이밍 분석(STA)에서 WNS ≥ 0인 최대 주파수다. 보드에서는 v1@115, v2@145로 기능 동작만 확인했다. &quot;실제 실리콘이 몇 MHz까지 버티는가&quot;는 별개의 실험이고, 범위 밖으로 두었다.</p>
<p><strong>ASIC P&amp;R을 완주하지 못했다.</strong> Nitro placer가 내부 assertion(<code>SDA101: grCapFrac &lt;= 1</code>)으로 죽었다. 칩을 1.4 mm(util 70%)에서 1.6 mm(util 53%)로 넓혀 재배치해도 같은 지점에서 재현됐다. 판정 근거 네 가지(assertion 성격, 파라미터 무관 재현, GEMM 대조군, 회피 수단 소진)를 확인한 뒤 툴 내부 버그로 결론 내고 중단했다. </p>
<p>다행히 v1/v2 비교 결론은 합성 결과로 완결되고, P&amp;R이 더해줄 건 &quot;post-route에서도 유지된다&quot; 한 줄이었다. 절차와 스크립트는 재개 가능하게 보존해 뒀다.</p>
<p><strong>PL 단독 전력을 분리하지 못했다.</strong> Zybo Z7-20은 코어 레일에 전류 측정 헤더가 없어서 rail 직접 측정은 트레이스 컷이 필요하다. 대여 보드라 배제했고, 차분 측정으로 &quot;PL 구성 + 클럭 + 앱 + DDR = 0.49 W&quot;까지만 비파괴로 분리했다.</p>
<p><strong>전체 설계 상한은 이제 FIR 코어가 아니다.</strong> 147 MHz worst path가 AXI DMA IP 내부거나 AXIS 래퍼다. FIR 코어 데이터패스를 더 손봐도 시스템 Fmax는 안 올라간다. 더 올리려면 DSP48 캐스케이드 기반 systolic 재설계 같은 v3급 구조 변경에 더해 DMA 경로와 래퍼까지 같이 봐야 한다.</p>
<p><strong>검증은 directed 방식이었다.</strong> 내가 상정한 시나리오를 골든모델과 비교하는 구조라, 4편의 TLAST 데드락은 시뮬레이션이 아니라 코드 리뷰에서 발견됐다. 입력과 백프레셔의 조합을 사람이 일일이 상상하는 방식의 한계다. 다음 단계는 constrained-random + coverage와 SVA 어서션인데, 이번 범위에서는 못 했다.</p>
<h2 id="6개월이-남긴-것">6개월이 남긴 것</h2>
<h3 id="산출물">산출물</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>RTL</td>
<td>v1(3-stage)·v2(4-stage) FIR 코어, ↓2 데시메이터, AXI-Stream 래퍼 — Verilog/SystemVerilog 16개 파일</td>
</tr>
<tr>
<td>검증</td>
<td>테스트벤치 20개(골든 대조 · skid 스트레스 · 버퍼 깊이 · 버블 스윕 회귀), pytest 163 케이스</td>
</tr>
<tr>
<td>모델·도구</td>
<td>Python 고정소수점 골든모델, Kaiser 계수 설계·저지대역 스펙 판정, PC 데모/FFT/자동 판정 도구 — 42개 파일</td>
</tr>
<tr>
<td>빌드 재현</td>
<td>Vivado 블록 디자인·클럭 스윕, Vitis BOOT 재생성, ASIC 합성·P&amp;R 스크립트 — Tcl 27개</td>
</tr>
<tr>
<td>배포 이미지</td>
<td><code>release/v1_115mhz/BOOT.bin</code>, <code>release/v2_145mhz/BOOT.bin</code></td>
</tr>
<tr>
<td>기록</td>
<td>개발 로그 50편, 근거 리포트 4종(Fmax 스윕 · ASIC 대비 · 전력 실측 · 보드 검증), 그리고 이 연재 5편</td>
</tr>
</tbody></table>
<h3 id="배운-것">배운 것</h3>
<p><strong>1. 테스트가 버그를 잡는다는 것부터 증명하라.</strong>
AXI-Stream 데드락을 고칠 때, 새 테스트벤치를 <strong>수정 전 RTL에 먼저</strong> 돌렸다. 예측한 임계값에서 정확히 일치하는 실패가 재현되는 걸 확인하고 나서 고쳤다. 이 순서를 지키지 않으면 &quot;고쳤더니 통과했다&quot;가 &quot;원래 통과하는 테스트였다&quot;와 구분되지 않는다.</p>
<p><strong>2. 근본 원인까지 안 가면 같은 버그가 다시 온다.</strong>
모든 DMA 전송이 멈춘 원인은 16,384 바이트가 DMA 기본 length field(14-bit)의 한계인 16,383을 정확히 1바이트 초과한 것이었다. 여기까지 안 가면 &quot;샘플 수 좀 줄여 보니 되네&quot;에서 멈췄을 거다. 그러면 나중에 다시 만난다.</p>
<p><strong>3. 한 점이 아니라 구간을 봐야 한다.</strong>
&quot;100 MHz 통과&quot;는 정보가 없고, WNS는 주파수에 대해 단조롭지 않으며, ASIC 합성 결과는 제약 구간에 따라 결론이 뒤집혀 보인다. 스윕이 필요한 이유가 전부 같다.</p>
<p><strong>4. 어디서 멈춰야 하는지 아는 것도 설계다.</strong>
v3를 안 만들었다. Nitro P&amp;R을 중단했다. 오실로스코프 Fmax 측정을 범위에서 뺐다. 셋 다 &quot;못 했다&quot;가 아니라 &quot;해도 결론이 안 바뀐다는 걸 확인하고 멈췄다&quot;이다. 그리고 그 판단 근거를 문서로 남겨서, 판단이 틀렸을 때 남이 재개할 수 있게 했다.</p>
<p><strong>5. 측정 방식의 한계를 먼저 적어두면, 예상 밖 결과가 결과가 된다.</strong>
S2 ≈ S1을 측정 전에 예측하고 적어두지 않았다면 &quot;측정 실패&quot;로 처리했을 거다. 적어뒀기 때문에 &quot;duty 0.01%의 증거&quot;가 됐다.</p>
<p><strong>6. 자랑 못 할 숫자를 같이 적어야 자랑할 숫자가 산다.</strong>
2.6배 옆에 &quot;데스크톱 CPU는 58.6 µs로 더 빠르다&quot;를 적었다. 그러고 나니 진짜 주장(결정론적 처리 시간)이 오히려 또렷해졌다. 유리한 숫자만 남긴 결산은 읽는 사람이 나머지를 의심하게 만든다.</p>
<h2 id="마치며">마치며</h2>
<p>3월에 시작할 때는 &quot;FIR 하나 만들면 되겠지&quot;였습니다. 8월에 남은 건 RTL 몇백 줄과, 그것보다 훨씬 두꺼운 <strong>왜 그렇게 만들었는지에 대한 기록 50편</strong>입니다.</p>
<p>가장 크게 바뀐 생각은 이겁니다. 하드웨어 설계에서 어려운 건 동작하게 만드는 게 아니라 <strong>동작한다는 걸 증명하는 것</strong>이더군요. 4편까지의 노력이 전부 거기에 쓰였고, 이번 편의 숫자는 전부 그것의 결과물입니다.</p>
<p>읽어주셔서 감사합니다. 코드·로그·재현 스크립트는 전부 공개해 뒀습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Zynq FIR IP 개발 #4 : 동작하는 회로의 버그 4개]]]></title>
            <link>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94-%ED%9A%8C%EB%A1%9C%EC%97%90%EC%84%9C-%EB%B2%84%EA%B7%B8-4%EA%B0%9C%EB%A5%BC-%EC%B0%BE%EC%95%84%EB%83%88%EB%8B%A4</link>
            <guid>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94-%ED%9A%8C%EB%A1%9C%EC%97%90%EC%84%9C-%EB%B2%84%EA%B7%B8-4%EA%B0%9C%EB%A5%BC-%EC%B0%BE%EC%95%84%EB%83%88%EB%8B%A4</guid>
            <pubDate>Wed, 05 Aug 2026 13:26:23 GMT</pubDate>
            <description><![CDATA[<p>3편에서 DMA 문제를 해결하고 보드가 동작하기 시작했다. 그리고 이렇게 선을 그어뒀다.</p>
<blockquote>
<p><strong>증명되지 않은 것 3번</strong> — 래퍼의 TLAST/valid/ready 처리가 긴 스트림에서 항상 안전한지</p>
</blockquote>
<p>이 글은 그 항목을 실제로 확인한 기록이다. 결론부터 쓰면, <strong>안전하지 않았다.</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>대상</td>
<td>AXI-Stream 래퍼 <code>fir_decimator_n43_axis[.v/_v2.v]</code></td>
</tr>
<tr>
<td>찾은 것</td>
<td>버그 4개, 그리고 고치는 과정에서 2개 더</td>
</tr>
<tr>
<td>그때 보드 상태</td>
<td>100MHz에서 정상 동작, SNR 74.9 dB, correlation 1.000000</td>
</tr>
<tr>
<td>기간</td>
<td>하루 (2026-07-03)</td>
</tr>
</tbody></table>
<p>버그가 있는 회로가 보드에서 멀쩡히 돌고 있었다. 그게 이 글의 주제다.</p>
<hr>
<h2 id="시작은-미확인이라고-적어둔-항목이었다">시작은 &quot;미확인&quot;이라고 적어둔 항목이었다</h2>
<p>이 시점에 보드 검증은 이미 끝나 있었다. 100MHz에서 데모가 돌았고, 골든 모델 대비 <strong>SNR 74.9 dB / max error 6 LSB / correlation 1.000000</strong>이라는 수치까지 뽑아둔 상태였다. Fmax 스윕도 끝나서 v1 버전의 Fmax는 116MHz, v2 버전의 Fmax는 146MHz를 확정했다.</p>
<p>남은 건 v1@115MHz, v2@145MHz로 보드 실측을 한 번 더 하는 것뿐이었다.</p>
<p>그 전에, 예전에 래퍼의 3단 skid buffer를 두고 &quot;미확인 코너케이스&quot;라고 적고 넘어간 항목을 털기로 했다. 깨진 래퍼로 주파수를 재면 나중에 원인을 못 가르기 때문이다. 3편에서 배운 것과 같은 판단이다 — <strong>측정하기 전에 측정 대상을 신뢰할 수 있게 만든다.</strong></p>
<p>의심은 &quot;skid buffer가 샘플을 흘릴 수도 있다&quot; 하나였다. 검증해보니 사실이었고, <strong>세 개가 더 나왔다.</strong></p>
<h2 id="버그-4개">버그 4개</h2>
<p>**</p>
<ol>
<li>skid buffer 드랍          — 3칸 용량 부족</li>
<li>무한 flush 데드락          — 종료 판정을 <code>==</code>로 해서 등호를 지나치면 영원히 0을 도출</li>
<li>패킷 경계 오염             — backpressure가 없어도 발생</li>
<li>홀수 길이 target 계산 오류 — + 데시메이터 위상 드리프트</li>
</ol>
<p>**</p>
<p>v1과 v2 양쪽 다 해당한다. 래퍼 로직이 동일해서 v2가 새로 만든 버그가 아니다.</p>
<p>재현은 전용 TB 두 개로 했다. <code>tb_skid_stress.sv</code>는 래퍼를 reference(<code>m_axis_tready=1</code> 고정)와 stress(40사이클 중 25사이클 low로 지속 backpressure) 쌍으로 인스턴스화하고 버블 없는 연속 입력을 흘린다.</p>
<p>기존 골든 TB로는 이 조합을 만들 수 없었다. <code>bubble=0</code>으로 놔도 샘플 사이에 최소 1 idle 사이클이 구조적으로 끼어들어서, <strong>&quot;쉴 틈 없이 들어오는데 하류가 계속 막는&quot; 상태</strong>가 원리적으로 안 만들어졌다.</p>
<p><strong>N_IN=200 결과</strong></p>
<table>
<thead>
<tr>
<th>DUT</th>
<th>수신 비트</th>
<th>TLAST</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td>v1 ref (tready=1)</td>
<td>102</td>
<td>정상(beat 99)</td>
<td>stray 2개 (버그 3)</td>
</tr>
<tr>
<td>v1 stress</td>
<td>3500+</td>
<td>없음</td>
<td><strong>무한 flush 데드락</strong> (버그 2)</td>
</tr>
<tr>
<td>v2 ref (tready=1)</td>
<td>102</td>
<td>정상(beat 99)</td>
<td>stray 2개 (버그 3)</td>
</tr>
<tr>
<td>v2 stress</td>
<td>87/102</td>
<td>없음</td>
<td><strong>15개 드랍 + TLAST 유실</strong> (버그 1)</td>
</tr>
</tbody></table>
<p>v2 stress를 보면 102개 중 15개가 사라졌는데, <strong>하필 TLAST가 실린 비트도 같이 드랍됐다.</strong> 하류 S2MM은 끝을 알 수 없으니 무한 대기에 빠진다. v1 stress는 종료 등호를 지나쳐 0을 3500개 넘게 토해냈다.</p>
<p>backpressure를 전혀 안 걸어도 200입력에 102비트가 나왔다. TLAST 뒤에 stray 2개가 붙고, 종료 후 카운터가 <code>out_cnt=2</code>로 잔류하고, v1은 데시메이션 위상이 뒤집혔다(reset값 1 → 0).</p>
<p>홀수 길이(N_IN=201)에서는 <code>TLAST</code>가 한 출력 일찍 나왔다. <code>target = (in_cnt+1)&gt;&gt;1</code>이 floor라서다.</p>
<h2 id="그런데-보드는-왜-통과했나">그런데 보드는 왜 통과했나</h2>
<p>이게 제일 오래 걸린 질문이었다. <strong>SNR 74.9 dB, correlation 1.000000으로 PASS한 회로</strong>에 버그가 넷 있었다.</p>
<p><strong>버그 1·2는 트리거가 안 당겨졌다.</strong></p>
<p>두 버그의 발생 조건은 &quot;하류가 여러 사이클 연속으로 backpressure를 거는 것&quot;이다. 그런데 실제 DMA는 그럴 일이 없다. 데시메이션 때문에 출력이 2사이클당 16비트, 약 100MB/s 수준이라 DDR이나 HP 포트가 밀릴 여지가 거의 없다. <code>tready</code>가 죽지 않으니 버그가 잠들어 있었다.</p>
<p>하지만 DDR이 혼잡해지거나, 클럭을 146MHz로 올리거나, S2MM FIFO를 줄이면 터진다.</p>
<p><strong>버그 3·4는 발생했는데 못 봤다.</strong></p>
<p>이쪽이 더 무섭다. 이 둘은 backpressure 없이도 매 두 번째 명령부터 발생했을 가능성이 높다. 그런데 내 검증 지표가 <strong>원리적으로 그걸 못 본다.</strong></p>
<table>
<thead>
<tr>
<th>오염의 종류</th>
<th>검증 지표가 못 보는 이유</th>
</tr>
</thead>
<tbody><tr>
<td>몇 샘플 시프트</td>
<td>FFT <strong>크기</strong> 스펙트럼은 시간 시프트에 불변 (위상에만 들어감)</td>
</tr>
<tr>
<td>데시메이션 위상 뒤집힘</td>
<td>Nyquist 아래 톤이면 어느 위상으로 뽑아도 같은 주파수로 보임</td>
</tr>
</tbody></table>
<p>FFT 크기 스펙트럼, SNR, 피크 주파수 — 셋 다 이 두 종류에 둔감하다.</p>
<p><strong>오염의 종류가 하필 내 테스트가 못 보는 종류였다.</strong></p>
<p>이 문장이 이 글에서 제일 중요하다. 테스트가 통과했다는 건 &quot;버그가 없다&quot;가 아니라 &quot;내 테스트가 볼 수 있는 버그가 없다&quot;였다.</p>
<h2 id="버퍼-깊이는-실측으로-정했다">버퍼 깊이는 실측으로 정했다</h2>
<p>버그 1을 고치려면 skid buffer를 키워야 하는데, <strong>몇 칸으로?</strong></p>
<p>여기서 &quot;넉넉하게 8칸&quot; 같은 식으로 가지 않았다. 격리 TB(<code>tb_buf_depth.sv</code>)를 따로 만들어 측정했다. flush나 패킷 FSM은 전부 빼고, 순수하게 &quot;연속 입력 + 최악 backpressure(54사이클 중 48 low)&quot;에서 코어 출력이 버퍼를 넘치는지만 센다.</p>
<p>단순한 방법을 사용했다. <strong>버퍼를 사실상 무한(64칸)으로 놓고 최대 점유량을 재면, 그게 필요한 깊이다.</strong></p>
<table>
<thead>
<tr>
<th>게이트 규칙</th>
<th>최대 점유</th>
</tr>
</thead>
<tbody><tr>
<td>현재 <code>~valid1</code> (cnt≤1에서 수락)</td>
<td><strong>4칸</strong></td>
</tr>
<tr>
<td><code>~valid0</code> (cnt≤0에서 수락, 한 칸 일찍 막기)</td>
<td><strong>3칸</strong></td>
</tr>
</tbody></table>
<p>유한 깊이로 바꿔가며 드랍 횟수도 셌다.</p>
<table>
<thead>
<tr>
<th>게이트</th>
<th>DEPTH=3</th>
<th>DEPTH=4</th>
<th>DEPTH=5</th>
</tr>
</thead>
<tbody><tr>
<td>현재 <code>~valid1</code></td>
<td>드랍 112</td>
<td><strong>0</strong></td>
<td>0</td>
</tr>
<tr>
<td><code>~valid0</code></td>
<td><strong>0</strong></td>
<td>0</td>
<td>—</td>
</tr>
</tbody></table>
<p>두 case 모두 <code>delivered/accepted ≈ 0.5</code>라 데시메이션 비율이 유지된다. throughput 손실은 없다.</p>
<p><strong>case A(게이트 유지, 3→4칸)를 골랐다.</strong> 버퍼만 키우면 입력 수락 타이밍(<code>core_in_valid</code>)을 안 건드리므로 버그 2·3·4 로직과 간섭이 없다. 가장 국소적인 수정이다. 안 B는 게이트 타이밍이 flush/FSM과 얽혀 있어서 그쪽 수정과 함께 봐야 안전하다.</p>
<p>깊이 4의 근거도 숫자로 남는다 — <strong>게이트 허용 2 + 파이프라인 in-flight 2.</strong> 매직넘버가 아니라 측정값이고, <code>tb_buf_depth</code>가 회귀로 남아서 나중에 레이턴시를 바꾸면 바로 잡힌다.</p>
<h2 id="1차-수정-그리고-계획에-없던-다섯-번째">1차 수정, 그리고 계획에 없던 다섯 번째</h2>
<p>계획대로 넷을 고쳤다.</p>
<table>
<thead>
<tr>
<th>버그</th>
<th>수정</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>skid buffer 3칸 → 4칸 (<code>valid3/data3/tlast3</code> 추가)</td>
</tr>
<tr>
<td>3</td>
<td>flush <strong>0-주입 제거</strong>. 파이프라인은 새 입력 없이 이전 스테이지 valid로 스스로 드레인</td>
</tr>
<tr>
<td>2</td>
<td>종료/TLAST 판정 <code>out_cnt+1 == target</code> → <strong><code>&gt;=</code></strong></td>
</tr>
<tr>
<td>4</td>
<td>로직 무변경. <code>(in_cnt+1)&gt;&gt;1</code>은 짝수 N에서 정확하므로 &quot;짝수 전용&quot; 주석만</td>
</tr>
</tbody></table>
<p>코어와 데시메이터, BD, Tcl은 손대지 않았다.</p>
<p>그리고 TB를 업그레이드했다. 단일 패킷으로는 버그 3의 패킷 간 오염을 못 잡으므로, 리셋 없이 2패킷을 연속으로 보내고 절대값으로 assert하게 바꿨다. 각 패킷 꼬리에 zero를 43개 이상 넣어 FIR 지연선을 비우면, 올바른 설계라면 <code>packet2 == packet1</code>이 성립한다.</p>
<p>이걸 먼저 돌렸더니 최소 패치로는 안 고쳐진 게 드러났다.</p>
<p>지속 backpressure에서 v1이 패킷 2의 <code>TLAST</code>를 잃고 데드락했다. 200비트가 전부 도착했는데 TLAST가 0개다. 그런데 v2는 통과했다.</p>
<p>같은 패치, 같은 로직인데 한쪽만 깨진다. 차이는 코어 레이턴시뿐이다(v1=4사이클, v2=5사이클).</p>
<p><strong>즉 최소 패치는 correct가 아니라 latency 타이밍 운에 의존하고 있었다.</strong> v2가 통과한 건 고쳐져서가 아니라 운이 좋아서였다.</p>
<p>원인은 이랬다. <code>TLAST</code> 입력이 버퍼가 꽉 차서 지연되면, 마지막 코어 출력이 <code>target</code>과 <code>waiting_for_last_out</code>이 확정되기 전에 코어를 떠나 버퍼로 들어간다. 그 출력에 <code>TLAST</code> 태깅을 놓치고, 이후 <code>out_cnt</code>가 target을 지나쳐버려서 <code>&gt;=</code>로 바꿔놨어도 그걸 다시 평가할 <code>core_out_valid</code> 펄스가 안 온다. 잠긴다.</p>
<p>해결은 한 줄이었다.</p>
<pre><code class="language-verilog">assign s_axis_tready = (core_ready | s_axis_tlast) &amp; ~flush_active;</code></pre>
<p><strong>짝수 길이 가정에서 마지막 샘플은 데시메이션 drop 위상이라 출력을 만들지 않는다.</strong> 그러니 버퍼가 꽉 차 있어도 안전하게 받을 수 있다. tlast를 즉시 수락하면 <code>target_out_cnt</code>가 마지막 코어 출력보다 먼저 확정되고, 마지막 출력이 TLAST 태그를 달고 슬롯에 실려 나간다. 태그가 데이터와 함께 흐르니 레이스가 사라진다.</p>
<blockquote>
<p>이 <code>| s_axis_tlast</code> 항을 지우면 v1이 backpressure에서 다시 깨진다. 래퍼를 재수정하거나 새 주파수로 재검증할 때 유지해야 한다.</p>
</blockquote>
<p>검증은 통과했다.</p>
<pre><code class="language-text">=== make run_bug ===
tb_buf_depth  : DEPTH=4 → overflow 0 (DEPTH=3은 112 드랍)
tb_skid_stress:
  v1 ref / v1 stress / v2 ref / v2 stress
  전부 pkts=2 len=[100,100] total=200
  RESULT: PASS — no drops/deadlock/stray, TLAST per packet, packet2==packet1

=== make run_all ===  (골든 회귀)
tb_fir_n43 / tb_fir_decimator_n43 / tb_fir_decimator_n43_axis : PASS</code></pre>
<p>여기서 끝난 줄 알았다.</p>
<h2 id="2차-고친-직후에-또-나왔다">2차: 고친 직후에 또 나왔다</h2>
<p>수정을 마치고 래퍼를 다시 훑어봤다. 그리고 잔존 데드락이 나왔다.</p>
<p>증상은 이렇다. 다운스트림 backpressure가 전혀 없어도(<code>m_axis_tready=1</code> 고정), <code>s_axis_tlast</code> 비트 직전에 tvalid 버블이 일정 사이클 이상이면 <code>TLAST</code>가 영구 유실되고 래퍼가 리셋 전까지 잠긴다.</p>
<pre><code class="language-text">v1 (코어 latency 4) : 버블 ≥ 3사이클에서 재현
v2 (코어 latency 5) : 버블 ≥ 4사이클에서 재현</code></pre>
<p>1차 수정이 닫은 건 <strong>&quot;backpressure로 인한 지연&quot;</strong> 경로였다. <strong>&quot;마스터 tvalid 버블로 인한 지연&quot;</strong> 경로는 그대로 열려 있었다. 원인 구조는 같다 — 마지막 출력이 <code>waiting_for_last_out=0</code>인 채로 태그 없이 버퍼에 실리고, 나중에 플래그가 서면 <code>out_cnt</code>는 이미 target을 지나 있다.</p>
<h3 id="왜-방금-만든-tb가-못-잡았나">왜 방금 만든 TB가 못 잡았나</h3>
<p><strong>첫째, <code>tb_skid_stress</code>는 zero-bubble 연속 입력만 검증한다.</strong> 파일 주석에도 &quot;continuous zero-bubble input&quot;이라고 적혀 있다. 버블이 유발하는 버그를, 버블을 안 만드는 TB로 찾을 수는 없다.</p>
<p><strong>둘째, 골든 TB는 버블을 주긴 하는데 최대 3사이클이다.</strong> <code>drive_packet(3)</code>. 그런데 v1의 데드락 발동 조건이 <strong>버블 ≥ 3사이클</strong>이다.</p>
<p><strong>경계값이 정확히 겹친다.</strong></p>
<p>즉 <code>run_all</code>이 통과하고 있던 건 설계가 맞아서가 아니라 <strong>랜덤 시드가 아직 그 조합을 안 뽑아서</strong>였다. 시드가 바뀌면 골든 회귀가 랜덤하게 FAIL할 수 있는 상태였다.</p>
<p>그리고 이 경로가 실보드에서 배제되지 않는 이유도 확인했다. <code>bd_fir_dma.tcl</code>에서 DMA의 MM2S와 S2MM이 <code>axi_mem_intercon</code>을 거쳐 <strong>같은 HP0 포트</strong>로 DDR에 접근한다. S2MM 쓰기와 MM2S 읽기가 경합하면 MM2S 버스트에 스톨이 생기고, 그게 스트림의 tvalid 갭이 된다. <strong>&quot;마스터가 버블을 안 만든다&quot;고 가정할 근거가 없다.</strong></p>
<hr>
<h2 id="hold-back-세-가지-수준-중에-고르기">hold-back: 세 가지 수준 중에 고르기</h2>
<p>수정 방향을 세 가지로 놓고 비교했다.</p>
<table>
<thead>
<tr>
<th>수준</th>
<th>내용</th>
<th>남는 구멍</th>
</tr>
</thead>
<tbody><tr>
<td>(a) 잠김만 해제</td>
<td>종료 판정을 펄스 게이트에서 매 사이클 레벨 체크로 전환</td>
<td>FSM은 풀리지만 그 패킷의 TLAST는 이미 유실 — 1패킷 실패는 그대로</td>
</tr>
<tr>
<td>(b) (a) + 소급 태깅</td>
<td>버퍼에 아직 남아 있으면 뒤늦게 태그</td>
<td>버블이 길어 이미 m_axis로 나갔으면 복구 불가</td>
</tr>
<tr>
<td>(c) (b) + 출력 보류</td>
<td>&quot;마지막일 수도 있는&quot; 최신 출력을 확정 전까지 m_axis에 노출 안 함</td>
<td>없음</td>
</tr>
</tbody></table>
<p><strong>(c)를 골랐다.</strong> (a)와 (b)는 &quot;고쳤다&quot;고 선언할 수 없는 잔여 실패 케이스가 남는다. 절반만 닫힌 구멍은 나중에 같은 자리를 또 파게 만든다. 그 대가는 앞에서 충분히 치렀다.</p>
<p>메커니즘은 이렇다.</p>
<pre><code class="language-text">코어가 출력을 낼 때마다  → 가장 최근 출력 하나를 &quot;미확정(마지막일 수도 있음)&quot;으로 표시
다음 출력이 나오면       → 직전 것은 &quot;마지막 아님&quot; 확정 → 태그 0으로 드레인 허용
s_axis_tlast가 수락되면  → 그 시점 out_cnt와 확정된 target을 비교
                            out_cnt == target → 미확정 슬롯이 그 마지막 출력, tlast 소급 설정
                            out_cnt &lt;  target → 마지막이 아님, 태그 0으로 드레인 허용</code></pre>
<p>핵심 규칙은 하나다. 미확정 슬롯은 확정되기 전까지 절대 <code>m_axis</code>로 나가지 않는다. 즉, 선반 맨 앞에 도착해도 그 자리에서 대기한다.</p>
<p>구현은 <code>pend_valid</code>/<code>pend_idx[1:0]</code>로 미확정 슬롯을 추적하고, <code>pend_idx==0</code>일 때 <code>m_axis_tvalid</code>와 transfer를 게이팅하는 방식이다. 여기에 세 가지를 더 넣었다.</p>
<ul>
<li><strong>소급 태깅</strong> — <code>tlast_accept</code> 사이클에 이미 마지막 출력이 나가 있으면 pend 슬롯의 tlast 비트를 1로 세팅</li>
<li><strong>같은 사이클 태깅</strong> — 마지막 출력이 tlast 수락과 같은 사이클에 나오는 경계(v1 버블=3)를 즉석 판정</li>
<li><strong>즉시 종결</strong> — tlast 수락 시점에 이미 조건이 만족되면 <code>waiting_for_last_out</code>을 세우지 않고 바로 종결</li>
<li><strong>종료 판정 레벨화</strong> — 펄스 경로와 별도로 매 사이클 레벨 체크 추가 (방어)</li>
</ul>
<p><strong>버퍼 깊이 4칸은 그대로 유지된다.</strong> hold가 걸리는 건 <code>pend_idx==0</code>일 때뿐이고, 미확정 출력은 항상 버퍼 내 최신 슬롯이므로 <code>pend_idx==0</code>이면 점유가 1이다. 즉 hold 상태에서 버퍼는 사실상 비어 있어서 점유 상한(2+2=4)에 영향이 없다. 이건 분석이고, 실제 근거는 아래 회귀 결과다.</p>
<p>부수 효과로 m_axis 레이턴시가 최대 2사이클 늘었다. 출력 rate(2사이클당 1비트)는 불변이고, 골든 TB로 데이터 경로가 안 바뀐 것도 확인했다.</p>
<hr>
<h2 id="검증-테스트가-진짜-잡는지부터-확인했다">검증: 테스트가 진짜 잡는지부터 확인했다</h2>
<p>신규 TB <code>tb_tlast_bubble.sv</code>를 만들었다. tlast 직전 tvalid 버블을 0~6까지 스윕하고, {v1, v2} × {tready=1, 지속 backpressure} 조합으로 <strong>28개 DUT</strong>를 각각 2패킷 무리셋으로 돌린다.</p>
<p>그런데 이걸 수정된 RTL에 바로 돌리지 않았다. <strong>먼저 수정 전 RTL로 돌렸다.</strong></p>
<p>TB가 PASS를 뱉으면 두 가지 가능성이 있다. 회로가 맞거나, <strong>TB가 아무것도 안 보고 있거나.</strong> 앞의 것을 주장하려면 뒤의 것을 먼저 배제해야 한다.</p>
<pre><code class="language-text">[수정 전 RTL]
v1: 버블 0~2 PASS / 3~6 FAIL  (pkts=0, TLAST 유실 데드락)
v2: 버블 0~3 PASS / 4~6 FAIL
RESULT: FAIL — 28 errors</code></pre>
<p>설계 단계에서 예측한 경계값(v1≥3, v2≥4)과 정확히 일치했다. 이걸 보고 나서야 이 TB를 믿을 수 있었다.</p>
<p>수정 후 결과다.</p>
<pre><code class="language-text">=== run_bug ===
tb_buf_depth    : DEPTH=4 → overflow 0   (깊이 회귀 유지)
tb_skid_stress  : PASS — 2 packets, no drops/deadlock/stray, packet2==packet1
tb_tlast_bubble : PASS — bubble 0..6 before TLAST, v1/v2, free+backpressure

=== run_all ===
tb_fir_n43 / tb_fir_decimator_n43 : PASS (S1/S2)
tb_fir_decimator_n43_axis         : PASS (S1/S2/S3)
tb_fir_decimator_n43_axis_v2      : PASS (S1/S2/S3)   </code></pre>
<p>골든 TB S2가 랜덤 버블을 최대 3까지 주는데, 이제 시드와 무관하게 안전하다.</p>
<h2 id="남은-한계">남은 한계</h2>
<p>전부 검증한 건 아니다.</p>
<p>펌웨어의 <code>dma_run</code> 타임아웃 이후 래퍼가 어떤 상태로 남는지, 그리고 그 상태에서 복구하는 abort 경로가 없다. 이번 수정 범위가 아니라 known limitation으로 문서화만 해뒀다.</p>
<p>그리고 depth 4와 <code>| s_axis_tlast</code> 예외는 여전히 &quot;이유가 있는 매직넘버&quot;다. 표준 AXI-Stream register-slice나 FIFO(full 기반 backpressure)로 바꾸면 레이턴시에 무관하게 correct-by-construction이 되는데, 그건 보류했다.</p>
<h2 id="이-편에서-남은-것">이 편에서 남은 것</h2>
<p><strong>&quot;동작한다&quot;와 &quot;맞다&quot;는 다르다.</strong> 이 회로는 보드에서 SNR 74.9 dB로 통과하고 있었고, 잘 작동한다고 생각했던 그 상태로 네 개의 버그가 있었다.</p>
<p>세 가지가 겹쳐서 안 보였다.</p>
<pre><code>① 발동 조건이 실사용에서 안 만들어졌다   (버그 1·2 — DMA가 느려서 backpressure가 안 걸림)
② 검증 지표가 원리적으로 둔감했다        (버그 3·4 — FFT 크기 스펙트럼이 못 보는 종류의 오염)
③ 테스트의 파라미터가 경계값과 겹쳤다     (버블 3 vs v1 데드락 조건 ≥3 — 시드 운으로 통과)</code></pre><p>②가 제일 무섭다. 테스트를 더 많이 돌려도 안 잡힌다. <strong>지표 자체를 바꿔야 보인다.</strong></p>
<p>그리고 이번에 두 번 겪은 것 — 고쳤다고 생각한 직후에 또 나왔다. 1차 수정은 v2에서만 통과해서 &quot;고쳐졌다&quot;고 보였고(실제로는 레이턴시 운), 2차 수정 대상은 1차 TB가 구조적으로 못 보는 자리에 있었다. 두 번 다 내가 만든 검증의 사각지대였다.</p>
<p>그래서 마지막 TB는 수정 전 코드에 먼저 돌려 FAIL부터 확인했다. 실패하는 걸 본 적 없는 테스트는, 통과해도 아무 말을 안 하는 것과 같다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>이번 편에서 제일 크게 남은 건 &quot;통과했다&quot;는 말의 의미입니다. 2편과 3편에서는 안
되는 걸 되게 만드느라 시간을 썼는데, 이번엔 이미 잘 되고 있는 걸 의심해야 했습니다.
보드가 돌고 수치까지 나온 회로를 다시 열어보는 건 생각보다 내키지 않았습니다. 잘 돌
아가는데 굳이, 하는 마음이 계속 들었습니다.</p>
<p>그런데 열어보니 넷이 나왔고, 그중 둘은 테스트를 아무리 더 돌려도 안 잡히는 종류였습니다. FFT 크기 스펙트럼이 원리적으로 못 보는 오염이었으니까요. 샘플 수를 늘리거나 더 여러 번 돌리는 게 답이 아니라, <strong>지표 자체를 바꿔야 보이는 버그</strong>가 있다는 걸
이때 알았습니다.</p>
<p>나머지 둘은 조건이 안 만들어져서 터지지 않았을 뿐입니다. DMA가 느려서 backpressure가 안 걸렸던 건데, 클럭을 146MHz로 올리거나 DDR이 혼잡해지면 깨어났을 겁니다. </p>
<p>다음 글은 마지막입니다. Fmax 스윕으로 v1/v2를 어디까지 올렸는지, CPU 대비 얼마나 빠
른지, 전력은 실제로 얼마 먹는지 — 1편부터 4편까지의 삽질이 결국 무엇을 만들었는지를 숫자로 정리하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Zynq FIR IP 개발 #3 : 끝나지 않는 MM2S — AXI DMA 1바이트 초과]]]></title>
            <link>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-3-MM2S%EA%B0%80-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4-AXI-DMA-1%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%B4%88%EA%B3%BC</link>
            <guid>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-3-MM2S%EA%B0%80-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4-AXI-DMA-1%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%B4%88%EA%B3%BC</guid>
            <pubDate>Wed, 05 Aug 2026 13:01:54 GMT</pubDate>
            <description><![CDATA[<p>2편에서 JTAG 로딩을 포기하고 SD 부팅으로 갈아탔다. 그러자 UART에 배너가 떴다.</p>
<pre><code class="language-text">READY</code></pre>
<p>FSBL 실행, DDR 초기화, 비트스트림 로드, app ELF 핸드오프까지 전부 통과. 16일을 잡아먹은 부팅 문제를 드디어 <strong>우회했다.</strong> 원인을 밝힌 게 아니라 경로를 바꿔서 피한 것이다. 이 차이가 뒤에서 다시 나온다.</p>
<p>그런데 PC에서 데모를 돌리면 여전히 이랬다.</p>
<pre><code class="language-bash">python sw/fir_decimator_demo.py --mode 1-1 --port /dev/ttyUSB1</code></pre>
<pre><code class="language-text">TimeoutError: 보드 응답 없음 (timeout). 연결 및 비트스트림을 확인하세요.</code></pre>
<p>이번 건은 <strong>하루 만에 잡았다.</strong> 2편이 16일 걸린 것과 대비된다. 문제가 더 쉬워서가 아니라 접근이 달랐고, 그 차이가 이 글의 요지다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>증상</td>
<td>SD 부팅은 되는데 <strong>MM2S DMA가 시작만 되고 끝나지 않는다</strong></td>
</tr>
<tr>
<td>기간</td>
<td>05-24 (하루)</td>
</tr>
<tr>
<td>원인</td>
<td>1편에서 기본값으로 넘긴 설정 한 줄</td>
</tr>
</tbody></table>
<hr>
<h2 id="0-c-코드가-dma를-다루는-방식">0. C 코드가 DMA를 다루는 방식</h2>
<p>문제가 이 코드 위에서 벌어지므로 골격만 짚고 간다.
Xilinx의 <code>XAxiDma</code> 드라이버를 쓰지 않고 <strong>레지스터를 직접 다룬다.</strong> Simple DMA라 필요한 레지스터가 몇 개 없고, 드라이버를 끼우면 문제가 생겼을 때 어느 층에서 막혔는지 안 보이기 때문이다.</p>
<pre><code class="language-c">#define DMA_BASE 0x40400000u   /* bd_fir_dma.tcl의 assign_bd_address 기준 */
#define N_IN  8192             /* 입력 샘플 수 */
#define N_OUT 4096             /* 출력 샘플 수 (N_IN / M, M=2) */

/* AXI DMA 레지스터 오프셋 (PG021) */
#define MM2S_DMACR  0x00u
#define MM2S_DMASR  0x04u
#define MM2S_SA     0x18u
#define MM2S_LENGTH 0x28u
#define S2MM_DMACR  0x30u
#define S2MM_DMASR  0x34u
#define S2MM_DA     0x48u
#define S2MM_LENGTH 0x58u</code></pre>
<p>전송 순서는 이렇다.</p>
<pre><code>[PS]      ① Xil_DCacheFlushRange(src_buf)
             CPU 캐시에 남아 있는 입력 샘플을 DDR로 내린다
                 ↓
[PS→DMA]  ② MM2S_DMACR[2] = 1          soft reset
             양 채널이 halted 상태가 될 때까지 폴링
                 ↓
[PS→DMA]  ③ S2MM_DMACR.RS  = 1         받는 쪽을 먼저 연다
          ④ S2MM_DA        = dst_buf   받을 주소
          ⑤ S2MM_LENGTH    = 8192 B    받을 크기 → S2MM 대기 상태 진입
                 ↓
[PS→DMA]  ⑥ MM2S_DMACR.RS  = 1
          ⑦ MM2S_SA        = src_buf   보낼 주소
          ⑧ MM2S_LENGTH    = 16384 B   ← 이 write가 전송 시작(kick)
                 ↓
[데이터]     DDR  -&gt; MM2S  -&gt; FIR(필터 + M=2)  -&gt; S2MM  -&gt; DDR
             여기서부터 CPU는 관여하지 않는다
                 ↓
[PS]      ⑨ MM2S_DMASR.IDLE 폴링       보내기 완료 대기
          ⑩ S2MM_DMASR.IDLE 폴링       받기 완료 대기
                 ↓
[PS]      ⑪ Xil_DCacheInvalidateRange(dst_buf)
             DMA가 DDR에 쓴 결과를 CPU가 읽기 전에 캐시를 버린다</code></pre><p>⑧번 한 줄이 이 글의 근본적인 원인이 숨어 있는 지점이다.</p>
<pre><code class="language-c">Xil_DCacheFlushRange((UINTPTR)src_buf, N_IN * sizeof(int16_t));

DMA_REG(MM2S_DMACR) = (1u &lt;&lt; 2);        /* soft reset, 완료까지 폴링 */

DMA_REG(S2MM_DMACR)  = DMA_RS_BIT;      /* 받는 쪽을 먼저 연다 */
DMA_REG(S2MM_DA)     = (uint32_t)dst_buf;
DMA_REG(S2MM_LENGTH) = N_OUT * sizeof(int16_t);

DMA_REG(MM2S_DMACR) = DMA_RS_BIT;
DMA_REG(MM2S_SA)    = (uint32_t)src_buf;
DMA_REG(MM2S_LENGTH) = N_IN * sizeof(int16_t);   /* 이 write가 kick */

/* MM2S IDLE 대기 → S2MM IDLE 대기 */
Xil_DCacheInvalidateRange((UINTPTR)dst_buf, N_OUT * sizeof(int16_t));</code></pre>
<p>네 가지가 중요하다.</p>
<ul>
<li><strong>S2MM을 먼저 연다.</strong> MM2S가 먼저 쏟아지면 받는 쪽이 준비되기 전에 스트림이 도착한다.</li>
<li><strong><code>MM2S_LENGTH</code> write가 곧 시작 신호다.</strong> 이 레지스터에 값을 쓰는 순간 전송이 시작된다.</li>
<li><strong>캐시를 직접 관리한다.</strong> DMA는 캐시를 안 거치고 DDR을 본다. 보내기 전 flush, 받은 뒤 invalidate를 빼먹으면 CPU와 DMA가 서로 다른 메모리를 보게 된다.</li>
<li><strong>soft reset은 딱 한 번만 건다.</strong> 이건 실제로 틀렸다가 고친 부분이다.</li>
</ul>
<p>마지막 항목을 조금 더 쓴다. 처음에는 채널마다 리셋을 걸었다.</p>
<pre><code class="language-c">/* 1차 시도 — 틀렸다 */
dma_reset_channel(S2MM_DMACR, ...);   // 전체 코어 리셋
DMA_REG(S2MM_DA)     = ...;
DMA_REG(S2MM_LENGTH) = ...;
dma_reset_channel(MM2S_DMACR, ...);   // 전체 코어를 다시 리셋 → 위 설정이 날아간다</code></pre>
<p>이름은 <code>MM2S_DMACR</code>, <code>S2MM_DMACR</code>로 채널별로 나뉘어 있지만, <strong><code>DMACR[2]</code>(reset bit)는 어느 쪽에 쓰든 DMA 코어 전체를 리셋한다</strong>(PG021). 채널별 독립 리셋 같은 건 없다. 그래서 S2MM을 설정해두고 MM2S를 리셋하면, 방금 넣은 <code>S2MM_DA</code>와 <code>S2MM_LENGTH</code>가 통째로 지워진다. 받을 준비가 안 된 채로 보내기가 시작되고, DMA는 영원히 안 끝난다.</p>
<p>레지스터 이름이 채널별로 보인다고 해서 동작도 채널별이라는 보장은 없었다. <strong>이 문장은 이 글 끝에서 한 번 더 나온다.</strong></p>
<p>버퍼는 전역(BSS)에 둔다. 16KB + 8KB는 스택 한계를 넘고, <code>aligned(32)</code>는 Cortex-A9 캐시 라인 경계를 맞추기 위한 것이다. 크기도 32의 배수라 invalidate가 이웃 데이터를 건드리지 않는다.</p>
<pre><code class="language-c">static int16_t __attribute__((aligned(32))) src_buf[N_IN];
static int16_t __attribute__((aligned(32))) dst_buf[N_OUT];</code></pre>
<hr>
<h2 id="0단계-디버깅-전에-보이게-만들기">0단계: 디버깅 전에, 보이게 만들기</h2>
<p>증상은 <code>TimeoutError</code> 한 줄뿐이었다. 여기서 바로 원인을 찾으러 가지 않았다. <strong>지금 상태로는 아무것도 안 보였기 때문이다.</strong></p>
<p>PC 수신기는 magic(<code>0xDEADBEEF</code>)만 기다리고 있었다. 그래서 보드가 <code>ERR:1</code>이라고 비명을 질러도 그건 magic이 아니라서 무시됐고, 전부 &quot;timeout&quot;으로 뭉개졌다. <strong>보드는 말하고 있었는데 못 보고 있었을 뿐이었다.</strong></p>
<p>세 가지를 고쳤다.</p>
<p><strong>1. Python 수신기가 magic을 기다리는 동안 들어온 텍스트를 버리지 않고 모아, 실패 시 함께 출력하도록.</strong></p>
<p><strong>2. C 앱에 DMA 단계별 trace를 심어 어디까지 갔는지 UART로 방출하도록.</strong></p>
<table>
<thead>
<tr>
<th>Trace</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>CMD</code></td>
<td>PC command 수신 완료</td>
</tr>
<tr>
<td><code>GEN</code></td>
<td>입력 멀티톤 생성 완료</td>
</tr>
<tr>
<td><code>D0</code></td>
<td><code>dma_run()</code> 진입</td>
</tr>
<tr>
<td><code>D1</code></td>
<td>AXI DMA soft reset write 직전</td>
</tr>
<tr>
<td><code>D2</code></td>
<td>AXI DMA reset bit clear 확인</td>
</tr>
<tr>
<td><code>D3</code></td>
<td>S2MM 주소/길이 설정 완료</td>
</tr>
<tr>
<td><code>D4</code></td>
<td>MM2S 주소/길이 설정 완료</td>
</tr>
<tr>
<td><code>D5</code></td>
<td>MM2S idle 확인</td>
</tr>
<tr>
<td><code>D6</code></td>
<td>S2MM idle 확인</td>
</tr>
</tbody></table>
<p><strong>3. 무한 대기를 없애고 전부 timeout을 걸도록.</strong> 특히 DMA reset bit이 내려오기를 기다리는 루프가 그냥 <code>while</code>이었다. 리셋이 안 끝나면 거기서 영원히 멈추고, 밖에서 보면 그냥 &quot;응답 없음&quot;이다. 리턴 코드를 나눴다.</p>
<pre><code class="language-text">0: 성공
1: MM2S timeout
2: S2MM timeout
3: AXI DMA reset timeout</code></pre>
<p>그리고 trace를 심으면서 <strong>어떤 출력이 나오면 어디가 문제인지</strong>를 미리 표로 적어뒀다. 결과를 보고 나서 해석하면 그때그때 말이 되는 쪽으로 끌려가기 때문이다.</p>
<table>
<thead>
<tr>
<th>보드 출력</th>
<th>어디가 문제인가</th>
</tr>
</thead>
<tbody><tr>
<td><code>CMD</code>조차 안 나옴</td>
<td>PC 명령이 보드에 안 들어갔거나, SD에 올린 이미지가 반영 안 됨</td>
</tr>
<tr>
<td><code>CMD</code>, <code>GEN</code>, <code>D0</code>, <code>D1</code>까지만</td>
<td>AXI DMA 레지스터 접근 자체 또는 DMA 클럭/리셋</td>
</tr>
<tr>
<td><code>ERR:3</code></td>
<td>DMA reset bit이 안 내려옴</td>
</tr>
<tr>
<td><code>ERR:1</code></td>
<td>MM2S 경로</td>
</tr>
<tr>
<td><code>ERR:2</code></td>
<td>S2MM 경로, TLAST, FIR AXI-Stream 출력</td>
</tr>
<tr>
<td><code>D6</code> 다음 magic 수신</td>
<td>전 경로 성공</td>
</tr>
</tbody></table>
<p>이 표를 먼저 만든 게 이번 디버깅에서 제일 잘한 것이었다. <strong>다음에 뭘 할지가 출력 한 줄로 결정</strong>되니, 매번 처음부터 고민할 필요가 없어진다.</p>
<p>이 세 가지를 넣고 나니 에러 메시지가 이렇게 바뀌었다.</p>
<pre><code class="language-text">RuntimeError: 보드 DMA 오류 ERR:1: MM2S DMA timeout.
최근 UART 텍스트:
GEN | D0 | D1 | D2 |
ST2 M=00000001 S=00000001 |
D3 | ST3 M=00000001 S=00000000 |
D4 | ST4 M=00000000 S=00000000 |
MM2S M=00000000 S=00000000 |
...
MM2STO M=00000000 S=00000000 |
ERR:1</code></pre>
<blockquote>
<p><code>CMD</code>가 안 보이는 건 보드가 안 찍어서가 아니다. C는 <code>CMD</code> 바로 다음 줄에서 <code>GEN</code>을 찍는다. 커맨드 전송 직후 응답을 읽는 구간에서 이미 소비돼 이 수집 버퍼엔 안 잡혔을 뿐이다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>지점</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>D0</code></td>
<td>source buffer 캐시 flush 진입</td>
</tr>
<tr>
<td><code>D1</code></td>
<td>DMA soft reset 요청</td>
</tr>
<tr>
<td><code>D2</code>, <code>ST2 M=1 S=1</code></td>
<td>reset 완료, 양 채널 halted</td>
</tr>
<tr>
<td><code>D3</code>, <code>ST3 M=1 S=0</code></td>
<td>S2MM 시작됨</td>
</tr>
<tr>
<td><code>D4</code>, <code>ST4 M=0 S=0</code></td>
<td>MM2S도 시작됨</td>
</tr>
<tr>
<td><code>M=0 S=0</code> 반복</td>
<td><strong>에러 비트 없이</strong> running 유지</td>
</tr>
<tr>
<td><code>ERR:1</code></td>
<td>MM2S의 IDLE 비트가 끝까지 안 올라옴</td>
</tr>
</tbody></table>
<p>이게 결정적이었다. <strong>AXI-Lite 접근도, reset도, run 비트 설정도 다 된다.</strong> DMA는 에러조차 안 낸다. 그냥 &quot;일하는 중&quot;인 채로 영원히 안 끝난다.</p>
<p>그리고 <code>ERR:1</code>이라는 것 자체가 정보였다. <code>ERR:2</code>(S2MM)가 아니라 <code>ERR:1</code>(MM2S)이다. <strong>한쪽 방향만 죽었다.</strong> 이 사실이 에러를 해결하는 데 큰 도움이 됐다.</p>
<hr>
<h2 id="1차-가설-fir-래퍼가-문제다">1차 가설: FIR 래퍼가 문제다</h2>
<p>가장 그럴듯했다.</p>
<ul>
<li>DMA 상태 레지스터에 slave/decode/internal error 비트가 하나도 없다</li>
<li>MM2S가 IDLE로 못 가는 건 <strong>downstream backpressure</strong>로도 생긴다</li>
<li>FIR 래퍼는 <code>s_axis_tready</code>, <code>m_axis_tvalid</code>, <code>m_axis_tlast</code>를 직접 제어한다</li>
<li>실제로 이 프로젝트에서 TLAST 데드락을 한 번 겪은 적도 있다</li>
</ul>
<p>그럴듯한데, 문제는 <strong>그럴듯하다는 것만으로는 아무것도 확정되지 않는다</strong>는 거였다. 2편에서 가설 다섯 개를 순서대로 쫓다가 16일을 쓴 직후였다.</p>
<p>그래서 RTL을 노려보는 대신, <strong>PL 쪽을 통째로 갈아끼울 수 있는 실험 장치</strong>를 만들었다. 메인 BD는 손대지 않고 디버그용 BD를 따로 만들었다. PS/DDR/DMA/C 코드는 전부 동일하게 두고 <strong>스트림 종단점만</strong> 교체하는 구조다.</p>
<h2 id="실험-1-fir을-지우고-decimator만-남긴다">실험 1: FIR을 지우고 decimator만 남긴다</h2>
<p><code>axis_decimator_m2_n43_debug</code> — FIR 연산을 전부 걷어내고 &quot;2개 중 1개만 통과&quot;시키는 로직만 남겼다. 포트 이름은 기존 래퍼와 동일하게 맞춰 BD에서 그대로 갈아끼울 수 있게 했다.</p>
<pre><code class="language-text">결과: 동일하게 MM2S timeout
ST4 M=00000000 S=00000000 반복</code></pre>
<p>FIR 내부 계산이나 FIR 래퍼 단독의 문제일 가능성이 내려갔다. 다만 이 래퍼도 입력과 출력 스트림이 이어져 있어서, S2MM 쪽 backpressure가 MM2S까지 거슬러 올라가는 구조는 여전히 남아 있었다. 아직은 결론을 내릴 수 없었다.</p>
<p>지금 와서 보면 <strong>이 실험은 안 해도 됐다.</strong> 바로 실험 2로 갔어야 한다. &quot;FIR 계산을 뺀다&quot;는 여전히 <strong>의심할 수 있는 상태</strong>였고, &quot;backpressure를 줄 방법이 없다&quot;만이 의심이 불가능한 상태였다. 배제 조건을 절반만 만든 실험은 후보를 절반만 지운다. 하루를 반나절로 못 줄인 이유가 여기 있다.</p>
<h3 id="곁가지-지금-보드에-뭐가-올라가-있는지-어떻게-아나">곁가지: 지금 보드에 뭐가 올라가 있는지 어떻게 아나</h3>
<p>여기서 한 번 멈췄다. BOOT 이미지가 세 개가 됐는데(메인 FIR / 단순 decimator / 곧 만들 smoke), <strong>부팅 배너가 셋 다 <code>READY</code>로 똑같았다.</strong></p>
<p>그러면 실험 결과를 믿을 수 없다. &quot;smoke가 실패했다&quot;가 아니라 &quot;SD에 예전 FIR 이미지가 그대로 있었다&quot;일 수 있으니까. 실험을 아무리 잘 설계해도 엉뚱한 대상에 돌리면 무의미하기에, 기억에 의존하기보다는 C 앱에 옵셔널 헤더로 태그를 심고 BOOT 생성 스크립트에 옵션을 붙였다.</p>
<pre><code class="language-c">#if __has_include(&quot;boot_tag.h&quot;)
#include &quot;boot_tag.h&quot;
#endif

#ifndef BOOT_TAG
#define BOOT_TAG &quot;FIR&quot;
#endif

uart_puts(&quot;READY &quot;);
uart_puts(BOOT_TAG);</code></pre>
<pre><code class="language-bash">vitis/rebuild_boot_image.sh \
  --bit build/output/bd_fir_dma_smoke_wrapper.bit \
  --boot-out build/output/BOOT_smoke.bin \
  --boot-tag SMOKE</code></pre>
<p>이제 부팅하면 <code>READY SMOKE</code>가 찍힌다. 어떤 BOOT를 사용하고 있는지 확인이 됐다.</p>
<h2 id="실험-2-스트림-종단점을-아예-무력화한다">실험 2: 스트림 종단점을 아예 무력화한다</h2>
<p><code>axis_dma_smoke_test</code> — 입력과 출력의 연결을 끊었다.</p>
<pre><code class="language-verilog">assign s_axis_tready = 1&#39;b1;                  // 입력은 무조건 받고 버린다
assign m_axis_tvalid = out_active;            // 출력은 내부 카운터로 자체 생성
assign m_axis_tdata  = {4&#39;h5, out_count};
assign m_axis_tlast  = out_active &amp;&amp; (out_count == N_OUT - 1);</code></pre>
<p><code>s_axis_tready</code>가 상수 1이다. <strong>이 회로는 MM2S에 backpressure를 줄 방법이 물리적으로 없다.</strong> 입력을 받는 즉시 버리고, 출력은 입력과 무관하게 혼자 만든다.</p>
<p>여기서도 MM2S가 안 끝나면, PL은 완전히 무죄다.</p>
<pre><code class="language-text">READY SMOKE
→ ERR:1: MM2S DMA timeout
→ ST4 M=00000000 S=00000000 반복</code></pre>
<p><strong>끝났다.</strong> 남은 건 AXI DMA 설정이거나, C가 넘기는 transfer length다.</p>
<hr>
<h2 id="결정적-단서-왜-mm2s만인가">결정적 단서: 왜 MM2S만인가</h2>
<p>PL이 무죄로 확정된 뒤 다시 <code>ERR:1</code>을 봤다. 이 시스템에는 방향이 두 개인데 <strong>한쪽만 죽었다.</strong></p>
<p>C 코드의 두 length를 나란히 놓았다.</p>
<pre><code class="language-c">DMA_REG(S2MM_LENGTH) = N_OUT * sizeof(int16_t);   // 4096 × 2 =  8192 bytes = 0x2000
DMA_REG(MM2S_LENGTH) = N_IN  * sizeof(int16_t);   // 8192 × 2 = 16384 bytes = 0x4000</code></pre>
<p>DMA의 buffer length 필드가 14비트라면 표현 가능한 최대값은 이렇다.</p>
<pre><code class="language-text">2^14 - 1 = 16383 bytes = 0x3FFF</code></pre>
<p>여기서 비대칭이 나온다.</p>
<pre><code class="language-text">S2MM_LENGTH =  8192 bytes ≤ 16383   → 14비트 안에 들어감
MM2S_LENGTH = 16384 bytes &gt; 16383   → 1바이트 초과</code></pre>
<p><strong>실패한 방향과 초과한 방향이 정확히 일치한다.</strong></p>
<p>이게 결정적이었던 이유는, 다른 후보들은 이 비대칭을 설명 못 하기 때문이다. HP0 경로가 죽었거나, PS7 DDR init이 틀렸거나, AXI-Lite가 잘못됐거나, UART가 깨졌다면 — 왜 하필 <strong>큰 쪽 한 방향만</strong> 죽고 작은 쪽은 멀쩡한가. 우연히 그렇게 되기 어렵다.</p>
<h2 id="원인-1편에서-그냥-넘긴-기본값">원인: 1편에서 그냥 넘긴 기본값</h2>
<p>1편에서 &quot;충분하다&quot;며 기본값으로 두고 지나간 그 항목이었다.</p>
<pre><code class="language-text">Width of Buffer Length Register = 14 bits</code></pre>
<p>1편에서 내가 계산에 쓴 건 <strong>출력</strong> 샘플 수였다. 4096 × 2B = 8192 bytes니까 절반도 안 쓴다고 봤다. 그런데 이 파라미터는 채널마다 따로 있는 게 아니라 <strong>MM2S와 S2MM에 함께 걸린다.</strong> 실제 제약은 둘 중 큰 쪽이 만들고, FIR은 M=2 데시메이터라 <strong>입력이 출력의 두 배</strong>다. 하필 여유를 확인한 쪽이 작은 쪽이었다.</p>
<p>증상까지 정확히 설명된다.</p>
<pre><code class="language-text">C에서 MM2S_LENGTH = 0x4000 write
DMA의 length 필드가 14비트면 0x4000은 표현 불가
→ 하위 14비트만 보면 0으로 해석될 수 있음
→ DMA는 running 상태로 들어가지만 완료 조건을 못 만남
→ 에러 비트는 안 서고, IDLE도 안 뜸
→ MM2S timeout</code></pre>
<p><code>ST4 M=00000000 S=00000000</code>이 계속 반복되던 게 이거였다. <strong>에러가 아니라, 끝날 수 없는 일을 하고 있었던 것이다.</strong></p>
<p>다만 여기는 선을 그어야 한다. <strong>인과는 확정됐다</strong> — 이 설정만 바꾸니 통과했고, smoke는 FIR를 안 거치니 다른 변수도 없다. 하지만 &quot;14비트로 잘려서 0이 됐다&quot;는 <strong>추론이다.</strong> DMA 내부를 들여다본 게 아니라 정황으로 맞춘 것이다.</p>
<p>확인할 방법은 있었다. <code>MM2S_LENGTH</code>에 <code>0x4000</code>을 쓴 직후 그 레지스터를 다시 읽어서 <code>0</code>이 나오는지 보면 된다. 실제로 이 체크는 뒤에 나올 Case B 목록에 적어뒀는데, Case A가 나오는 바람에 안 돌렸다. 고쳐졌으니 넘어갔지만, 엄밀히는 안 본 것이다.</p>
<blockquote>
<p><strong>2편의 에러와는 원인이 다르다.</strong> 둘 다 &quot;DDR 데이터가 이상하다&quot;로 보여서 한동안 같은 문제로 의심했지만, 접근 경로가 다르다. 2편에서는 <code>PC xsdb → JTAG → DAP → DDR 컨트롤러</code>이고 3편은 <code>C 앱 → AXI DMA → HP0 → DDR</code>이다. 결정적으로 JTAG <code>mwr</code>/<code>mrd</code>에서는 오염이 났는데 SD 부팅으로 올린 앱은 전부 정상 실행됐다. DDR 자체가 깨졌다면 이럴 수 없다. 증상 범주가 같다고 원인을 묶으면, 한쪽 고쳐놓고 다른 쪽이 안 낫는 걸 보며 또 며칠을 쓴다.</p>
</blockquote>
<h2 id="수정">수정</h2>
<p>BD의 DMA 설정에 length width를 명시했다.</p>
<pre><code class="language-tcl">set_property -dict [list \
  CONFIG.c_include_sg {0} \
  CONFIG.c_sg_length_width {23} \
  CONFIG.c_m_axis_mm2s_tdata_width {16} \
  CONFIG.c_s_axis_s2mm_tdata_width {16} \
] $axi_dma_0</code></pre>
<p><strong>왜 15가 아니라 23인가.</strong> 이 파라미터는 8~26 범위를 갖는다. 최소 해결값은 15비트(<code>2^15 - 1 = 32767 bytes</code>)고 최대는 26이다. 그 사이에서 23을 골랐다 — <code>2^23 - 1 ≈ 8MB</code>면 이 프로젝트에서 다시 문제가 될 일은 없다고 판단했다.</p>
<p>바뀌지 않는 것도 확인해뒀다 — AXI-Stream <code>tdata</code> 폭 16비트, 샘플 포맷 <code>int16_t</code>, FIR 계수와 Q 포맷, DDR 폭, UART 패킷 구조. 이 설정은 <strong>한 번에 전송 가능한 byte count 범위만</strong> 바꾼다.</p>
<p>그리고 이 설정을 smoke BD 하나가 아니라 <strong>메인 BD와 debug BD 전부에</strong> 넣었다. 원인 검증만 하려면 smoke에만 넣으면 되지만, 같은 실수를 다시 밟고 싶지 않았다.</p>
<h3 id="돌리기-전에-결과별-판정을-적어뒀다">돌리기 전에 결과별 판정을 적어뒀다</h3>
<p>빌드를 걸어놓고, 결과가 나오기 전에 세 가지 경우와 각각의 판정을 문서에 먼저 썼다.</p>
<table>
<thead>
<tr>
<th>결과</th>
<th>판정</th>
<th>다음에 볼 것</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A.</strong> smoke 통과</td>
<td>원인 = <code>c_sg_length_width</code> 기본 14비트 확정</td>
<td>메인 FIR 재빌드 → BOOT 재생성 → 데모 재실행</td>
</tr>
<tr>
<td><strong>B.</strong> smoke 여전히 MM2S timeout</td>
<td>length width는 필요한 수정이지만 유일 원인은 아님</td>
<td>MM2S read path 설정, HP0 read transaction, 레지스터 프로그래밍 순서, <code>MM2S_LENGTH</code> writeback 확인, 길이를 8192 B·1024 B로 줄여 재현</td>
</tr>
<tr>
<td><strong>C.</strong> MM2S는 통과, S2MM timeout</td>
<td>MM2S 문제는 해결, 원인이 write 방향으로 이동</td>
<td>출력 TLAST, <code>S2MM_LENGTH</code>, HP0 write path, destination buffer invalidate</td>
</tr>
</tbody></table>
<p>이렇게 해두면 <strong>결과를 보고 나서 해석을 지어내지 않게 된다.</strong> 2편에서 뼈저리게 겪은 게 그거였다. <code>ps7_init</code>을 바꿨더니 패턴이 달라지자 &quot;방향은 맞다&quot;고 믿어버렸는데, 판정 기준을 미리 정해뒀다면 &quot;패턴 변화는 해결이 아니다&quot;를 바로 알았을 것이다.</p>
<h2 id="검증-1-smoke">검증 1: smoke</h2>
<p>순서를 지켰다. 먼저 smoke부터.</p>
<pre><code class="language-text">수정 전 smoke : ERR:1 MM2S timeout
수정 후 smoke : timeout 예외 없이 plot 창까지 도달</code></pre>
<p>여기서 판정 근거는 <strong>그림의 모양이 아니라 그림이 존재한다는 사실</strong>이었다. smoke의 출력은 FIR 결과가 아니라 내부 카운터라 스펙트럼은 볼 것이 없다. 하지만 출력 FFT가 그려졌다는 건 보드가 UART로 payload를 끝까지 돌려보냈다는 뜻이다. 그거면 충분했다.</p>
<p>smoke는 FIR을 안 거친다. 그러니 이 차이는 계수나 데이터패스가 아니라 DMA 설정 수정의 효과로만 설명된다.</p>
<p><strong>Case A였다.</strong> 원인 = <code>c_sg_length_width</code> 기본 14비트, 확정.</p>
<h2 id="검증-2-메인-fir">검증 2: 메인 FIR</h2>
<p>여기서 바로 메인 FIR로 넘어가기 전에, <strong>판정표를 한 번 더 썼다.</strong> smoke가 통과했다고 메인 FIR가 통과한다는 보장이 없고, 실패했을 때 또 감으로 헤맬 수는 없었다.</p>
<table>
<thead>
<tr>
<th>결과</th>
<th>판정</th>
<th>다음에 볼 것</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A.</strong> 메인 FIR도 plot 도달</td>
<td>length width가 전체 timeout의 직접 원인으로 확정</td>
<td>필터 성능 — 5MHz 보존, 20/30MHz 감쇠, 출력 4096 샘플, 골든 대비</td>
</tr>
<tr>
<td><strong>B.</strong> 메인 FIR에서 다시 timeout</td>
<td>transport는 살아 있음. 다시 FIR AXIS 래퍼</td>
<td><code>s_axis_tready</code>가 중간에 low로 붙는지, <code>m_axis_tlast</code>가 4096번째에서 정확히 1 사이클인지, 입력 TLAST와 출력 TLAST 관계가 꼬이는지</td>
</tr>
<tr>
<td><strong>C.</strong> 통과하는데 스펙트럼이 이상</td>
<td>transport 해결, 순수 FIR correctness 단계</td>
<td>계수 순서, Q 포맷 스케일링, 반올림/포화, 데시메이션 위상, 파이프라인 정렬</td>
</tr>
</tbody></table>
<p>그리고 메인 FIR를 같은 설정으로 재빌드했다.</p>
<pre><code class="language-text">WNS = 0.692 ns
bitstream / XSA 생성 성공
→ READY FIR</code></pre>
<p>성공!!!
timeout 없이 기대했던 plot이 그대로 출력됐다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/fe679e79-5d9d-4962-92aa-c4dd4988adb9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/638b309a-4074-4f57-90ef-3304b6706e67/image.png" alt=""></p>
<p><strong>이번에도 Case A였다.</strong></p>
<p>판정표를 두 번 쓴 게 낭비처럼 보일 수 있는데, 실제로는 이게 하루로 끝난 이유다. smoke가 통과했을 때 &quot;다 됐다&quot;가 아니라 &quot;<strong>여기까지</strong> 됐다&quot;로 읽을 수 있었던 건, 그 다음 판정 기준이 이미 종이에 있었기 때문이다.</p>
<h2 id="왜-1편에서-그냥-넘어갔나">왜 1편에서 그냥 넘어갔나</h2>
<p>사실 이 프로젝트에서 <strong>같은 실수를 두 번</strong> 했다.</p>
<table>
<thead>
<tr>
<th>무엇</th>
<th>이름이 알려준 범위</th>
<th>실제 범위</th>
</tr>
</thead>
<tbody><tr>
<td>soft reset</td>
<td><code>MM2S_DMACR</code> / <code>S2MM_DMACR</code> — 채널마다 하나씩</td>
<td>DMA 코어 전체</td>
</tr>
<tr>
<td>length width</td>
<td><code>c_sg_length_width</code> — <strong>sg</strong>니까 SG 모드 전용</td>
<td>simple 모드에도 적용</td>
</tr>
</tbody></table>
<p>앞의 것은 0번에서 겪었고, 뒤의 것이 이 글의 원인이다. 둘 다 <strong>이름이 기능 범위를 실제보다 좁게 보이게 만든 케이스</strong>다. 이름은 문서가 아니다. 그런데 나는 두 번 다 이름을 문서처럼 읽었다.</p>
<p><strong>첫째, 파라미터 이름이 <code>c_sg_length_width</code>다.</strong></p>
<p><code>sg</code>는 Scatter Gather다. 그런데 이 설계는 SG를 껐다.</p>
<pre><code class="language-tcl">CONFIG.c_include_sg {0}</code></pre>
<p>이름만 보면 &quot;SG 안 쓰는데 이게 무슨 상관이야&quot; 하고 넘어가게 된다. 실제로 나는 1편에서 그렇게 넘겼다. 하지만 이 파라미터는 SG 모드 전용이 아니라, <strong>simple 모드에서도 buffer length 레지스터가 몇 비트짜리 byte count를 받는지를 결정한다.</strong></p>
<p><strong>둘째, 이 버그는 아무 데서도 신호를 안 준다.</strong></p>
<table>
<thead>
<tr>
<th>단계</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>C 컴파일</td>
<td>에러 없음</td>
</tr>
<tr>
<td>Vivado 빌드</td>
<td>에러 없음</td>
</tr>
<tr>
<td>AXI-Lite write/read</td>
<td>정상 동작</td>
</tr>
<tr>
<td>DMA start bit</td>
<td>정상적으로 들어감</td>
</tr>
<tr>
<td><code>DMASR</code> error bit</td>
<td>안 뜸</td>
</tr>
<tr>
<td>소프트웨어에서 보이는 것</td>
<td>그냥 timeout</td>
</tr>
</tbody></table>
<p>C 코드에서 <code>MM2S_LENGTH</code>는 그냥 32비트 메모리 맵 레지스터로 보인다.</p>
<pre><code class="language-c">DMA_REG(MM2S_LENGTH) = 16384;</code></pre>
<p>이 줄에는 아무 문제가 없다. 문제는 IP 내부의 유효 필드 폭이 14비트라는 것이고, <strong>그 사실은 C 코드 어디에도 적혀 있지 않다.</strong> 그래서 처음에 RTL handshake 문제처럼 보였던 것이다.</p>
<h2 id="통과가-증명한-것과-증명하지-않은-것">통과가 증명한 것과, 증명하지 않은 것</h2>
<p>여기서 선을 그어둬야 했다. plot이 나왔다고 FIR가 맞다는 뜻이 아니기 때문이다.</p>
<p><strong>증명된 것</strong></p>
<ol>
<li>PS가 AXI-Lite로 DMA 레지스터를 쓸 수 있다</li>
<li>DMA reset/start 시퀀스가 동작한다</li>
<li>MM2S가 DDR에서 16,384 bytes를 읽어 PL 스트림으로 보낼 수 있다</li>
<li>S2MM이 PL 스트림 8,192 bytes를 DDR에 쓸 수 있다</li>
<li>C 앱이 그 버퍼를 읽어 UART로 PC에 보낼 수 있다</li>
<li>Python이 받아서 plot까지 만들 수 있다</li>
</ol>
<p>동시에 <strong>1차 원인 후보에서 내려간 것</strong>도 정리됐다.</p>
<pre><code class="language-text">보드 불량
PS7 init / DDR init
UART 기본 통신
Python 수신기만의 문제
AXI-Lite 레지스터 접근
HP0 DDR read/write path 전체 불능
FIR MAC 연산 자체 때문에 MM2S가 멈춘다는 가설</code></pre>
<p><strong>증명되지 않은 것</strong></p>
<ol>
<li>FIR 계수가 맞는지</li>
<li><code>fir_decimator_n43_axis.v</code>의 필터링 결과가 골든 모델과 일치하는지</li>
<li>래퍼의 TLAST/valid/ready 처리가 긴 스트림에서 항상 안전한지</li>
<li>5MHz 통과대역, 20/30MHz 저지대역 감쇠가 스펙을 만족하는지</li>
</ol>
<p>특히 4번. 위 plot을 보면 5MHz는 살아 있고 20/30MHz는 내려앉았다.
하지만 그건 눈대중이지 수치가 아니다. 몇 dB 감쇠인지, 스펙이 요구하는 값을 넘겼는지는 아직 측정하지 않았다.</p>
<p>인프라가 살아난 것이지, 회로가 맞다는 뜻은 아니다.</p>
<p>그리고 3번을 위에 적어둔 Case B 목록과 나란히 놓으면 이렇게 된다.</p>
<pre><code class="language-text">m_axis_tlast가 4096번째 output에서 정확히 1 cycle 발생하는지
input TLAST와 output TLAST 관계가 꼬이지 않는지</code></pre>
<p>메인 FIR가 통과했으니 이 목록은 쓸 일이 없었다. <strong>그런데 두 달쯤 뒤, 정확히 이 두 줄에 해당하는 버그가 나온다.</strong> 통과했다고 목록이 틀린 게 아니었다. 그건 다음 편에서 다룬다.</p>
<h2 id="16일과-하루의-차이">16일과 하루의 차이</h2>
<p>같은 &quot;보드가 안 돈다&quot;였는데 2편은 16일이 걸렸고 끝내 원인을 못 밝혔다. 이번 건은 관찰부터 원인 확정, 검증까지 하루에 끝났다. 문제가 더 쉬워서가 아니다.</p>
<p><strong>2편에서는 그럴듯한 가설이 떠오르는 순서대로 쫓아다녔다.</strong> JTAG 클럭 → 버스트 → DDR 경로 → Board Delay → ps7_init. 하나씩 확인할 때마다 &quot;아니었다&quot;는 정보는 얻었지만, 다음에 뭘 볼지는 여전히 감으로 정했다.</p>
<p><strong>이번에는 처음부터 &quot;무엇을 배제할 것인가&quot;를 정하고, 그 조건을 만족하는 회로를 만들어서 돌렸다.</strong> 특히 <code>assign s_axis_tready = 1&#39;b1;</code> 한 줄짜리 smoke test가 결정적이었다. RTL을 며칠 노려보는 것보다, <strong>&quot;이 회로는 구조적으로 backpressure를 줄 수 없다&quot;</strong>는 상태를 만들어놓고 한 번 돌리는 게 빨랐다.</p>
<p>의심스러운 걸 고치는 게 아니라 <strong>의심 자체가 불가능한 상태를 만드는 것</strong>이 디버깅이었다.</p>
<p>돌아보면 그 하루는 실험 하나가 아니라 네 겹이었다.</p>
<pre><code>① 관측 가능하게 만든다      trace + 출력 판정표
② 실험 장치를 신뢰 가능하게  boot tag로 &quot;엉뚱한 이미지&quot; 가능성 제거
③ 판정을 먼저 정한다        Case A/B/C를 결과 보기 전에 문서화 (두 번)
④ 증명 범위에 선을 긋는다    통과가 증명한 것 / 안 한 것 구분</code></pre><p>이 중 셋(①②③)이 <strong>원인을 찾기 전에</strong> 깔린 것들이다. 2편에서는 저 자리에 아무것도 없이 가설부터 쫓았고, 그래서 16일이 갔다.</p>
<p>그리고 결정적 원인은 1편에서 내가 표에 &quot;충분함&quot;이라고 적고 넘어간 한 줄이었다. 그때 계산을 안 한 게 아니라 <strong>틀린 쪽을 계산했다.</strong> 이게 제일 무서운 종류의 실수였다. 계산을 안 했으면 불안해서라도 다시 봤을 텐데, 계산을 했기 때문에 확인이 끝났다고 믿었다.</p>
<hr>
<h2 id="마무리하며">마무리하며</h2>
<p><code>c_sg_length_width</code>를 미리 알고 있었다면 한 시간이면 끝났을 겁니다. 하지만 몰랐고, 그래서 하루가 걸렸습니다. 2편도 똑같이 몰랐는데 16일이 갔습니다. 차이는 아는 양이 아니라, 모르는 상태에서 무엇을 먼저 깔아뒀느냐였습니다. 보이게 만들고, 실험 장치를 믿을 수 있게 만들고, 판정을 미리 적어두는 것. 셋 다 원인 자체와는 아무 상관 없는 작업인데, 정작 원인을 찾아준 건 그 셋이었습니다.</p>
<p>하나 더 남습니다. 이름을 문서처럼 읽지 말 것. <code>S2MM_DMACR</code>은 채널별로 보였지만 코어 전체를 리셋했고, <code>c_sg_length_width</code>는 SG 전용으로 보였지만 simple 모드에도 걸렸습니다. 같은 실수를 두 번 하고 나서야 이게 우연이 아니라 패턴이라는 걸 알았습니다.</p>
<p>그리고 이번 편 마지막에 &quot;증명한 것 / 증명하지 않은 것&quot;으로 선을 그으면서, 3번 항목을 증명 안 된 채로 남겨뒀습니다 — 래퍼의 <code>TLAST</code>/<code>valid</code>/<code>ready</code> 처리가 긴 스트림에서 항상 안전한지. 다음 글이 그 이야기입니다.</p>
<p>보드 데모는 계속 통과했습니다. mode 1-1도 mode 1-2도 정상이었고, 발표에 쓸 plot도 다 나왔습니다. 그런데 시뮬레이션을 제대로 돌리자 AXI-Stream 래퍼에서 skid buffer 버그가 네 개 나왔습니다. 고치고 나서 코드 리뷰를 했더니 <code>TLAST</code> 직전 버블에서 데드락이 하나 더 있었고, hold-back으로 다시 고쳤습니다. 마지막엔 버블 스윕 회귀 테스트를 붙여 같은 게 다시 들어오지 못하게 막았습니다.</p>
<p>&quot;동작한다&quot;와 &quot;맞다&quot;는 다릅니다. 3편이 인프라를 살린 이야기였다면, 다음 편은 그 위에 올라간 회로가 실제로 맞는지 따지는 이야기입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ [Zynq FIR IP 개발 #2 :  DDR byte[3] 오염 — JTAG ELF 로딩에 걸린 16일]]]></title>
            <link>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-2-DDR-byte3-%EC%98%A4%EC%97%BC</link>
            <guid>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-2-DDR-byte3-%EC%98%A4%EC%97%BC</guid>
            <pubDate>Wed, 05 Aug 2026 10:24:08 GMT</pubDate>
            <description><![CDATA[<p>1편에서 Block Design을 만들고 비트스트림과 <code>.xsa</code>까지 뽑았다. 이제 Vitis에서 C 코드를 짜서 보드에 올리면 끝날 줄 알았다.</p>
<p>보드에 올리는 것부터 막혔다. <strong>16일이 걸렸고, 끝내 원인을 못 밝혔다.</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>증상</td>
<td>JTAG로 ELF를 DDR에 올리면 32비트 워드의 <strong>상위 1바이트가 오염된다</strong></td>
</tr>
<tr>
<td>기간</td>
<td>05-08 ~ 05-24 (16일)</td>
</tr>
<tr>
<td>결말</td>
<td>완전 해결 실패. JTAG를 버리고 SD 부팅으로 우회</td>
</tr>
</tbody></table>
<p>원인을 못 밝힌 이야기를 굳이 쓰는 이유는, 그 과정에서 배운 게 원인 자체보다 많았기 때문이다. 특히 <strong>답을 이미 적어놓고도 못 찾은 경험</strong>이 그렇다.</p>
<hr>
<h2 id="이-글에-나오는-xsdb-명령어">이 글에 나오는 xsdb 명령어</h2>
<p><code>xsdb</code>는 Xilinx의 JTAG 디버거다. PC에서 JTAG 케이블을 통해 보드의 메모리를 직접 읽고 쓰거나, CPU를 세우고 실행시킬 수 있다. 이 글에서 계속 나오는 명령은 여섯 개다.</p>
<table>
<thead>
<tr>
<th>명령</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td><code>fpga</code></td>
<td>비트스트림을 PL에 프로그래밍 (PS는 안 건드림)</td>
</tr>
<tr>
<td><code>dow</code></td>
<td>ELF를 메모리에 다운로드</td>
</tr>
<tr>
<td><code>dow -data &lt;bin&gt; &lt;addr&gt;</code></td>
<td>바이너리를 특정 주소에 그대로 적재</td>
</tr>
<tr>
<td><code>mwr</code> / <code>mrd</code></td>
<td>메모리 워드 단위 쓰기 / 읽기</td>
</tr>
<tr>
<td><code>con</code> / <code>stop</code></td>
<td>실행 / 정지</td>
</tr>
<tr>
<td><code>rst</code></td>
<td>리셋</td>
</tr>
</tbody></table>
<hr>
<h2 id="시작하기-전에-jtag-부팅에는-ps7_init이-필요하다">시작하기 전에: JTAG 부팅에는 ps7_init이 필요하다</h2>
<p><code>dow</code>로 ELF를 올리려 하자 첫 에러가 이거였다.</p>
<pre><code>Memory write error at 0x100000. Cannot access DDR: the controller is held in reset.</code></pre><p><code>fpga</code> 명령은 <strong>PL만 프로그래밍하고 PS는 건드리지 않는다.</strong> SD 부팅이라면 FSBL이 알아서 DDR 컨트롤러를 깨우지만, JTAG 직접 부팅에는 FSBL이 없다. 그러니 DDR은 리셋 상태 그대로다.</p>
<p>비트스트림을 올린 뒤, CPU 리셋 다음에 <code>ps7_init</code>을 직접 돌려야 한다.</p>
<pre><code class="language-tcl">targets -set -filter {name =~ &quot;*A9*#0&quot;}
rst -processor -clear-registers
source $PS7_INIT
ps7_init
ps7_post_config</code></pre>
<p>이걸 통과하고 나서 진짜 문제가 시작됐다.</p>
<hr>
<h2 id="증상">증상</h2>
<p><code>xsdb</code>로 비트스트림을 올리고 ELF를 DDR(<code>0x100000</code>)에 다운로드한 뒤 읽어보면 이랬다.</p>
<pre><code class="language-tcl">mwr 0x100000 0x11111111
mwr 0x100004 0x22222222
mwr 0x100008 0x33333333
mwr 0x10000C 0x44444444
mrd 0x100000 4

# 100000: F2111111   ← byte[3]=F2, 비정상
# 100004: F2222222   ← byte[3]=F2, 비정상
# 100008: F2333333   ← byte[3]=F2, 비정상
# 10000C: 44444444   ← 정상</code></pre>
<p>하위 3바이트는 멀쩡한데 <strong>최상위 1바이트만</strong> 다른 값으로 덮인다. 그리고 <strong>마지막 워드만 정상</strong>이다.</p>
<p>ELF가 이렇게 깨지면 CPU는 부팅 즉시 죽는다. 이 ELF는 맨 앞에 예외 벡터 테이블이 놓이기 때문에, <code>0x100000</code>부터 오염된 워드가 그대로 잘못된 명령어가 된다. 실제로 <code>dow</code>가 &quot;성공&quot;으로 끝난 뒤 <code>con</code>을 쳐도 UART에 한 글자도 안 나왔고, PC를 읽어보면 이랬다.</p>
<pre><code class="language-tcl">rrd pc
→ 0x00000010    # Data Abort 벡터</code></pre>
<p>부팅한 게 아니라 예외 핸들러로 떨어진 것이다. 세션에 따라 SVC exception(<code>PC=0x8</code>)으로 죽기도 했다.</p>
<hr>
<h2 id="원인은-사실-이때-이미-발견했다">원인은 사실 이때 이미 발견했다</h2>
<p>&quot;마지막 워드만 정상&quot;이라는 지문이 결정적이었다. 오염이 무작위였다면 마지막 워드라고 정상일 이유가 없다. 순서와 관련이 있다는 뜻이다.</p>
<p>명령을 주는 방식을 바꿔가며 관찰을 모았다.</p>
<table>
<thead>
<tr>
<th>명령을 주는 방식</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>터미널에서 한 줄씩 손으로 타이핑</td>
<td>전부 정상</td>
</tr>
<tr>
<td>버스트 <code>mwr {w0 w1 w2 w3}</code> 한 번</td>
<td>마지막 워드만 정상</td>
</tr>
<tr>
<td><code>source script.tcl</code> / <code>dow</code></td>
<td>전부 오염</td>
</tr>
</tbody></table>
<p>이 셋을 동시에 설명하는 가장 단순한 가설은 이거였다.</p>
<blockquote>
<p>xsdb의 JTAG AXI 마스터는 <code>mwr</code> 실행 즉시 byte[0~2]를 전송하지만, <strong>byte[3]은 내부
파이프라인 버퍼에 hold한다.</strong> byte[3]이 commit되는 조건은 &quot;xsdb가 stdin에서 <strong>다음 명령을
기다리며 블로킹할 때</strong>&quot;다.</p>
</blockquote>
<p>손으로 칠 때는 Enter를 누르기 전까지 xsdb가 stdin에서 멈춰 있으니 매 워드가 커밋된다.
버스트는 명령 하나라 끝에서 딱 한 번 블로킹하고, 그래서 마지막 워드만 제대로 쓰여진다. <code>dow</code>는 내부 bulk 전송이고 <code>source</code>는 파서가 파일에서 곧장 읽으므로, 워드 사이에 stdin 블로킹이 아예 없다.</p>
<p><strong>Xilinx 문서에 이렇게 적혀 있는 건 아니다.</strong> 관찰 세 개를 동시에 설명하려고 내가 세운 동작 모델이다. 이 구분이 나중에 중요해진다.</p>
<p>그리고 이건 DDR도, ps7_init도, 보드도 아니라 xsdb 툴 레이어의 문제라는 뜻이었다.</p>
<h3 id="즉-정리하면">즉, 정리하면</h3>
<p>xsdb한테 &quot;이 주소에 이 값 써라&quot;라고 시키면, 4바이트 중 앞의 3바이트는 바로 써진다.
그런데 맨 위 1바이트는 안 나가고 xsdb 안에 잠깐 hold되어 있다. 이 데이터는 xsdb가 할 일을 다 하고 다음 명령어를 기다릴 때 써진다.</p>
<p>그러니까 터미널에 한 줄 치고 엔터 누르고, 또 한 줄 치고 엔터 누르면 매번 xsdb가
명령어를 기다리느라 멈추니까 그때마다 마지막 1바이트가 잘 나간다. 그래서 손으로 치면 멀쩡했던 거다.</p>
<p>문제는 ELF를 올릴 때인데, 이건 워드가 만 개가 넘어서 손으로 칠 수 없기에 스크립트로 한 번에 입력하는데, 그러면 xsdb가 중간에 멈출 필요가 없다. 그래서 워드마다 맨 위 1바이트가 계속 안 나간 채로 밀리고, 다음 워드 쓸 때 그 자리에 이전 값이 남아 있는 게 보인다. 스크립트가 끝나면 그제서야 xsdb가 멈추니까 딱 하나, 제일 마지막 워드만 살아남는다.</p>
<hr>
<h2 id="첫-해결-시도">첫 해결 시도</h2>
<p><code>mwr</code> 뒤에 <code>mrd</code>를 붙이는 것이었다. <code>mrd</code>가 &quot;다음 JTAG 명령&quot; 역할을 하면서 직전 <code>mwr</code>의 byte[3]을 밀어낼 거라고 생각했다.</p>
<pre><code class="language-python">cmds.append(f&quot;mwr 0x{addr:08X} 0x{word:08X}&quot;)
cmds.append(f&quot;mrd 0x{addr:08X} 1&quot;)   # JTAG write buffer flush 유도</code></pre>
<p>ELF의 PT_LOAD 세그먼트를 워드 단위로 쪼개 이 쌍을 13,769개 생성했다.
<strong>그런데 해결이 되지 않았다.</strong>
sourced 모드에서는 <code>mrd</code>도 같은 실행 컨텍스트에서 연속 파싱될 뿐이라 stdin 블로킹이 생기지 않기 때문이다.</p>
<table>
<thead>
<tr>
<th>삽입한 것</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>mrd 0x{same_addr} 1</code></td>
<td>byte[3] 여전히 오염</td>
</tr>
<tr>
<td><code>after 1</code> (1 ms)</td>
<td>byte[3]이 데이터가 아니라 <code>0x00</code>으로 flush됨</td>
</tr>
<tr>
<td><code>after 0</code></td>
<td>byte[3]뿐 아니라 byte[0~2]까지 전부 <code>0x00</code></td>
</tr>
<tr>
<td><code>mrd 0xF8000000 1</code> (다른 주소)</td>
<td>sourced 모드 배치 처리라 효과 없음</td>
</tr>
</tbody></table>
<p><code>after 1</code>은 오히려 xsdb의 idle timeout을 트리거해서, byte[3]을 실제 데이터가 아닌 <code>0x00</code>으로 밀어내버렸다.</p>
<p>그래서 pexpect로 PTY를 에뮬레이션해 xsdb가 자신을 인터랙티브 터미널로 착각하게 만들었다.</p>
<pre><code class="language-python">xsdb = pexpect.spawn(&#39;xsdb&#39;, encoding=&#39;utf-8&#39;, timeout=60)
for addr, word in words:
    xsdb.sendline(f&quot;mwr 0x{addr:08X} 0x{word:08X}&quot;)
    xsdb.expect(&#39;xsdb%&#39;)   # ← 이 대기가 인터랙티브 REPL과 같은 barrier
    xsdb.buffer = &#39;&#39;</code></pre>
<p>이때는 성공했다고 기록했다. 나중에 이 기록이 틀렸다는 걸 알게 된다.</p>
<hr>
<h2 id="두-번째-해결-시도-한-줄씩-치는-걸-자동화하려다-만난-4연속-에러">두 번째 해결 시도: 한 줄씩 치는 걸 자동화하려다 만난 4연속 에러</h2>
<p>손으로 치면 정상이라는 걸 알았으니, 손으로 치는 걸 그대로 흉내내면 될 것이라고 생각했다.</p>
<p><strong>1. — <code>subprocess.PIPE</code>로 붙였더니 초기화 단계에서 무한 대기.</strong>
xsdb가 자기가 비대화형 환경에 있다는 걸 감지하고 블록 버퍼링을 해버려서 프롬프트를 안 내보냈다. 프로그램은 자기 출력이 어디로 나가는지 확인하고 버퍼링 방식을 바꾼다. 터미널이면 사람이 보고 있다는 뜻이니 <strong>한 줄 나올 때마다 바로</strong> 내보내고, 파이프면 다른 프로그램이 받는다는 뜻이니 <strong>몇 KB 모일 때까지 쌓아뒀다가</strong> 한 번에 내보낸다. 후자가 훨씬 효율적이기 때문이다.</p>
<p>문제는 <code>xsdb%</code> 프롬프트가 짧다는 것이다. 몇 바이트짜리라 버퍼가 찰 리가 없다. Python은 <code>xsdb%</code>가 오기를 기다리고, xsdb는 버퍼가 찰 때까지 안 내보낸다. <strong>둘 다 상대를 기다리며 영원히 멈춘다.</strong> 명령을 한 줄도 못 보내고 초기화 단계에서 죽은 이유가 이거였다.
→ <code>pexpect</code>로 가상 터미널(PTY)을 에뮬레이션해서 대화형 환경을 강제했다.</p>
<p><strong>2. — 되긴 되는데, 너무 느렸다.</strong></p>
<pre><code>약 19 words/s × 13,000 words = 10분 이상</code></pre><p>워드 하나마다 Python과 xsdb 사이에 문자열을 주고받고 컨텍스트 스위칭이 일어난다. 그 오버헤드가 실제 JTAG 전송 시간보다 수백 배 컸다. 진행률 숫자가 기어가는 걸 그냥 쳐다보고 있었다. 느리기만 한 것도 아니었다. 중간중간 프롬프트가 안 돌아와서 그대로 멈췄다. 결국 워드마다 타임아웃을 걸고 재시도하는 루프를 씌워야 했다.</p>
<pre><code class="language-python">for i, (addr, word) in enumerate(words):
    while True:
        try:
            xsdb.sendline(f&quot;mwr -force 0x{addr:08X} 0x{word:08X}&quot;)
            xsdb.expect(&#39;xsdb%&#39;, timeout=30)
            break
        except pexpect.TIMEOUT:
            print(f&quot;\n[{i+1}] timeout, 재시도...&quot;, flush=True)</code></pre>
<p>한 번 돌릴 때마다 10분이 넘는데 거기에 멈춤과 재시도가 얹혔다. 설정을 바꿔가며 몇 번 돌리다 보면 30분이 훌쩍 갔다.
→ 128워드씩 묶어서 보내는 chunking을 적용했다.</p>
<p><strong>3. — chunking을 적용했더니, 14% 지점(2048워드)에서 전송이 멈췄다.</strong>
리눅스 커널의 PTY 입력 큐(4~8KB)가 가득 찼는데, xsdb 파서가 소화하는 속도가 파이썬이 타이핑하는 속도를 못 따라간다. 커널 레벨에서 막힌 것이다.
→ 텍스트 통신 자체를 줄이려고, 모든 <code>mwr</code> 명령을 담은 임시 <code>.tcl</code> 파일을 만들어 xsdb에서 <code>source</code>로 한 번에 실행하도록 구조를 바꿨다.</p>
<p><strong>4. — <code>.tcl</code> 방식으로 바꿨더니, 이번엔 <code>Cannot access DDR</code>.</strong>
새로 짠 시퀀스에 들어 있던 <code>rst -system</code>이 Zynq PS 레지스터를 전부 초기화하면서 DDR 컨트롤러가 리셋 상태에 묶였다.
→ 메모리 접근 전에 <code>ps7_init</code>을 반드시 재실행하도록 시퀀스를 고쳤다.</p>
<p>여기까지 고치고 나서야 로딩이 끝까지 완주했다. <strong>그런데 읽어보니 byte[3]은 여전히 오염돼 있었다.</strong>
하루를 꼬박 쓴 결과가 &quot;처음 증상 그대로&quot;였다.
그리고 이 4연속 에러에는 내가 그때 못 본 게 하나 있었다.</p>
<p><strong>3번의 해결책이 원래 문제를 되살렸다는 것이다.</strong></p>
<p><code>source script.tcl</code>은 xsdb 파서가 파일 버퍼에서 명령을 곧장 읽는다. 그러면 워드 사이에 stdin 블로킹이 <strong>없다.</strong> 내가 그토록 흉내내려던 &quot;한 줄씩 치는 상태&quot;를, 속도 문제를 풀려다 스스로 없애버린 것이다.</p>
<p>성능을 위한 우회가 정확성 조건을 깨뜨렸는데, 두 문제를 다른 날 다른 이름으로 겪는 바람에 연결이 안 됐다.</p>
<hr>
<h2 id="디버깅을-방해한-네-가지-함정">디버깅을 방해한 네 가지 함정</h2>
<p>원인을 쫓는 것과 별개로, <strong>관측 자체를 못 믿게 만드는</strong> 문제들이 계속 끼어들었다.</p>
<h3 id="함정-1--읽는-쪽도-거짓말을-한다-l2-캐시">함정 1 — 읽는 쪽도 거짓말을 한다 (L2 캐시)</h3>
<p>flush를 적용하고 나서도 <code>mrd</code> 결과가 이상하게 보이는 경우가 있었다. DDR을 읽었다고 생각했는데 실제로는 <strong>L2 캐시(PL310)에 남아 있던 이전 펌웨어의 데이터</strong>를 읽고 있었다.</p>
<p>그래서 ELF 로드 직전에 L2를 통째로 무효화했다.</p>
<pre><code class="language-tcl">mwr 0xF8F0277C 0x0000FFFF    ;# PL310 Invalidate by Way
after 200</code></pre>
<p>쓰는 쪽만 의심하고 있었는데, 읽는 쪽도 의심 대상이었다.</p>
<h3 id="함정-2--이전-펌웨어가-돌고-있으면-아무것도-안-된다">함정 2 — 이전 펌웨어가 돌고 있으면 아무것도 안 된다</h3>
<p>전원을 안 끄고 다시 다운로드하면 이렇게 났다.</p>
<pre><code>Memory write error at 0xE0001034. AP transaction timeout</code></pre><p>이전 세션에서 <code>con</code>으로 실행된 펌웨어가 DMA/UART 레지스터를 점유한 채 무한 루프를 돌고 있어서다. <code>rst -processor</code>로 halt를 시도해도 <code>Cannot halt processor core, timeout</code>이 뜬다. 이미 멈춰 세울 수 없는 상태다.</p>
<p><strong>전원 재공급 말고는 방법이 없었다.</strong> 소프트 리셋으로는 복구가 안 된다.</p>
<h3 id="함정-3--완료라고-찍혔는데-전부-실패였다">함정 3 — &quot;완료&quot;라고 찍혔는데 전부 실패였다</h3>
<p>이게 제일 악질이었다. pexpect 방식으로 13,769워드를 다 밀어넣고 &quot;완료&quot; 메시지까지 봤는데, 읽어보니 이랬다.</p>
<pre><code>100000: 00000000
100004: 00000000
100008: 00000000
10000C: FF000000</code></pre><p>byte[3] 버그의 패턴(<code>00000031</code>처럼 상위 1바이트만 이상)과 <strong>완전히 다르다.</strong> 전부 0이다. 쓰기 자체가 처음부터 끝까지 실패한 것이다.</p>
<p>두 실패 모드를 구별하는 기준을 이때 정리했다.</p>
<table>
<thead>
<tr>
<th>읽은 패턴</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>0x000000XX</code> — 하위 3바이트만 정상</td>
<td>쓰기는 성공, byte[3] flush 실패</td>
</tr>
<tr>
<td><code>0x00000000</code> — 전체 0</td>
<td>쓰기 자체가 실패, DDR 접근 불가</td>
</tr>
</tbody></table>
<p>원인은 함정 2였다. 이전 <code>con</code> 세션이 살아 있어서 <code>ps7_init</code> 중 UART1 레지스터 접근이 timeout으로 죽었고, 그 뒤 모든 <code>mwr</code>이 실패하고 있었다. 그런데 왜 몰랐냐면 —</p>
<p><strong>xsdb는 <code>mwr</code>이 성공하면 아무 출력도 하지 않는다. 출력이 있으면 그게 에러다.</strong> 그리고 내 스크립트는 매 반복마다 <code>xsdb.buffer = &#39;&#39;</code>로 버퍼를 비우고 있었다. 에러 메시지를 13,769번 받아놓고 13,769번 버린 것이다.</p>
<p>두 가지를 넣었다.</p>
<pre><code class="language-python"># 1. 로딩 시작 전에 DDR이 살아 있는지부터 확인
cmd(xsdb, &quot;mwr 0x100000 0xDEADBEEF&quot;)
out = cmd(xsdb, &quot;mrd 0x100000 1&quot;)
if &quot;DEADBEEF&quot; not in out.upper():
    sys.exit(&quot;DDR 접근 실패 — 전원 재공급 후 재시도&quot;)

# 2. mwr이 뭔가 출력하면 그건 에러다
out = xsdb.before.strip()
if out:
    print(f&quot;[경고] mwr 0x{addr:08X}: {out}&quot;)</code></pre>
<p>앞의 4줄이 13,769번의 헛수고를 막는다. <strong>&quot;성공했다는 신호가 없는 것&quot;과 &quot;성공한 것&quot;은 다르다</strong>는 걸 여기서 배웠다.</p>
<p>그리고 로딩이 끝난 뒤에도 스스로 확인하게 만들었다. 벡터 테이블 첫 4워드를 읽어서 기대값과 대조하고, 틀리면 거기서 죽는다.</p>
<pre><code class="language-python">xsdb.sendline(&quot;mrd -force 0x100000 4&quot;)
xsdb.expect(&#39;xsdb%&#39;, timeout=10)
out = xsdb.before.strip()
expected = {0x100000: 0xEA000031, 0x100004: 0xEA00000D,
            0x100008: 0xEA000013, 0x10000C: 0xEA000023}
ok = all(f&quot;{v:08X}&quot;.upper() in out.upper() for v in expected.values())
if not ok:
    sys.exit(&quot;ELF 로딩 실패 — 전원 재공급 후 재시도&quot;)</code></pre>
<p>없던 성공 신호를 직접 만들어 붙인 셈이다. 이걸 넣고 나서야 &quot;올렸는데 왜 안 돌지&quot;를 고민하는 시간이 사라졌다.</p>
<h3 id="함정-4--하드웨어-문제인-줄-알았는데-내-스크립트-버그였다">함정 4 — 하드웨어 문제인 줄 알았는데 내 스크립트 버그였다</h3>
<p>DDR sanity check가 계속 실패했다. <code>mwr</code>로 <code>0xDEADBEEF</code>를 쓰고 <code>mrd</code>로 읽었는데 <strong>빈 문자열</strong>이 돌아왔다. DDR 접근이 아예 안 되는 줄 알고 전원을 껐다 켜고, <code>ps7_init</code>을 다시 돌리고, 한참을 헤맸다.</p>
<p>원인은 내가 만든 헬퍼 함수였다. 긴 명령이 여러 줄로 표시될 때 <code>xsdb%</code>가 중복 출력되는 걸 막으려고, 남은 프롬프트를 전부 소진하는 while 루프를 넣어뒀었다.</p>
<pre><code class="language-python">while True:
    try:
        xsdb.expect(&#39;xsdb%&#39;, timeout=0.05)
        last_before = xsdb.before      # ← mrd 출력이 여기서 덮어써진다
    except pexpect.TIMEOUT:
        break</code></pre>
<p><code>mwr</code>은 성공 시 출력이 없다. 그래서 이 루프가 <code>mwr</code> 직후 남은 프롬프트를 먹으면서, 바로 뒤 <code>mrd</code>가 뱉은 출력까지 같이 삼켰다. 하드웨어는 멀쩡했고 값도 제대로 써지고 있었는데, <strong>읽어오는 코드가 결과를 버리고 있었던 것이다.</strong></p>
<p>sanity check만 헬퍼를 안 쓰고 직접 <code>sendline</code>/<code>expect</code>로 바꿨더니 바로 정상이 나왔다.</p>
<p>함정 1이 &quot;읽는 쪽(캐시)이 거짓말한다&quot;였다면, 이건 &quot;읽는 코드가 거짓말한다&quot;였다. 디버깅 도구를 직접 만들어 쓸 때는 <strong>그 도구도 디버깅 대상</strong>이다.</p>
<hr>
<h2 id="그리고-며칠-뒤-증상이-재발했다">그리고 며칠 뒤, 증상이 재발했다</h2>
<p>문제는 그다음이었다. 다시 오염이 나타났는데, 이번엔 <strong>내가 위 진단을 떠올리지 못했다.</strong> 그리고 완전히 다른 방향을 파기 시작했다.</p>
<h3 id="가설-1--jtag-클럭이-빠르다">가설 1 — JTAG 클럭이 빠르다</h3>
<pre><code class="language-tcl">jtag frequency 1000000    ;# 1MHz로 대폭 하향
dow build/output/fir_decimator_demo.elf</code></pre>
<p>전송 속도는 0.1MB/s로 정직하게 느려졌다. 오염은 그대로.</p>
<h3 id="가설-2--버스트-전송이-문제다">가설 2 — 버스트 전송이 문제다</h3>
<pre><code class="language-tcl">mwr -force 0x100000 0x12345678      → 12345678   정상

mwr 0x100000 {0x11111111 0x22222222 0x33333333 0x44444444}
                                    → 11222222 ...  byte[3]에 이전 값 잔류</code></pre>
<p>단일은 되고 버스트는 깨진다. 여기서 &quot;버스트 FIFO 오버플로우&quot;를 의심했다.</p>
<h3 id="가설-3--ddr-경로만의-문제인가">가설 3 — DDR 경로만의 문제인가</h3>
<p>같은 JTAG, 같은 명령, 목적지만 바꿨다. 외부 DDR 대신 칩 내부 메모리 OCM으로.</p>
<pre><code class="language-tcl"># OCM 버스트
mwr -force 0x00000000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00000000 4
→ 11111111 22222222 33333333 44444444   ✅ 완벽

# DDR 버스트
mwr -force 0x00100000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00100000 4
→ 00111111 00222222 00333333 44444444   ❌ byte[3] 소실</code></pre>
<p>OCM은 멀쩡하고 DDR만 깨진다. <strong>이 결과가 나를 완전히 DDR 하드웨어 쪽으로 몰아갔다.</strong></p>
<h3 id="가설-4--board-delay가-0ns였다">가설 4 — Board Delay가 0ns였다</h3>
<p>Vivado에서 PS7 IP의 <code>DDR Configuration</code>을 열었더니 실제로 이상한 게 나왔다. 프로젝트 타겟은 Zybo Z7-20인데 <strong>IP 내부 Presets가 <code>None</code></strong>이었고, DDR 배선 길이 차를 보정하는 <strong><code>Board Delay</code>가 전부 0ns</strong>였다.</p>
<p>말이 됐다. 모든 데이터 선의 길이가 같다고 착각하면, 실제로 조금 긴 배선의 타이밍 마진이 먼저 무너진다. 증상이 특정 바이트 레인에만 몰린 것과 맞아떨어졌다.</p>
<p>BD를 새로 만들고 <code>Run Block Automation</code>으로 Zybo 보드 파일을 강제 주입해 <code>DQ[31:24]</code>의 Board Delay가 <code>0.244ns</code> 같은 실제 값으로 채워진 걸 확인했다. 비트스트림을 다시 굽고 같은 테스트를 돌렸다.</p>
<p><strong>똑같이 깨졌다.</strong> 오염되는 값의 패턴만 <code>11</code>(잔류)에서 <code>00</code>(소실)로 바뀌었을 뿐이다.</p>
<h3 id="가설-5--ps7_inittcl이-잘못됐다">가설 5 — ps7_init.tcl이 잘못됐다</h3>
<p><code>ps7_init.tcl</code>은 DDR 컨트롤러 레지스터를 세팅하는 스크립트다. 이게 틀리면 DDR 타이밍이 틀어진다. Digilent 공식 Z7-20 레퍼런스와 diff를 떠봤더니 실제로 차이가 있었다.</p>
<p><strong>레퍼런스에는 있는데 내 파일에는 없는 것</strong></p>
<pre><code class="language-text">0xF8000180  0x00100A20        ← DDRIOB 설정 (DDR I/O 버퍼)
0xF8000190  0x00100500        ← DDRIOB 설정 (DDR I/O 버퍼)
mask_delay 0xF8F00200 × 5     ← DDR PHY 초기화 타이밍 딜레이</code></pre>
<p><strong>DDRC 타이밍 레지스터 값도 달랐다</strong></p>
<table>
<thead>
<tr>
<th>레지스터</th>
<th>레퍼런스 (Z7-20)</th>
<th>내 프로젝트</th>
</tr>
</thead>
<tbody><tr>
<td><code>0xF8006004</code></td>
<td><code>0x00001081</code></td>
<td><code>0x00001082</code></td>
</tr>
<tr>
<td><code>0xF8006014</code></td>
<td><code>0x0004281A</code></td>
<td><code>0x0004285B</code></td>
</tr>
<tr>
<td><code>0xF8006018</code></td>
<td><code>0x44E458D2</code></td>
<td><code>0x44E458D3</code></td>
</tr>
<tr>
<td><code>0xF800601C</code></td>
<td><code>0x720238E5</code></td>
<td><code>0x7282BCE5</code></td>
</tr>
<tr>
<td><code>0xF8006030</code></td>
<td><code>0x00040930</code></td>
<td><code>0x00040B30</code></td>
</tr>
</tbody></table>
<p>DDR PHY 딜레이 설정이 통째로 빠져 있고 타이밍 값도 다르다. 이제 확실하다고 생각했다.</p>
<p>레퍼런스 <code>ps7_init.tcl</code>로 교체했다. word 0만 정상화되고 나머지는 그대로였다. 게다가 실행할 때마다 오염값이 달라졌다.</p>
<pre><code class="language-text">실행 1: F9000031  F900000D  F9000013  EA000023
실행 2: F2000031  F200000D  F2000013  F2000023
실행 3: F4000031  F400000D  F4000013  F4000023</code></pre>
<p>기대값은 <code>EA000031 EA00000D EA000013 EA000023</code>이다. <code>F9</code>, <code>F2</code>, <code>F4</code> — <strong>비결정적이다.</strong> &quot;마지막 워드만 정상&quot;이라는 규칙마저 3차에서 무너졌다.</p>
<p>버스트 테스트로 정리하면 이렇다. word 0은 살아났지만 나머지는 실행할 때마다 다른 값이 박혔다.</p>
<table>
<thead>
<tr>
<th>ps7_init 버전</th>
<th>word0</th>
<th>word1</th>
<th>word2</th>
<th>word3</th>
</tr>
</thead>
<tbody><tr>
<td>원본 (BD 수정 전)</td>
<td><code>00</code></td>
<td><code>00</code></td>
<td><code>00</code></td>
<td><code>44</code> ✅</td>
</tr>
<tr>
<td>원본 (BD 수정 후)</td>
<td><code>00</code></td>
<td><code>00</code></td>
<td><code>00</code></td>
<td><code>44</code> ✅</td>
</tr>
<tr>
<td>레퍼런스 (잔류 있음)</td>
<td><code>11</code> ✅</td>
<td><code>D0</code></td>
<td><code>EC</code></td>
<td><code>44</code> ✅</td>
</tr>
<tr>
<td>레퍼런스 (클리어 후)</td>
<td><code>11</code> ✅</td>
<td><code>11</code></td>
<td><code>F9</code></td>
<td><code>44</code> ✅</td>
</tr>
</tbody></table>
<p>패턴이 <strong>바뀌기는 하는데 사라지지는 않는다.</strong> 이게 사람을 제일 헷갈리게 했다. 뭔가 건드릴 때마다 결과가 달라지니 &quot;방향은 맞다&quot;고 믿게 된다.</p>
<hr>
<h2 id="교차-검토에서-마주친-것">교차 검토에서 마주친 것</h2>
<p>여기서 막혀서, 이전 디버깅 로그를 처음부터 다시 읽었다. 그리고 며칠 전 문서에서 이 문장을 찾았다.</p>
<blockquote>
<p>xsdb의 JTAG AXI 마스터는 byte[3]을 파이프라인 버퍼에 hold한다. commit 조건은 다음 명령 대기 시 REPL 블로킹이다.</p>
</blockquote>
<p><strong>이미 진단해놨고, 해결책까지 만들어놨던 문제였다.</strong> 가설 1~5가 전부 잘못된 방향이었다. <code>ps7_init</code> 교체로 word 0이 정상화된 것도 DDR이 좋아져서가 아니라, ps7_init 시퀀스의 레지스터 write들이 <strong>파이프라인 버퍼 상태를 우연히 흔든 부수 효과</strong>였을 뿐이다.</p>
<p>이 사실을 문서에 그대로 적었다. &quot;ps7_init.tcl / DDR PHY 방향 조사는 잘못된 가설 추적이었다.&quot;</p>
<hr>
<h2 id="그런데-진단대로-해도-안-됐다">그런데 진단대로 해도 안 됐다</h2>
<p>이제 알았으니 고치면 될 일이었다. pexpect로 xsdb REPL을 붙잡고, 워드마다 프롬프트를 확실히 기다리며 단일 <code>mwr</code>을 13,769번 실행했다. 진단이 맞다면 매 워드가 다음 명령을 만나므로 전부 커밋돼야 한다.</p>
<pre><code class="language-tcl">mrd -force 0x100000 4
→ F9000031  F900000D  F9000013  ...</code></pre>
<p><strong>비결정적으로 재발했다.</strong> 이전 문서가 &quot;검증된 방법&quot;으로 기록해둔 방식인데도 그랬다.</p>
<hr>
<h2 id="마지막-시도-변수를-하나만-남기고-다-지우기">마지막 시도: 변수를 하나만 남기고 다 지우기</h2>
<p>열흘쯤 뒤에 혹시나 해서 한 번 더 시도했다. 아직 완전히 검증되지 않은 가설이 하나 남아 있었기 때문이다.</p>
<p><strong>&quot;내 Block Design이 잘못돼서 <code>ps7_init</code>이 틀리게 생성된 것 아닌가?&quot;</strong></p>
<p>앞에서 본 것처럼 내 XSA의 <code>ps7_init</code>은 레퍼런스와 값이 달랐다(<code>0x1082</code> vs <code>0x1081</code>). FIR과 DMA를 붙인 BD가 PS7 설정을 어딘가 건드렸을 수 있다. 그럴듯했다.</p>
<p>이번엔 가설을 쫓는 대신 <strong>통제 실험</strong>을 짰다. 확인해야 할 건 &quot;BD가 원인인가&quot;이므로, <strong>BD를 없애면 된다.</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설정</th>
</tr>
</thead>
<tbody><tr>
<td>Project type</td>
<td>RTL Project</td>
</tr>
<tr>
<td>Sources / constraints</td>
<td>추가 없음</td>
</tr>
<tr>
<td>Board</td>
<td>Zybo Z7-20</td>
</tr>
<tr>
<td>Block Design</td>
<td><strong>ZYNQ7 Processing System 단독</strong></td>
</tr>
<tr>
<td>적용</td>
<td>Run Block Automation</td>
</tr>
<tr>
<td>Export</td>
<td>bitstream 없이 XSA만</td>
</tr>
</tbody></table>
<p>FIR도, DMA도, 내가 쓴 RTL도 한 줄 없는 최소 프로젝트다. 여기서 나온 <code>ps7_init</code>이 레퍼런스와 같으면 내 BD가 범인이고, 다르면 내 BD는 무죄다.</p>
<p>결과는 이랬다.</p>
<pre><code class="language-text">최소 PS7-only XSA의 ps7_init:
  0xF8006004 -&gt; 0x00001082 / 0x00081082
  0xF8006014 -&gt; 0x0004285B
  0xF8006018 -&gt; 0x44E458D3
  0xF800601C -&gt; 0x7282BCE5
  0xF8006030 -&gt; 0x00040B30</code></pre>
<p><strong>내 FIR DMA XSA와 완전히 같았다.</strong> PS7 하나만 올려도 같은 값이 나온다는 건, 이게 내 설계 탓이 아니라 <strong>현재 Vivado 2024.2 + 현재 Zybo 보드 파일 조합의 기본 생성 결과</strong>라는 뜻이다. 가설 하나가 깨끗하게 사라졌다.</p>
<p>그다음, 두 종류의 <code>ps7_init</code>으로 각각 DDR 버스트 쓰기를 돌렸다.</p>
<pre><code class="language-tcl"># (A) 레퍼런스 계열 ps7_init (0x1081 계열)
source .../fir_dma_bringup_demo/_ide/psinit/ps7_init.tcl
ps7_init; ps7_post_config
mwr -force 0x00100000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00100000 4
→ D7111111  E9222222  4D333333  EF444444

# (B) 최소 XSA에서 꺼낸 ps7_init (0x1082 계열)
source /tmp/zybo_ps7_bringup_min_ps7_init.tcl
ps7_init; ps7_post_config
mwr -force 0x00100000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00100000 4
→ 00111111  00222222  00333333  44444444</code></pre>
<p><strong>둘 다 오염됐다.</strong> 값의 모양만 다르고 byte[3]이 깨지는 건 똑같다.</p>
<p>같은 라운드에서 두 가지를 더 확인했다.</p>
<p><strong>XSA의 하드웨어 설정 자체는 정상이었다.</strong> <code>hwh</code>를 열어보니 DDR 파라미터가 전부 제대로 들어가 있었다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>DDR part</td>
<td><code>MT41K256M16 RE-125</code></td>
</tr>
<tr>
<td>DDR bus width</td>
<td><code>32 Bit</code></td>
</tr>
<tr>
<td>Board delay</td>
<td><code>0.221 / 0.222 / 0.217 / 0.244</code></td>
</tr>
<tr>
<td>CL / CWL</td>
<td><code>7 / 6</code></td>
</tr>
<tr>
<td>Speed bin</td>
<td><code>DDR3_1066F</code></td>
</tr>
</tbody></table>
<p>가설 4에서 <code>0ns</code>였던 Board Delay가 이제 실제 값으로 채워져 있다. <strong>그걸 고쳐놨는데도 오염이 남아 있다는 뜻이다.</strong></p>
<p><strong>FIR RTL도 다시 의심했다가 배제했다.</strong> 시뮬레이션을 전부 다시 돌렸다.</p>
<pre><code class="language-text">make run_all
PASS [S1] TREADY=1 data+TLAST: 4096 samples
PASS [S2] Random Backpressure + Bubble: 4096 samples
PASS [S3] Reset Recovery: 4096 samples
PASS tb_fir_decimator_n43_axis: all scenarios
5개 TB 전부 PASS</code></pre>
<p>애초에 <code>dow</code>/<code>mwr</code>/<code>mrd</code> 경로는 PL을 거치지도 않는다. 그래도 확인해둔 이유는, 그때쯤 되면 <strong>모든 게 의심스러워지기 때문이다.</strong> 근거 없이 의심하는 걸 끊으려면 근거를 만들어야 했다.</p>
<p>이 시도의 결론은 짧다.</p>
<blockquote>
<p><code>ps7_init</code> 값이 무엇이든 byte[3] 오염은 재현된다. 이건 <code>ps7_init</code> 문제가 아니다.</p>
</blockquote>
<p>여기서 확실해진 게 하나 더 있다. <strong><code>ps7_init</code> 값이 레퍼런스와 다르다는 사실과, DDR write가 오염된다는 사실은 별개의 문제였다.</strong> 둘 다 &quot;DDR이 이상하다&quot;로 묶여 있었을 뿐이다. 둘을 한 덩어리로 보고 있는 동안은 어느 쪽도 못 풀었다.</p>
<hr>
<h2 id="결론-실패했다">결론: 실패했다</h2>
<p>16일 동안 지운 변수는 이렇다.</p>
<table>
<thead>
<tr>
<th>시도</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>jtag frequency 1000000</code> + <code>dow</code></td>
<td>속도만 감소, 오염 동일</td>
</tr>
<tr>
<td>단일 <code>mwr</code> 수동</td>
<td>정상</td>
</tr>
<tr>
<td>단일 <code>mwr</code> × 13,769 (REPL 배리어)</td>
<td>비결정적 오염 재발</td>
</tr>
<tr>
<td>버스트 <code>mwr</code></td>
<td>byte[3] 잔류/소실</td>
</tr>
<tr>
<td><code>mwr</code> + <code>after 1</code></td>
<td>byte[3] → <code>0x00</code> null flush</td>
</tr>
<tr>
<td>OCM 버스트</td>
<td><strong>완전 정상</strong></td>
</tr>
<tr>
<td>Board Delay 수정 비트스트림</td>
<td>오염 동일</td>
</tr>
<tr>
<td>레퍼런스 <code>ps7_init.tcl</code></td>
<td>오염 패턴만 변화</td>
</tr>
<tr>
<td>최소 PS7-only XSA의 <code>ps7_init</code></td>
<td>오염 동일</td>
</tr>
<tr>
<td>FIR RTL 시뮬레이션 (TB 5종)</td>
<td>전부 PASS — 애초에 이 경로와 무관</td>
</tr>
</tbody></table>
<p>이걸 다 지우고 남은 건 둘이었다.</p>
<ol>
<li><strong>xsdb의 JTAG-AXI 파이프라인</strong> — 진단은 정확하지만 REPL 배리어로 완전히 해소되지 않는다. <code>source</code> 모드에서는 아예 재현조차 안 된다.</li>
<li><strong>DDR PHY 타이밍</strong> — OCM이 완전 정상이고 DDR만 깨진다는 사실, Board Delay 수정이 오염 <strong>패턴</strong>을 바꿨다는 사실은 이 가설이 살아 있다는 뜻이다.</li>
</ol>
<p>둘이 겹쳐 있을 가능성이 높다고 봤다. 그리고 둘 다 <strong>내가 만든 게 아니다.</strong> 하나는 툴 레이어, 하나는 하드웨어다. <strong>내가 코드나 설계로 고칠 수 있는 대상이 남아 있지 않았다.</strong></p>
<p>원인을 특정하진 못했지만, 내가 고칠 수 있는 게 없다는 건 확실했다. 그래서 여기서 멈췄다.</p>
<hr>
<h2 id="판단-원인-규명과-목표-달성은-다르다">판단: 원인 규명과 목표 달성은 다르다</h2>
<p>이 시점에 내가 하려던 건 &quot;JTAG를 고치는 것&quot;이 아니라 &quot;보드에서 FIR을 돌리는 것&quot;이었다. JTAG 로딩은 목표로 가는 <strong>여러 경로 중 하나</strong>일 뿐이었다.</p>
<p>그래서 경로를 바꿨다. <strong>SD카드 부팅</strong>이다.</p>
<ul>
<li>FSBL이 DDR을 하드웨어적으로 초기화하고 ELF를 로드한다</li>
<li><code>BOOT.bin</code>(비트스트림 + FSBL + ELF)을 SD에 넣고 전원을 넣으면 자동 실행된다</li>
<li><strong>JTAG 쓰기 경로를 통째로 우회</strong>하므로 byte lane 3 문제의 영향권 밖이다</li>
</ul>
<p>이후 이 프로젝트의 모든 보드 검증은 SD boot + UART를 기준 경로로 고정했다. 규칙을 다섯 줄로 적어놓고 다시는 흔들리지 않기로 했다.</p>
<ol>
<li>JTAG ELF loading / <code>dow</code> / 버스트 <code>mwr</code> 기반 부팅 경로는 <strong>폐기한다.</strong></li>
<li>SD카드 <code>BOOT.bin</code> 부팅 경로로 진행한다.</li>
<li>&quot;<code>ps7_init</code>이 <code>0x1082</code> 계열이면 XSA가 잘못된 것&quot;이라는 이전 판정 기준은 <strong>철회한다.</strong></li>
<li>SD 부팅용 FSBL은 XSA가 생성한 <code>ps7_init.c</code>를 그대로 쓰고, 판정은 DONE LED / UART <code>READY</code> / Python FFT로 한다.</li>
<li>JTAG은 <strong>보드 연결 확인, FPGA 프로그래밍, 단순 target 제어</strong>에만 쓴다. DDR bulk load 검증에는 쓰지 않는다.</li>
</ol>
<p>덧붙이면, 새 보드로 교체한 뒤 같은 SD 부팅에서 DONE LED가 켜지고 <code>READY</code>까지 도달했다. 이전 보드에서는 SD 부팅조차 실패했었다. 그래서 이 증상에 <strong>보드 개체 불량이 섞여 있었을 가능성</strong>도 배제하지 못한다. 이것도 미해결로 남겨뒀다.</p>
<hr>
<h2 id="남은-것">남은 것</h2>
<p>16일을 쓰고 원인을 못 밝힌 채 우회했다. 그런데 진짜 손해는 그게 아니었다.</p>
<p><strong>답을 이미 적어놨는데 안 읽은 것.</strong> 로그를 남기는 이유는 나중에 읽기 위해서인데, 정작 같은 증상이 다른 얼굴로 돌아왔을 때 나는 그걸 새 문제로 취급했다. 그래서 그 뒤로는 증상이 재발하면 코드보다 <strong>내 문서를 먼저 grep한다.</strong></p>
<p>그리고 하나 더. OCM 정상 / DDR 오염이라는 관찰은 그 자체로는 정확했지만, 나를 하드웨어 쪽으로 몰아갔다. <strong>깨끗한 실험 결과가 항상 올바른 방향을 가리키지는 않는다.</strong></p>
<p>세 번째는 <strong>증상이 같은 범주에 있다고 원인을 묶지 말 것</strong>이다. <code>ps7_init</code> 값이 레퍼런스와 다른 것과 DDR write가 오염되는 것은 둘 다 &quot;DDR이 이상하다&quot;로 보였지만 별개의 문제였다. 묶어놓고 보는 동안은 어느 쪽도 안 풀렸고, 게다가 그 오해 때문에 &quot;<code>ps7_init</code> 값이 다르면 XSA가 잘못된 것&quot;이라는 <strong>틀린 판정 기준</strong>까지 만들어서 한동안 그걸로 다른 실험들을 채점하고 있었다.</p>
<p>마지막으로, 이 글에서 얻은 것 중 제일 오래 쓰게 된 건 원인이 아니라 습관 쪽이었다.</p>
<ul>
<li>관측 도구부터 검증한다 (L2 캐시, silent failure, 내가 만든 헬퍼 함수)</li>
<li>&quot;성공했다는 신호가 없는 것&quot;과 &quot;성공한 것&quot;은 다르다 — 없으면 만들어 붙인다</li>
<li>성능을 위한 우회가 정확성 조건을 깨뜨리지 않았는지 확인한다</li>
<li>가설을 쫓기 전에, <strong>변수를 하나만 남기고 다 지울 수 있는지</strong> 먼저 본다</li>
</ul>
<hr>
<h2 id="마무리하며">마무리하며</h2>
<p>SD 부팅으로 바꾸니 UART에 <code>READY</code>가 떴다. 부트 체인은 통과다.</p>
<p>그런데 PC에서 데모를 돌리면 여전히 timeout이었다. 이번엔 DMA가 시작은 되는데 끝나지 않았다. 다음 글은 그 원인을 하루 만에 잡은 이야기다. 원인은 1편에서 내가 &quot;충분함&quot;이라고 적고 넘어간 설정 한 줄이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #17 : GitHub push 인증 오류 해결하기 - PAT Token 방식]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-17-GitHub-push-%EC%9D%B8%EC%A6%9D-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-PAT-Token-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-17-GitHub-push-%EC%9D%B8%EC%A6%9D-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-PAT-Token-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Thu, 11 Jun 2026 09:26:35 GMT</pubDate>
            <description><![CDATA[<p>GitHub repository를 HTTPS 방식으로 clone한 경우, public repository이거나 읽기 권한이 이미 충족된 상태에서는 git clone 또는 git pull은 정상적으로 동작할 수 있습니다. 그러나 git push는 repository 내용을 변경하는 작업이므로 별도의 쓰기 권한 인증이 필요합니다.</p>
<p>이번 글에서는 fine-grained personal access token을 발급하여 HTTPS 방식의 git push 인증 문제를 해결하는 과정을 정리하겠습니다.</p>
<p>HTTPS 방식의 remote를 사용하는 경우, GitHub에 push할 때 username과 password를 요구할 수 있습니다.</p>
<p>이때 입력해야 하는 값은 아래의 이미지와 같습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/ae450784-ea6e-4ad5-b92c-2cd63204851e/image.png" alt="">
Username: GitHub 사용자명
Password: GitHub 계정 비밀번호가 아니라 발급받은 Personal Access Token</p>
<p>즉, password 자리에 GitHub 로그인 비밀번호를 입력하는 것이 아니라, GitHub에서 발급한 token을 입력해야 합니다.</p>
<h2 id="token-발급하기">Token 발급하기</h2>
<p>이번에는 fine-grained token을 기준으로 생성하였습니다.</p>
<p>GitHub 웹사이트에서 다음 순서로 이동합니다.</p>
<p>GitHub 우측 상단 프로필</p>
<p>→ Settings
<img src="https://velog.velcdn.com/images/young-0320/post/20ec0f84-4dd2-4067-99f9-44148a2a9c69/image.png" alt=""></p>
<p>→ 좌측 메뉴 하단 Developer settings
<img src="https://velog.velcdn.com/images/young-0320/post/2325bcfd-425b-4f0e-9da2-a4c8abff3252/image.png" alt=""></p>
<p>→ Personal access tokens</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/a5c18c4c-bf26-41fc-982e-654a54d60856/image.png" alt="">
→ Fine-grained tokens
→ Generate new token</p>
<p>토큰 생성 화면에서 먼저 token name을 입력합니다.
이 이름은 토큰을 구분하기 위한 관리용 이름이므로 아무 이름이나 사용해도 됩니다.</p>
<p>그다음 Repository access 항목에서 접근 범위를 설정합니다.</p>
<p>Only select repositories</p>
<p>를 선택한 뒤, push할 대상 repository를 선택합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/8bc4c6a3-6d8f-401c-8eea-7a2c911989b1/image.png" alt=""></p>
<p>이번 경우에는 다음 repository를 선택하였습니다.</p>
<p>simple-cpu-gemm-accelerator</p>
<p>이후 Permissions 탭에서 repository 권한을 설정합니다.</p>
<p>Add permissions
<img src="https://velog.velcdn.com/images/young-0320/post/f1c3de45-b2c9-4111-baaf-4609c61c13da/image.png" alt=""></p>
<p>→ Contents 선택
<img src="https://velog.velcdn.com/images/young-0320/post/0c2d9bfb-6e95-4194-a6d5-b188a82d0eda/image.png" alt=""></p>
<p>→ Read-only를 Read and write로 변경</p>
<p>여기서 중요한 점은 Contents 권한을 반드시 Read and write로 설정해야 한다는 것입니다.</p>
<p>Read-only 상태에서는 repository 내용을 읽을 수는 있지만, git push처럼 파일 내용을 변경하는 작업은 수행할 수 없습니다.</p>
<p>Metadata 권한은 기본적으로 Read-only로 표시될 수 있으며, push를 위해 핵심적으로 확인해야 할 권한은 Contents: Read and write입니다.</p>
<p>설정을 마친 뒤 token을 생성하면 github_pat_... 형태의 token 값이 한 번만 표시됩니다.</p>
<p>이 값은 다시 확인할 수 없으므로 바로 복사해 둬야 합니다.</p>
<p>이후 터미널에서 git push를 실행했을 때 username과 password를 물어보면 다음처럼 입력합니다.</p>
<p>Username: GitHub 사용자명
Password: 방금 생성한 fine-grained token</p>
<p>주의할 점은 password 자리에 GitHub 계정 비밀번호를 입력하는 것이 아니라, 방금 생성한 Personal Access Token을 입력해야 한다는 것입니다.</p>
<h2 id="인증정보-저장하기">인증정보 저장하기</h2>
<p><code>git push</code>를 할 때마다 username과 token을 입력하는 것이 번거롭다면 Git credential helper를 사용할 수 있습니다.</p>
<pre><code>git config --global credential.helper store</code></pre><p>해당 명령어를 실행한 뒤 다시 <code>git push</code>를 수행하고 username과 token을 한 번 입력하면, 이후부터는 동일한 인증 정보를 반복해서 입력하지 않아도 됩니다.</p>
<p>이 명령어는 토큰을 <code>.git-credentials</code> 파일에 저장합니다.</p>
<p>저장 위치는 다음과 같습니다.</p>
<pre><code>~/.git-credentials</code></pre><p>다만 해당 방식은 token을 단순 파일에 저장합니다. 따라서 학교 서버처럼 공용성이 있는 환경에서는 보안상 좋은 방식은 아닙니다.</p>
<p>이번 경우에는 편의를 위해 사용했지만, 프로젝트가 끝난 뒤에는 해당 파일을 삭제하거나 GitHub에서 token을 삭제하는 것이 안전합니다.</p>
<p>추가로 <code>.git-credentials</code>처럼 <code>.</code>으로 시작하는 숨김 파일은 <code>Ctrl + H</code>를 누르면 볼 수 있습니다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>이번 글에서는 fine-grained token으로 HTTPS push 인증 문제를 해결하는 방법을 정리하였습니다.</p>
<p>GitHub 인증은 SSH key 방식으로도 처리할 수 있습니다. HTTPS 방식에서는 push할 때 username과 token을 입력해야 하므로, 서버 환경에서 반복적으로 GitHub를 사용할 경우 다소 번거로울 수 있습니다.</p>
<p>이런 경우에는 서버에 SSH key를 생성하고 GitHub 계정에 등록한 뒤, remote URL을 SSH 방식으로 바꾸는 것이 더 편할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RTL log #4 : Oasys RTL synthesis 워크플로우 정리]]></title>
            <link>https://velog.io/@young-0320/RTL-log-4-Oasys-RTL-synthesis-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@young-0320/RTL-log-4-Oasys-RTL-synthesis-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 11 Jun 2026 09:09:09 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 Verilog RTL을 Oasys로 합성하는 과정을 정리해보겠습니다.</p>
<p>Oasys는 Verilog RTL을 standard-cell 기반의 gate-level netlist로 바꾸는 합성 도구입니다.
즉, 우리가 작성한 RTL 코드를 특정 공정 라이브러리에 존재하는 NAND, NOR, DFF 같은 standard cell 조합으로 변환합니다.</p>
<p>이번 작업의 전체 흐름은 다음과 같습니다.</p>
<pre><code class="language-text">1. 합성을 위한 Verilog 파일과 top module을 선택한다.
2. *.f 파일로 Verilog 파일을 관리한다.
3. 합성을 위한 step*_mode*_config.tcl 파일을 작성한다.
4. Oasys 실행 후 config를 open한다.
5. synthesis를 실행한다.
6. synthesized Verilog netlist를 export한다.
7. timing, area, power report를 export한다.</code></pre>
<hr>
<h2 id="1-f-파일-작성">1. <code>.f</code> 파일 작성</h2>
<p>먼저 합성에 필요한 Verilog 파일 목록을 <code>.f</code> 파일로 작성합니다.</p>
<pre><code class="language-text">rtl_v2/gemm_accelerator/gemm_mmio_reg.v
rtl_v2/gemm_accelerator/gemm_controller_fsm.v
rtl_v2/gemm_accelerator/gemm_local_buffer.v
rtl_v2/gemm_accelerator/gemm_lsu.v
rtl_v2/gemm_accelerator/gemm_mac_datapath.v
rtl_v2/gemm_accelerator/gemm_mac_datapath4.v
rtl_v2/gemm_accelerator/gemm_mac_datapath_at.v
rtl_v2/gemm_accelerator/gemm_accelerator_top.v
asic/oasys/step1_gemm_accelerator_top_mode0.v
asic/oasys/step1_gemm_accelerator_top_mode1.v
asic/oasys/step1_gemm_accelerator_top_mode4.v</code></pre>
<p>위 파일에는 합성에 필요한 RTL source file을 한 줄씩 적습니다.</p>
<p>여기서 중요한 점은 repository root 기준의 상대 경로로 적었다는 점입니다.</p>
<p>이렇게 작성하면 나중에 Tcl config에서 <code>REPO_ROOT</code>를 앞에 붙여 절대 경로로 바꿀 수 있습니다.</p>
<p>즉, <code>.f</code> 파일의 역할은 다음과 같습니다.</p>
<pre><code class="language-text">합성 대상 Verilog 파일 목록 관리
config.tcl 내부를 깔끔하게 유지
source file 추가/삭제 시 수정 위치를 단순화</code></pre>
<h2 id="2-oasys-config-tcl-작성">2. Oasys config Tcl 작성</h2>
<p>다음은 Oasys 합성을 위한 config Tcl 파일입니다.</p>
<p>예시는 <code>step1 mode0</code>을 합성하는 설정입니다.
*<em>이때 주석은 코드 뒤에 작성하면 안됩니다. *</em></p>
<pre><code class="language-tcl">namespace eval config {
    global input
    set REPO_ROOT {/mnt/NewHDD/home/ddl2026/ddl2026_2023104135/ddl2026_folder/simple-cpu-gemm-accelerator}

    # =========================================================
    # step1 mode0
    set STEP       {step1}
    set TOP_MODULE {step1_gemm_accelerator_top_mode0}
    # =========================================================

    set FILELIST &quot;$REPO_ROOT/asic/oasys/$STEP.f&quot;
    set fp [open $FILELIST r]
    set input(verilog_files) {}

    foreach line [split [string trim [read $fp]] &quot;\n&quot;] {
        lappend input(verilog_files) &quot;$REPO_ROOT/$line&quot;
    }

    close $fp

    set input(system_verilog)                 {true}
    set input(verilog_dirs)                   [list &quot;$REPO_ROOT/rtl_v2/gemm_accelerator&quot;]
    set input(verilog_defs)                   {}
    set input(top_module)                     $TOP_MODULE

    set input(lib_files)                      {{default {/mnt/NewHDD/home/vlsiadmin/TannerEDA/TannerTools_v2021.2/Process/Generic_250nm/Generic_250nm_LogicGates/Liberty/TANNER_TT_2P50V_25C.lib}}}
    set input(target_library)                 {default}
    set input(lef_files)                      {/mnt/NewHDD/home/vlsiadmin/TannerEDA/TannerTools_v2021.2/Process/Generic_250nm/Generic_250nm_LogicGates/Generic250nm_StdCells.lef}
    set input(tech_file)                      {/mnt/NewHDD/home/vlsiadmin/TannerEDA/TannerTools_v2021.2/Process/Generic_250nm/Generic_250nm_LogicGates/Generic250nm_tech.lef}

    set input(sdc_files)                      [list &quot;$REPO_ROOT/asic/oasys/clk.sdc&quot;]
    set input(def_files)                      {}
    set input(power_files)                    {}
    set input(vcd_file)                       {}
    set input(vcd_scope)                      {}
    set input(sa_probability)                 {}
    set input(sa_togg_perc)                   {}
    set input(sa_togg_rate)                   {}

    set input(clock_gating_minimum_bitwidth)  {4}
    set input(clock_gating_sequential_cell)   {none}
    set input(clock_gating_control_point)     {}
    set input(clock_gating_control_port)      {}
    set input(clock_gating_observation_point) {false}

    set input(comb_vt_target_library)         {default}
    set input(high_vt_target_library)         {default}
    set input(flow_synthesize)                {true}
    set input(synthesize_map_to_scan)         {false}
    set input(synthesize_gate_clock)          {false}
    set input(flow_optimize)                  {true}
    set input(optimize_leakage)               {false}
    set input(optimize_area)                  {false}
    set input(flow_refine)                    {false}
    set input(pre_synthesize)                 {}
    set input(pre_optimize)                   {}
}</code></pre>
<hr>
<h2 id="3-tcl-기본-문법">3. Tcl 기본 문법</h2>
<p>config 파일을 이해하려면 먼저 Tcl 문법 몇 가지를 알아야 합니다.</p>
<h3 id=""><code>{}</code></h3>
<pre><code class="language-tcl">set STEP {step1}</code></pre>
<p><code>{}</code>는 문자열을 그대로 저장할 때 사용합니다.</p>
<p>Tcl에서 <code>{}</code> 안에서는 변수 치환이 일어나지 않습니다.
따라서 경로나 module 이름을 그대로 넣고 싶을 때 사용합니다.</p>
<hr>
<h3 id="-1"><code>&quot;&quot;</code></h3>
<pre><code class="language-tcl">set FILELIST &quot;$REPO_ROOT/asic/oasys/$STEP.f&quot;</code></pre>
<p><code>&quot;&quot;</code> 안에서는 변수 치환이 일어납니다.</p>
<p>즉, <code>$REPO_ROOT</code>와 <code>$STEP</code>이 실제 값으로 바뀝니다.</p>
<p>예를 들어,</p>
<pre><code class="language-tcl">set REPO_ROOT {/home/user/project}
set STEP {step1}</code></pre>
<p>이라면,</p>
<pre><code class="language-tcl">&quot;$REPO_ROOT/asic/oasys/$STEP.f&quot;</code></pre>
<p>는 다음 문자열이 됩니다.</p>
<pre><code class="language-text">/home/user/project/asic/oasys/step1.f</code></pre>
<hr>
<h3 id="변수명"><code>$변수명</code></h3>
<pre><code class="language-tcl">$REPO_ROOT
$STEP
$TOP_MODULE</code></pre>
<p><code>$</code>는 Tcl에서 변수 값을 가져올 때 사용합니다.</p>
<hr>
<h3 id="-2"><code>[]</code></h3>
<pre><code class="language-tcl">set fp [open $FILELIST r]</code></pre>
<p><code>[]</code> 안의 명령어를 먼저 실행하고, 그 결과를 바깥 명령어에 전달합니다.</p>
<p>위 코드는 <code>$FILELIST</code> 경로의 파일을 read mode로 열고, 그 결과인 file pointer를 <code>fp</code>에 저장합니다.</p>
<hr>
<h3 id="list-"><code>[list ...]</code></h3>
<pre><code class="language-tcl">set input(verilog_dirs) [list &quot;$REPO_ROOT/rtl_v2/gemm_accelerator&quot;]</code></pre>
<p><code>list</code>는 Tcl list를 만드는 명령어입니다.</p>
<p>경로에 공백이나 특수 문자가 있어도 하나의 list element로 안전하게 전달할 수 있습니다.</p>
<hr>
<h2 id="4-config-tcl의-기본-구성-요소">4. config Tcl의 기본 구성 요소</h2>
<ol>
<li><p>시스템 베릴로그 사용여부</p>
</li>
<li><p>RTL 경로 설정</p>
</li>
<li><p>라이브러리 경로 설정</p>
</li>
<li><p>sdc 경로 설정</p>
<pre><code class="language-tcl">namespace eval config {
 global input
 # 레포지토리 경로 지정
 set REPO_ROOT {/mnt/NewHDD/home/ddl2026/ddl2026_2023104135/ddl2026_folder/simple-cpu-gemm-accelerator}

 # =========================================================
 # step1 mode0
 set STEP       {step1}
 set TOP_MODULE {step1_gemm_accelerator_top_mode0}
 # =========================================================

 # .f 파일 경로 생성 및 읽기
 set FILELIST &quot;$REPO_ROOT/asic/oasys/$STEP.f&quot;
 set fp [open $FILELIST r]
 set input(verilog_files) {}

 foreach line [split [string trim [read $fp]] &quot;\n&quot;] {
     lappend input(verilog_files) &quot;$REPO_ROOT/$line&quot;
 }

 close $fp
 # 시스템베릴로그 사용 여부
 set input(system_verilog)                 {true}
 # RTL 경로 설정
 set input(verilog_dirs)                   [list &quot;$REPO_ROOT/rtl_v2/gemm_accelerator&quot;]
 set input(verilog_defs)                   {}
 # Top module 지정
 set input(top_module)                     $TOP_MODULE

 # 라이브러리 경로 설정
 set input(lib_files)                      {{default {/mnt/NewHDD/home/vlsiadmin/TannerEDA/TannerTools_v2021.2/Process/Generic_250nm/Generic_250nm_LogicGates/Liberty/TANNER_TT_2P50V_25C.lib}}}
 set input(target_library)                 {default}
 set input(lef_files)                      {/mnt/NewHDD/home/vlsiadmin/TannerEDA/TannerTools_v2021.2/Process/Generic_250nm/Generic_250nm_LogicGates/Generic250nm_StdCells.lef}
 set input(tech_file)                      {/mnt/NewHDD/home/vlsiadmin/TannerEDA/TannerTools_v2021.2/Process/Generic_250nm/Generic_250nm_LogicGates/Generic250nm_tech.lef}
 # Constraint와 power 관련 설정
 set input(sdc_files)                      [list &quot;$REPO_ROOT/asic/oasys/clk.sdc&quot;]
 set input(def_files)                      {}
 set input(power_files)                    {}
 set input(vcd_file)                       {}
 set input(vcd_scope)                      {}
 set input(sa_probability)                 {}
 set input(sa_togg_perc)                   {}
 set input(sa_togg_rate)                   {}
 # Clock gating 관련 설정
 set input(clock_gating_minimum_bitwidth)  {4}
 set input(clock_gating_sequential_cell)   {none}
 set input(clock_gating_control_point)     {}
 set input(clock_gating_control_port)      {}
 set input(clock_gating_observation_point) {false}

 set input(comb_vt_target_library)         {default}
 set input(high_vt_target_library)         {default}
 set input(flow_synthesize)                {true}
 set input(synthesize_map_to_scan)         {false}
 set input(synthesize_gate_clock)          {false}
 set input(flow_optimize)                  {true}
 set input(optimize_leakage)               {false}
 set input(optimize_area)                  {false}
 set input(flow_refine)                    {false}
 set input(pre_synthesize)                 {}
 set input(pre_optimize)                   {}
}</code></pre>
</li>
</ol>
<h2 id="5-constraint와-power-관련-설정">5. Constraint와 power 관련 설정</h2>
<h3 id="sdc-file-지정">SDC file 지정</h3>
<p><code>.sdc</code> 파일은 timing constraint를 정의하는 파일입니다.</p>
<p>대표적으로 clock period를 설정합니다.</p>
<p>예를 들어 <code>clk.sdc</code>에는 다음과 같은 내용이 들어갈 수 있습니다.</p>
<pre><code class="language-tcl">create_clock -name clk -period 10 [get_ports clk]</code></pre>
<p>이 constraint가 있어야 Oasys가 어느 정도 속도를 목표로 합성해야 하는지 판단할 수 있습니다.</p>
<hr>
<h3 id="def-file">DEF file</h3>
<pre><code class="language-tcl">set input(def_files) {}</code></pre>
<p><code>.def</code> 파일은 placement, routing, pin location 같은 physical design 정보를 담습니다.</p>
<p>현재 단계는 RTL synthesis이므로 별도 DEF를 사용하지 않습니다.</p>
<hr>
<h3 id="power-file">Power file</h3>
<pre><code class="language-tcl">set input(power_files) {}</code></pre>
<p>전력 분석용 추가 파일을 지정하는 항목입니다.</p>
<p>현재는 사용하지 않으므로 빈 값으로 둡니다.</p>
<hr>
<h3 id="vcd-file">VCD file</h3>
<pre><code class="language-tcl">set input(vcd_file) {}
set input(vcd_scope) {}</code></pre>
<p><code>.vcd</code> 파일은 simulation waveform에서 나온 switching activity 정보를 담습니다.</p>
<p>VCD를 넣으면 실제 시뮬레이션에서 어떤 신호가 얼마나 toggle되었는지를 기반으로 power estimation을 할 수 있습니다.</p>
<p>현재는 비워두었기 때문에 power report는 실제 workload 기반이라기보다는 tool의 기본 activity assumption에 가까운 결과입니다.</p>
<p>정확한 전력 비교를 하려면 simulation에서 VCD를 생성하고, 그 VCD를 Oasys power analysis에 넣는 것이 좋습니다.</p>
<hr>
<h3 id="static-activity-설정">Static activity 설정</h3>
<pre><code class="language-tcl">set input(sa_probability) {}
set input(sa_togg_perc) {}
set input(sa_togg_rate) {}</code></pre>
<p>VCD 없이 switching activity를 수동으로 지정할 때 사용하는 항목입니다.</p>
<p>각 항목의 의미는 다음과 같습니다.</p>
<pre><code class="language-text">sa_probability : signal이 1일 확률
sa_togg_perc   : toggle percentage
sa_togg_rate   : toggle rate</code></pre>
<p>현재는 별도 activity 정보를 주지 않았습니다.</p>
<hr>
<h2 id="6-clock-gating-관련-설정">6. Clock gating 관련 설정</h2>
<pre><code class="language-tcl">set input(clock_gating_minimum_bitwidth)  {4}
set input(clock_gating_sequential_cell)   {none}
set input(clock_gating_control_point)     {}
set input(clock_gating_control_port)      {}
set input(clock_gating_observation_point) {false}</code></pre>
<p>clock gating은 불필요한 clock switching을 줄여 dynamic power를 감소시키는 기법입니다.</p>
<p>하지만 현재 설정에서는 clock gating을 적극적으로 사용하지 않습니다.</p>
<p>특히 다음 줄이 중요합니다.</p>
<pre><code class="language-tcl">set input(clock_gating_sequential_cell) {none}</code></pre>
<p>clock gating용 sequential cell을 지정하지 않았다는 의미입니다.</p>
<p>또한 아래 설정에서도 gated clock 합성을 꺼두었습니다.</p>
<pre><code class="language-tcl">set input(synthesize_gate_clock) {false}</code></pre>
<p>따라서 현재 config는 clock gating 최적화보다는 기본 합성과 기본 optimization에 초점을 둔 설정입니다.</p>
<h2 id="7-oasys-실행">7. Oasys 실행</h2>
<p>config 작성이 끝나면 Oasys를 실행합니다.</p>
<p>터미널에서 다음 명령어를 입력합니다.</p>
<pre><code class="language-bash">oasys -gui</code></pre>
<p>이후 Oasys GUI에서 미리 작성한 config file을 엽니다.</p>
<pre><code class="language-text">Design → Open Config → 작성한 .tcl 파일 선택</code></pre>
<p><img src="https://velog.velcdn.com/images/young-0320/post/950d0259-6a7e-4b1a-b476-1244130a1ebf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/22831074-6db1-41e1-a9a7-0216364c65ca/image.png" alt=""></p>
<p>config가 정상적으로 열리면 synthesis를 실행합니다.</p>
<hr>
<h2 id="8-synthesized-verilog-netlist-export">8. Synthesized Verilog netlist export</h2>
<p>합성이 끝나면 synthesized Verilog netlist를 export합니다.</p>
<p>Oasys command 창에 다음 명령어를 입력합니다.</p>
<pre><code class="language-tcl">write_verilog &quot;step1_mode0_synth.v&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/young-0320/post/cf72a4f7-764d-4c43-bddc-ad129398aa18/image.png" alt=""></p>
<p>여기서 <code>step1_mode0_synth.v</code>는 예시 파일명입니다.</p>
<p>다른 mode를 합성했다면 다음처럼 이름을 바꿔 저장합니다.</p>
<pre><code class="language-tcl">write_verilog &quot;step1_mode1_synth.v&quot;
write_verilog &quot;step1_mode4_synth.v&quot;</code></pre>
<p>저장 경로를 따로 지정하지 않으면 Oasys를 실행한 현재 작업 디렉토리에 파일이 저장됩니다.
(pwd 입력했을 때 출력되는 경로가 현재 작업 디렉토리입니다.)</p>
<p>원하는 폴더에 저장하고 싶다면 경로까지 명시합니다.</p>
<pre><code class="language-tcl">write_verilog &quot;/mnt/NewHDD/home/ddl2026/ddl2026_2023104135/ddl2026_folder/simple-cpu-gemm-accelerator/asic/oasys/results/step1_mode0_synth.v&quot;</code></pre>
<p>합성 후에는 library에 존재하는 cell instance 형태로 바뀝니다.</p>
<pre><code class="language-verilog">AND2X1 U123 (
    .A(a),
    .B(b),
    .Y(y)
);</code></pre>
<h2 id="9-timing-area-power-report-export">9. Timing, area, power report export</h2>
<p>합성 결과를 확인하기 위해 timing, area, power report를 export합니다.</p>
<p>Oasys command 창에서 다음 명령어를 입력합니다.</p>
<pre><code class="language-tcl">report_timing &gt; &quot;step1_mode0_timing.rpt&quot;
report_area   &gt; &quot;step1_mode0_area.rpt&quot;
report_power  &gt; &quot;step1_mode0_power.rpt&quot;</code></pre>
<p>다른 mode에 대해서는 파일 이름만 바꿔 저장합니다.</p>
<pre><code class="language-tcl">report_timing &gt; &quot;step1_mode1_timing.rpt&quot;
report_area   &gt; &quot;step1_mode1_area.rpt&quot;
report_power  &gt; &quot;step1_mode1_power.rpt&quot;</code></pre>
<pre><code class="language-tcl">report_timing &gt; &quot;step1_mode4_timing.rpt&quot;
report_area   &gt; &quot;step1_mode4_area.rpt&quot;
report_power  &gt; &quot;step1_mode4_power.rpt&quot;</code></pre>
<p>이 report 파일들도 저장 경로를 따로 지정하지 않으면 현재 Oasys 실행 디렉토리에 저장됩니다.</p>
<p>원하는 폴더에 저장하고 싶다면 다음처럼 경로를 명시합니다.</p>
<pre><code class="language-tcl">report_timing &gt; &quot;$REPO_ROOT/asic/oasys/results/step1_mode0_timing.rpt&quot;
report_area   &gt; &quot;$REPO_ROOT/asic/oasys/results/step1_mode0_area.rpt&quot;
report_power  &gt; &quot;$REPO_ROOT/asic/oasys/results/step1_mode0_power.rpt&quot;</code></pre>
<h2 id="10-각-report의-의미">10. 각 report의 의미</h2>
<h3 id="timing-report">Timing report</h3>
<p>Timing report는 설계가 주어진 clock constraint를 만족하는지 확인하는 report입니다.</p>
<p>가장 중요하게 봐야 할 값은 slack입니다.</p>
<pre><code class="language-text">slack &gt; 0  : timing 만족
slack &lt; 0  : timing violation</code></pre>
<p>즉, slack이 음수라면 현재 clock period 안에 신호가 도착하지 못한다는 의미입니다.</p>
<p>이 경우 datapath를 줄이거나, pipeline을 추가하거나, clock constraint를 완화해야 합니다.</p>
<hr>
<h3 id="area-report">Area report</h3>
<p>Area report는 합성된 standard cell의 총 면적을 보여줍니다.</p>
<p>GEMM accelerator처럼 datapath 구조가 중요한 설계에서는 area report를 통해 구조별 cost를 비교할 수 있습니다.</p>
<p>예를 들어 다음과 같은 비교가 가능합니다.</p>
<pre><code class="language-text">mode0 vs mode1 vs mode4
single MAC vs 4-lane MAC
buffer 추가 전후
control logic 면적
datapath 면적</code></pre>
<p>단순히 기능이 맞는지만 보는 것이 아니라, 같은 기능을 구현하는 데 얼마나 많은 cell area가 필요한지도 확인해야 합니다.</p>
<hr>
<h3 id="power-report">Power report</h3>
<p>Power report는 설계의 전력 소모를 보여줍니다.</p>
<p>일반적으로 다음 항목을 확인합니다.</p>
<pre><code class="language-text">dynamic power
leakage power
total power</code></pre>
<p>다만 현재 config에서는 VCD 파일을 넣지 않았습니다.</p>
<p>따라서 power report는 실제 GEMM workload 기반의 정확한 전력이라기보다는, tool의 기본 switching activity 추정에 기반한 결과입니다.</p>
<p>더 정확한 전력 비교를 하려면 simulation에서 VCD를 생성하고, 그 VCD를 Oasys에 넣어 power analysis를 수행해야 합니다.</p>
<hr>
<h2 id="11-정리">11. 정리</h2>
<p>이번 Oasys workflow의 핵심은 다음과 같습니다.</p>
<pre><code class="language-text">.f 파일
→ 합성할 RTL source 목록 관리

config.tcl
→ source file, top module, library, constraint, optimization option 지정

.lib
→ standard cell의 timing / power / logic 정보

.lef
→ standard cell의 physical information

tech.lef
→ 공정 routing layer 및 technology rule

.sdc
→ clock period 등 timing constraint

.vcd
→ 실제 simulation switching activity 기반 power estimation</code></pre>
<p>Oasys 합성은 단순히 Verilog를 gate-level netlist로 바꾸는 작업이 아닙니다.</p>
<p>RTL이 특정 공정 library와 timing constraint 아래에서 어떤 hardware structure로 구현되는지 확인하는 과정입니다.</p>
<p>따라서 synthesized Verilog netlist만 저장하는 것이 아니라, timing, area, power report까지 함께 저장해야 설계 비교가 가능합니다.</p>
<p>특히 GEMM accelerator처럼 datapath 구조가 성능과 면적에 직접 영향을 주는 설계에서는 mode별 합성 결과를 비교하는 것이 중요합니다.</p>
<pre><code class="language-text">mode0 : baseline 구조
mode1 : 개선 구조
mode4 : 4-lane datapath 구조</code></pre>
<p>같은 constraint와 같은 library 조건에서 각 mode를 합성하면, RTL 구조 변경이 실제 hardware cost로 어떻게 나타나는지 확인할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Zynq FIR IP 개발 #1: Block Design부터 비트스트림·XSA까지]]]></title>
            <link>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-1</link>
            <guid>https://velog.io/@young-0320/Zynq-FIR-IP-%EA%B0%9C%EB%B0%9C-1</guid>
            <pubDate>Wed, 06 May 2026 04:43:33 GMT</pubDate>
            <description><![CDATA[<h1 id="zynq-fir-ip의-구조-이해">Zynq FIR IP의 구조 이해</h1>
<p>개발 환경: Vivado 2024.2 / Zybo Z7-20 (xc7z020clg400-1) / Ubuntu
대상 IP: fir_decimator_n43_axis (N=43 transposed FIR + M=2 데시메이터, 16-bit AXI-Stream)
입력 8192 샘플 → 출력 4096 샘플</p>
<p>시뮬레이션 골든은 4117샘플이다. 테스트벤치는 마지막 입력 뒤에도 0을 밀어 넣어 파이프라인에 남은 42샘플까지 전부 flush하므로 FIR 출력이 8192+43−1 = 8234, 여기서 M=2 데시메이션하면 4117이 된다. 보드는 DMA가 8192샘플만 밀어 넣고 끝내므로 테일이 나오지 않고 정확히 8192/2 = 4096이다. 이 글부터는 보드 기준 4096을 쓴다.</p>
<pre><code>  ① PS(C코드): 멀티톤 신호 합성
         ↓
  ② PS: DDR에 입력 배열 저장
         ↓
  ③ PS: DMA에 전송 명령 (시작 주소, 길이)
         ↓
  ④ DMA MM2S: DDR → AXI-Stream → FIR S_AXIS (입력)
         ↓
  ⑤ FIR IP: 필터링 + 데시메이션 (M=2)
         ↓
  ⑥ DMA S2MM: FIR M_AXIS → AXI-Stream → DDR (출력 배열 저장)
         ↓
  ⑦ PS(C코드): DDR에서 결과 읽기 → UART 출력</code></pre><p>③~⑥ 구간에서 PS는 명령만 내리고, 실제 데이터 이동은 DMA가 CPU 개입 없이 처리한다. 이것이 DMA를 쓰는 이유다.</p>
<p>DMA는 채널이 2개</p>
<ul>
<li><strong>MM2S</strong> (Memory-Mapped to Stream): DDR → FIR 방향</li>
<li><strong>S2MM</strong> (Stream to Memory-Mapped): FIR → DDR 방향</li>
</ul>
<h2 id="생성하려는-block-design">생성하려는 block design</h2>
<p><strong>bd_fir_dma</strong>: custom fir ip와 PS ip, DMA ip를 붙인 block design</p>
<pre><code>Block Design: bd_fir_dma
├── ZYNQ7 Processing System   ← IP 1
├── AXI DMA                   ← IP 2
└── fir_decimator_n43_axis    ← IP 3 (우리 것)</code></pre><table>
<thead>
<tr>
<th>#</th>
<th>IP 이름</th>
<th>출처</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><strong>ZYNQ7 Processing System</strong></td>
<td>Xilinx 기본 제공</td>
<td>ARM 코어 + DDR + HP0 포트</td>
</tr>
<tr>
<td>2</td>
<td><strong>AXI Direct Memory Access</strong></td>
<td>Xilinx 기본 제공</td>
<td>DDR ↔ AXI-Stream 브리지</td>
</tr>
<tr>
<td>3</td>
<td><strong>fir_decimator_n43_axis</strong></td>
<td>우리가 만든 RTL</td>
<td>FIR 필터 + 데시메이터</td>
</tr>
</tbody></table>
<p>Vivado가 &quot;Run Connection Automation&quot; 시 <strong>자동으로 추가</strong>하는 IP 3개:</p>
<table>
<thead>
<tr>
<th>#</th>
<th>IP 이름</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>4</td>
<td><strong>Processor System Reset</strong></td>
<td>PS 리셋 신호 동기화</td>
</tr>
<tr>
<td>5</td>
<td><strong>AXI SmartConnect</strong></td>
<td>PS GP0 → DMA AXI-Lite 연결 중계</td>
</tr>
<tr>
<td>6</td>
<td><strong>AXI Interconnect</strong></td>
<td>DMA MM2S/S2MM → PS HP0 메모리 경로 병합</td>
</tr>
</tbody></table>
<h3 id="큰-그림-1-block-design-생성">큰 그림 1: Block Design 생성</h3>
<p>GUI에서 IP들을 배치하고 연결하는 것:</p>
<ul>
<li><strong>Zynq PS7 IP</strong> — ARM 코어, HP0 포트 활성화</li>
<li><strong>AXI DMA IP</strong>  — DDR ↔ AXI-Stream 브리지</li>
<li><strong>Custom FIR IP</strong> (<code>fir_decimator_n43_axis</code>) — Module Reference로 삽입</li>
</ul>
<p>연결:</p>
<pre><code>AXI DMA MM2S → (AXI-Stream) → FIR S_AXIS (입력)
AXI DMA S2MM ← (AXI-Stream) ← FIR M_AXIS (출력)
AXI DMA ↔ PS7 HP0 (DDR 직접 접근)</code></pre><h3 id="큰-그림-2-비트스트림-생성--xsa-내보내기">큰 그림 2: 비트스트림 생성 + XSA 내보내기</h3>
<p>Block Design → Synthesize → Implement → Generate Bitstream → Export Hardware (.xsa)</p>
<h1 id="vivado-gui로-block-design-완성하기">Vivado GUI로 Block Design 완성하기</h1>
<p><strong>진행 순서</strong></p>
<ol>
<li>새 프로젝트 생성 (Board: Zybo Z7-20)</li>
<li>Create Block Design</li>
<li>IP 추가: ZYNQ7 Processing System</li>
<li>IP 추가: AXI DMA</li>
<li>IP 추가: 우리 FIR (Module Reference)</li>
<li>Create HDL Wrapper</li>
<li>Generate Bitstream</li>
<li>Export Hardware</li>
</ol>
<h2 id="1-새-프로젝트-생성">1. 새 프로젝트 생성</h2>
<ol>
<li><code>Create Project</code> 클릭</li>
<li>Project name: <code>fir_decimator_trans_n43</code></li>
<li>Project location: <code>원하는 경로 설정</code>
<img src="https://velog.velcdn.com/images/young-0320/post/f3060ef3-f9ea-49e9-8a95-7b64c9d97bbc/image.png" alt=""></li>
<li>Project type: <strong>RTL Project</strong>
Sources 추가할 때 RTL 파일들 넣기:<ul>
<li><code>rtl/transposed_form/n43/fir_decimator_n43_axis.v</code> ← 최상위 top</li>
<li><code>rtl/transposed_form/decimator_m2_phase0.v</code></li>
<li><code>rtl/transposed_form/n43/fir_decimator_n43.v</code></li>
<li><code>rtl/transposed_form/n43/fir_n43.v</code>
<img src="https://velog.velcdn.com/images/young-0320/post/93723260-6fd7-4af4-a460-1679a96df54f/image.png" alt=""></li>
</ul>
</li>
<li>constraint file은 <strong>포함하지 말기!</strong> 나는 실수로 포함시킴
<img src="https://velog.velcdn.com/images/young-0320/post/0770b621-7540-4aef-96be-cc92bfe06ce1/image.png" alt=""></li>
<li>Default Part에서 Board 탭 → <code>Zybo Z7-20</code> 선택
<img src="https://velog.velcdn.com/images/young-0320/post/68c4bf8f-c22c-4304-a718-2503ebedfa75/image.png" alt=""></li>
<li>finish
<img src="https://velog.velcdn.com/images/young-0320/post/55e97e89-cc35-4713-81cf-bc3b90ee64be/image.png" alt=""></li>
</ol>
<h2 id="2-create-block-design">2. Create Block Design</h2>
<p>Vivado 좌측 Flow Navigator에서:</p>
<ol>
<li><code>IP INTEGRATOR</code> → <code>Create Block Design</code> 클릭</li>
<li>Design name: <code>bd_fir_dma</code>
<img src="https://velog.velcdn.com/images/young-0320/post/7042f95e-34bf-4b07-b58c-2d5c70508d0a/image.png" alt=""></li>
</ol>
<h2 id="3-ip-추가-zynq7-processing-system">3. IP 추가: ZYNQ7 Processing System</h2>
<ol>
<li>캔버스 우클릭 → <code>Add IP</code> → 검색창에 <code>zynq</code> 입력 → <strong>ZYNQ7 Processing System</strong> 더블클릭
<img src="https://velog.velcdn.com/images/young-0320/post/d7636f1e-520b-4021-86d9-569fc338120e/image.png" alt="">
<img src="https://velog.velcdn.com/images/young-0320/post/e9bb2126-57c7-46c7-9d24-c6f1a74e378f/image.png" alt=""></li>
<li>PS 설정하기
블록 더블클릭 이후 수정할 두 가지 항목 확인</li>
</ol>
<p><strong>1. 포트 활성화</strong>
<code>PS-PL Configuration</code> → <code>HP Slave AXI Interface</code> → <code>S AXI HP0 Interface</code> 체크</p>
<ul>
<li><strong>HP0</strong>: PL(DMA)이 PS(CPU)의 DDR에 접근하는 유일한 경로, 기본 비활성화이므로 활성화 해주기</li>
<li>GP 포트는 PS→PL 방향 마스터 포트라서 이 용도로 쓸 수 없다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/young-0320/post/50559e9a-9201-4721-8c42-c91ab31cce7d/image.png" alt=""></p>
<p><strong>2. 클럭 확인</strong>
<code>Clock Configuration</code> → <code>PL Fabric Clocks</code> → <code>FCLK_CLK0</code> 가 100MHz인지 확인
<img src="https://velog.velcdn.com/images/young-0320/post/b491bff6-1bca-4c31-b111-b14b24adca42/image.png" alt=""></p>
<h2 id="4-ip-추가-axi-dma">4. IP 추가: AXI DMA</h2>
<ol>
<li>캔버스 우클릭 → <code>Add IP</code> → 검색창에 <code>axi dma</code> 입력 → <strong>AXI Direct Memory Access</strong> 더블클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/young-0320/post/0d9599ae-4214-4d0c-9b90-e26a139232c2/image.png" alt=""></p>
<ol start="2">
<li>DMA 설정하기</li>
</ol>
<p>블록 더블클릭 이후 확인/수정할 항목 확인
<img src="https://velog.velcdn.com/images/young-0320/post/b93257b9-3ff6-4301-8062-187f408b531e/image.png" alt=""></p>
<p><strong>상단</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Enable Scatter Gather Engine</td>
<td>해제 ✓</td>
<td>Simple DMA 모드. 연속 배열 하나를 옮기는 구조라 시작 주소와 길이만 넘기는 Simple DMA로 충분</td>
</tr>
<tr>
<td>Enable Micro DMA</td>
<td>해제</td>
<td>기능 제한된 경량 DMA. 해당 없음</td>
</tr>
<tr>
<td>Enable Multi Channel Support</td>
<td>비활성</td>
<td>Scatter Gather 전용. 해당 없음</td>
</tr>
<tr>
<td>Enable Control/Status Stream</td>
<td>비활성</td>
<td>Scatter Gather 전용. 해당 없음</td>
</tr>
<tr>
<td>Width of Buffer Length Register</td>
<td>14 bits</td>
<td>최대 전송 크기 2¹⁴ -1 = 16383 bytes. 출력 4117샘플 × 2bytes ≈ 8KB이므로 충분</td>
</tr>
<tr>
<td>Address Width</td>
<td>32 bits</td>
<td>Zynq-7000은 32비트 주소 체계. 맞음</td>
</tr>
</tbody></table>
<p>⚠️ 이 판단이 나중에 MM2S timeout의 원인이 된다. 여기서 내가 계산에 쓴 건 <strong>출력</strong> 쪽 길이였다. 그런데 이 설정은 채널별이 아니라 MM2S와 S2MM에 함께 적용되는 파라미터다. 실제 제약은 둘 중 큰 쪽이 건다.</p>
<pre><code>S2MM (출력) : 4096샘플 × 2B =  8192 bytes  →  16383 이내, 문제 없음
MM2S (입력) : 8192샘플 × 2B = 16384 bytes  →  16383을 정확히 1 byte 초과</code></pre><p>FIR는 M=2 데시메이터라 입력이 출력의 두 배인데, 하필 여유를 확인한 쪽이 작은 출력이었다.
이 시점의 나는 몰랐고, 보드를 올린 뒤에야 알게 된다. 상세는 3편.</p>
<hr>
<p><strong>Read Channel (MM2S) — DDR → FIR 방향</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Number of Channels</td>
<td>1</td>
<td>채널 하나면 충분</td>
</tr>
<tr>
<td>Memory Map Data Width</td>
<td>32</td>
<td>DDR(HP0) 쪽 버스 폭. 32비트 표준</td>
</tr>
<tr>
<td>Stream Data Width</td>
<td><strong>16</strong> ✓</td>
<td>FIR s_axis_tdata 폭과 일치</td>
</tr>
<tr>
<td>Max Burst Size</td>
<td>16</td>
<td>DDR 읽기 시 한 번에 16 beat. 기본값으로 충분</td>
</tr>
<tr>
<td>Allow Unaligned Transfers</td>
<td>해제</td>
<td>16비트 샘플은 정렬 보장됨</td>
</tr>
</tbody></table>
<hr>
<p><strong>Write Channel (S2MM) — FIR → DDR 방향</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Number of Channels</td>
<td>1</td>
<td>채널 하나면 충분</td>
</tr>
<tr>
<td>Memory Map Data Width</td>
<td>32 (AUTO)</td>
<td>DDR 쪽 버스 폭. 동일</td>
</tr>
<tr>
<td>Stream Data Width</td>
<td><strong>16 (MANUAL)</strong> ✓</td>
<td>FIR m_axis_tdata 폭과 일치</td>
</tr>
<tr>
<td>Max Burst Size</td>
<td>16</td>
<td>DDR 쓰기 시 한 번에 16 beat</td>
</tr>
<tr>
<td>Allow Unaligned Transfers</td>
<td>해제</td>
<td>정렬 보장됨</td>
</tr>
</tbody></table>
<hr>
<p><strong>하단</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Enable Single AXI4 Data Interface</td>
<td>AUTO/해제</td>
<td>MM2S와 S2MM이 DDR 포트를 각각 따로 씀. 동시 읽기+쓰기 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-ip-추가-custom-fir">5. IP 추가: Custom FIR</h2>
<p>커스텀 RTL을 BD에 넣는 방법은 IP Packaging과 Module Reference 두 가지다. IP Packaging 방식은 재사용할 때 이득이 있다. 해당 FIR IP는 이 Block Design 하나에서만 쓰이고 AXI-Stream 인터페이스도 이미 RTL에 있어서, Module Reference 방식을 사용했다.</p>
<ol>
<li>캔버스 우클릭 → <code>Add Module</code> → <code>fir_decimator_n43_axis</code> 선택 
<img src="https://velog.velcdn.com/images/young-0320/post/5452623f-56f1-4505-937b-57581035e509/image.png" alt=""></li>
</ol>
<ol start="2">
<li><p>캔버스 상단 초록색 배너 <strong>&quot;Run Connection Automation&quot;</strong> 클릭 
<img src="https://velog.velcdn.com/images/young-0320/post/007a9bc4-1981-41f2-abb3-d4285e8754d4/image.png" alt=""></p>
</li>
<li><p>모든 항목 체크 : Vivado가 SmartConnect, Processor System Reset, AXI Interconnect를 자동 생성하고 연결한다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/young-0320/post/0655d2a5-e315-4d6d-8421-d1fa89281707/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/c81b3156-2a1e-40ae-b097-4ebac6bace7a/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>연결되는 경로</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>axi_dma_0</code> → S_AXI_LITE</td>
<td>PS GP0 → SmartConnect → DMA S_AXI_LITE</td>
<td>PS가 DMA 레지스터에 명령을 쓰는 제어 경로</td>
</tr>
<tr>
<td><code>processing_system7_0</code> → S_AXI_HP0</td>
<td>DMA M_AXI_MM2S/S2MM → PS HP0</td>
<td>DMA가 DDR을 읽고 쓰는 데이터 경로</td>
</tr>
</tbody></table>
<p>FIR의 AXI-Stream 포트(<code>s_axis</code>, <code>m_axis</code>)는 이 목록에 나오지 않는다. Vivado가 DMA의 어느 채널을 FIR의 어느 포트에 연결할지 판단하지 못하기 때문이다. 아래의 7번에서 수동 연결한다.</p>
<h3 id="⚠️-connection-automation이-dma-클럭을-다-연결해주지는-않는다">⚠️ Connection Automation이 DMA 클럭을 다 연결해주지는 않는다.</h3>
<p>Validate Design에서 이런 에러가 났다.</p>
<p> <code>[BD 41-758] /axi_dma_0/m_axi_s2mm_aclk is not connected to a valid clock source</code></p>
<p>DMA의 클럭 포트는 <code>s_axi_lite_aclk</code>, <code>m_axi_mm2s_aclk</code>, <code>m_axi_s2mm_aclk</code> 세 개인데, Run Connection Automation이 이걸 전부 연결해주지는 않는다. 셋 다 연결돼 있는지 확인하고, 빠진 것은 PS의 <code>FCLK_CLK0</code>에 수동으로 연결한다.</p>
<h2 id="6-자동-생성된-ipaxi-interconnect-확인">6. 자동 생성된 IP:AXI Interconnect 확인</h2>
<p><strong>&quot;Run Connection Automation&quot;</strong>으로 자동 생성된 <strong>axi_mem_intercon(AXI Interconnect)</strong> 더블클릭</p>
<ul>
<li>Number of Slave Interfaces → 2</li>
</ul>
<p>DMA는 DDR 쪽으로 M_AXI_MM2S(읽기)와 M_AXI_S2MM(쓰기) 두 개의 마스터 포트를 낸다.
이 둘이 PS의 HP0 하나로 들어가야 하므로 Interconnect의 slave interface가 2개 필요하다.
MM2S/S2MM을 한 번에 자동 연결했다면 Vivado가 이미 2로 잡아준다.
<img src="https://velog.velcdn.com/images/young-0320/post/ef36386d-6147-4cd9-9d8f-0be05838e72b/image.png" alt=""></p>
<h2 id="7-fir-모듈-수동-연결">7. FIR 모듈 수동 연결</h2>
<p><strong>포트 연결 요약:</strong></p>
<pre><code>PS GP0 → SmartConnect → DMA S_AXI_LITE     (PS가 DMA 제어)
DMA M_AXI_MM2S → PS HP0                    (DMA가 DDR 읽기)
DMA M_AXI_S2MM → PS HP0                    (DMA가 DDR 쓰기)
DMA M_AXIS_MM2S → FIR S_AXIS               (DDR → FIR)
FIR M_AXIS → DMA S_AXIS_S2MM               (FIR → DDR)</code></pre><h3 id="배선-전에-알아야-할-두-가지">배선 전에 알아야 할 두 가지</h3>
<p><strong>1. AXI-Stream은 번들을 통째로 연결한다.</strong>
BD에서 AXI-Stream 포트는 wire 묶음이 아니라 인터페이스로 관리된다. 펼쳐서 <code>tdata</code>/<code>tvalid</code>/<code>tready</code>/<code>tlast</code>를 하나씩 연결하면 Vivado가 AXI-Stream 프로토콜 연결로 인식하지 못하고, DMA도 <code>S_AXIS_S2MM</code> 인터페이스 단위로만 인식해서 핸드셰이크가 보장되지 않는다.</p>
<p><strong>2. 빈 캔버스에 drop하면 외부 포트가 생긴다.</strong>
대상 포트 위가 아닌 곳에 놓으면 Vivado가 Make External을 실행해 FPGA 핀으로 노출시키고, 비트스트림 단계에서 <code>[DRC NSTD-1] 39 out of 39 logical ports use I/O standard &#39;DEFAULT&#39;</code>로 터진다. <code>get_bd_ports *</code>로 확인·삭제하고 초록 체크가 뜰 때 놓아 재연결한다.</p>
<p><strong>첫 번째: aclk 연결</strong></p>
<p>FIR 블록의 <code>aclk</code> 포트 → PS 블록의 <code>FCLK_CLK0</code> 포트로 드래그
<img src="https://velog.velcdn.com/images/young-0320/post/cc2a9e22-bcba-4a71-9209-c04d69a00318/image.png" alt=""></p>
<p><strong>두 번째: aresetn 연결</strong></p>
<p>FIR 블록의 <code>aresetn</code> 포트 → <code>rst_ps7_0_100M</code> 블록의 <code>peripheral_aresetn</code> 포트로 드래그
<img src="https://velog.velcdn.com/images/young-0320/post/b6880fa9-7092-44b9-9655-1e48bbeb9c2f/image.png" alt=""></p>
<p><code>aresetn</code> : FIR IP의 active-low 리셋 포트</p>
<p><strong>세 번째: DMA → FIR 입력 연결</strong></p>
<p><code>axi_dma_0</code> 블록의 <code>M_AXIS_MM2S</code> 포트 → FIR 블록의 <code>s_axis</code> 포트로 드래그
<img src="https://velog.velcdn.com/images/young-0320/post/a4fb6562-783a-4222-8fa1-afd5fc1df810/image.png" alt=""></p>
<p><strong>네 번째: FIR → DMA 출력 연결</strong></p>
<p>FIR 블록의 <code>m_axis</code> 포트 → <code>axi_dma_0</code> 블록의 <code>S_AXIS_S2MM</code> 포트로 드래그</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/799c8738-81a4-4abf-9239-4137d52bffc6/image.png" alt=""></p>
<p><strong>다섯 번째: Run Block Automation 클릭</strong>
상단 녹색 배너의 <code>Run Block Automation</code> 클릭</p>
<p>※ Block Automation은 PS7 IP를 추가한 직후(3번)에 실행하는 게 일반적이다. DDR / FIXED_IO를 외부 포트로 빼주는 단계라 언제 눌러도 결과는 같지만, 따라 하는 입장이라면 PS 추가 직후에 눌러도 된다. 나는 배선을 다 한 뒤에 눌렀다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/97e9121d-cfd5-4fab-95da-347c8f8d975d/image.png" alt="">
<img src="https://velog.velcdn.com/images/young-0320/post/cd448049-0671-4716-9415-05374322e46d/image.png" alt=""></p>
<p><strong>여섯 번째: 검증하기</strong>
단축키 F6를 눌러 Validate Design 진행
<img src="https://velog.velcdn.com/images/young-0320/post/45f11d66-6cf1-4ec8-b68f-dc32d8bde602/image.png" alt="">
Digilent 보드 파일을 쓰는 Zybo 설계에서는 흔히 뜨는 경고라고 한다.</p>
<p>다만 넘기기 전에 DDR 파트는 확인해야 한다. 이 시점의 BD에는 PS7 DDR 파트가 Zybo Z7-20이 아니라 Z7-10용 파트로 들어가 있었다. 보드 파일이 제대로 적용되지 않은 채로 BD가 만들어졌기 때문이고, 그대로 아래 11번에서 TCL로 추출까지 됐다. <code>ps7_init</code>은 DDR 파트를 기준으로 CAS latency, tRCD, tRP 같은 타이밍 레지스터를 설정하므로, 파트가 틀리면 DDR이 불안정해지고 DMA 전송 실패로 이어질 수 있다.</p>
<pre><code>get_property CONFIG.PCW_UIPARAM_DDR_PARTNO [get_bd_cells /processing_system7_0]
# MT41K256M16 RE-125 → 정상 (Z7-20)
# MT41J128M8 JP-125  → 잘못됨 (Z7-10). BD 상단 녹색 배너에서 보드 파일 재적용</code></pre><h2 id="8-create-hdl-wrapper">8. Create HDL Wrapper</h2>
<p>Sources 탭에서 Block Design 파일(<code>bd_fir_dma</code>) 우클릭 → <code>Create HDL Wrapper</code> → <code>Let Vivado manage wrapper and auto-update</code> 선택
<img src="https://velog.velcdn.com/images/young-0320/post/ee1e0863-facb-4854-bb29-03275eddcde7/image.png" alt=""></p>
<h2 id="9-generate-bitstream">9. Generate Bitstream</h2>
<p><code>Synthesis</code> → <code>Implementation</code> → <code>Generate Bitstream</code></p>
<h3 id="error-1-비트스트림-생성-실패">error 1: 비트스트림 생성 실패</h3>
<p>로그 첫 줄: <code>synth_design -top fir_decimator_n43_axis</code></p>
<h3 id="error-1-해결">error 1 해결</h3>
<p>두 가지 문제가 동시에 존재했다.</p>
<p><strong>문제 1: 합성 Top 모듈 오설정</strong></p>
<p>합성 Top 모듈이 <code>bd_fir_dma_wrapper</code>가 아닌 <code>fir_decimator_n43_axis</code>로 설정되어 있었다. 
이는 Block Design 생성 이전에 FIR IP 단독 검증을 위해 <code>fir_decimator_n43_axis</code>를 Top으로 설정해 두었던 상태가 그대로 남아 있었기 때문이다. Block Design Wrapper가 아닌 FIR 모듈 단독으로 합성을 시도하니 DMA, PS 등 나머지 연결이 전혀 없는 불완전한 설계가 합성 대상이 되었다.</p>
<p>→ Sources 패널에서 <code>bd_fir_dma_wrapper</code>를 우클릭 → Set as Top으로 변경</p>
<p><strong>문제 2: 불필요한 XDC 파일 활성화</strong></p>
<p>로그에서 zybo_n43.xdc가 포함되어 있었다. 이 XDC 파일은 FIR IP를 단독으로 합성할 때 사용하던 핀 제약 파일로, fir_decimator_n43_axis의 포트를 물리 핀에 직접 매핑하는 내용이 담겨있었다. Block Design 기반 설계에서는 FIR 포트가 내부적으로 DMA에 연결되므로 물리 핀 매핑이 필요 없는데, 이 XDC가 여전히 활성화되어 있어 잘못된 핀 제약이 적용되고 있었다.</p>
<p>→ Sources 패널 → Constraints 폴더 → zybo_n43.xdc 우클릭 → Disable File</p>
<p>두 문제를 모두 수정한 뒤 다시 시도했다.</p>
<h2 id="9-1-generate-bitstream-다시-시도">9-1. Generate Bitstream 다시 시도</h2>
<p><strong>드디어 성공</strong> 
<img src="https://velog.velcdn.com/images/young-0320/post/9665c300-6295-4ebe-80f3-c44312d5422d/image.png" alt=""></p>
<p><code>IMPLEMENTATION</code> → <code>Open Implemented Design</code> → <code>Report Timing Summary</code>
<img src="https://velog.velcdn.com/images/young-0320/post/b4e602d7-7fa9-44ef-b7fb-0c55e0c0e740/image.png" alt=""></p>
<ul>
<li>WNS = +1.332ns </li>
<li>TNS = 0.000ns (타이밍 위반 없음)</li>
<li>Failing Endpoints = 0</li>
</ul>
<p>Block Design을 만들기 전, FIR 코어만 단독으로 합성해 100MHz 타이밍 클로저를 확인했을 때는 WNS가 +0.278ns였다. 여유가 더 커진 건 조건이 달라서다. 단독 합성 때는 FIR 포트가 <code>zybo_n43.xdc</code>로 물리 핀에 묶여 I/O 경로가 크리티컬 패스에 들어갔지만, Block Design에서는 같은 포트가 DMA로 가는 내부 연결이라 그 경로가 사라진다.</p>
<h2 id="10-export-hardware">10. Export Hardware</h2>
<p><code>File</code> → <code>Export</code> → <code>Export Hardware</code> → <code>Include bitstream</code> 체크
지정한 디렉토리에 xsa 파일을 export하는 과정입니다.
<strong>XSA</strong> (Xilinx Support Archive):
Vivado가 만드는 하드웨어 설명 패키지 파일
<img src="https://velog.velcdn.com/images/young-0320/post/b51a79c1-2e56-4b31-8f9d-38e5e6eb5424/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/3c459df6-64f7-45b7-82aa-3c2d522fa0ba/image.png" alt=""></p>
<h2 id="11-tcl-스크립트-추출">11. TCL 스크립트 추출</h2>
<p>Block Design은 Vivado 프로젝트 파일(.xpr) 안에만 존재합니다. 프로젝트가 유실되거나 다른 환경에서 재현해야 할 때를 대비해 <code>write_bd_tcl</code>로 Block Design 전체를 TCL 파일로 추출해 레포에 보관합니다.</p>
<h3 id="추출-명령어">추출 명령어</h3>
<p>Block Design이 열린 상태에서 Vivado TCL Console에서 실행:</p>
<pre><code>write_bd_tcl -force &lt;repo&gt;/vivado/bd_fir_dma.tcl</code></pre><p>추출 결과: <code>&lt;repo&gt;/vivado</code> 디렉토리안에 <code>bd_fir_dma.tcl</code> 스크립트 저장</p>
<p>현재 레포에서는 이 파일이 vivado/fir_n43/bd_fir_dma.tcl로 옮겨져 있고, 2편에서 수정하는 c_sg_length_width {23}과 위의 DDR 파트 수정(MT41K256M16 RE-125)이 이미 반영돼 있다. 지금 레포의 TCL은 이 글 시점의 파일이 아니다.</p>
<h3 id="포함-내용">포함 내용</h3>
<p><code>write_bd_tcl</code>이 추출하는 정보:</p>
<ul>
<li>모든 IP 정보 (processing_system7:5.5, axi_dma:7.1, smartconnect:1.0, axi_interconnect:2.1, proc_sys_reset:5.0)</li>
<li>IP 파라미터 설정 전체 (stream width=16 등)</li>
<li>모든 배선 연결 (AXI, AXI-Stream, 클럭, 리셋)</li>
<li>주소 할당 (<code>assign_bd_address</code>)</li>
<li>마지막에 <code>validate_bd_design</code> 자동 호출</li>
</ul>
<h3 id="재현-방법">재현 방법</h3>
<p>다른 머신에서 Block Design을 재현하려면:</p>
<ol>
<li>Vivado 2024.2 (Zybo Z7-20 보드 파일은 레포 boards/ 사용 가능)</li>
<li>RTL 소스 4개가 포함된 Vivado 프로젝트 생성 (1 참고)</li>
<li>Vivado TCL Console에서:</li>
</ol>
<pre><code class="language-tcl">source vivado/fir_n43/bd_fir_dma.tcl</code></pre>
<p>RTL 소스 없이 sourcing하면 <code>fir_decimator_n43_axis</code> 모듈을 찾지 못해 실패한다.
지금 레포에는 프로젝트 생성부터 BD 구성, 비트스트림, XSA까지 한 번에 도는 배치 스크립트도 있다. 보드 파일도 레포 boards/에 포함돼 별도 설치가 필요 없다.</p>
<pre><code>vivado -mode batch -source vivado/fir_n43/build_bd_fir_dma.tcl</code></pre><h2 id="error-정리">error 정리</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>증상</th>
<th>원인</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>[BD 41-758] m_axi_s2mm_aclk is not connected to a valid clock source</code></td>
<td>Run Connection Automation이 DMA 클럭 포트를 전부 연결하지 않음</td>
<td><code>s_axi_lite_aclk</code> / <code>m_axi_mm2s_aclk</code> / <code>m_axi_s2mm_aclk</code>를 <code>FCLK_CLK0</code>에 수동 연결</td>
</tr>
<tr>
<td>2</td>
<td><code>[DRC NSTD-1] 39 out of 39 logical ports use I/O standard &#39;DEFAULT&#39;</code></td>
<td>포트를 빈 캔버스에 drop → Vivado가 Make External 실행, AXI-Stream 포트가 FPGA 물리 핀으로 노출</td>
<td><code>get_bd_ports *</code>로 확인 후 외부 포트 삭제, 대상 포트 위에서 초록 체크가 뜰 때 재연결</td>
</tr>
<tr>
<td>3</td>
<td>비트스트림 생성 실패, 로그에 <code>synth_design -top fir_decimator_n43_axis</code></td>
<td>합성 Top이 <code>bd_fir_dma_wrapper</code>가 아니고, 단독 합성용 <code>zybo_n43.xdc</code>가 활성화되어 잘못된 핀 제약 적용</td>
<td>Top을 wrapper로 변경 + XDC <code>Disable File</code></td>
</tr>
<tr>
<td>4</td>
<td>(이후에 발견) PS7 DDR 파트가 <code>MT41J128M8 JP-125</code> (Zybo Z7-10용)</td>
<td>BD 생성 시 보드 파일이 제대로 적용되지 않음</td>
<td>보드 파일 재적용 후 <code>PCW_UIPARAM_DDR_PARTNO</code>가 <code>MT41K256M16 RE-125</code>인지 확인</td>
</tr>
</tbody></table>
<h2 id="마무리하며">마무리하며</h2>
<p>Validate Design 성공과 비트스트림 생성 성공은 다르다는 걸 이번 작업에서 체감했습니다. Vivado가 &quot;연결에 문제없다&quot;고 판정해도, 합성 Top이 잘못 설정되어 있거나 프로젝트 초기에 넣어둔 XDC 파일이 남아있으면 비트스트림 단계에서 막힙니다. 에러 메세지를 읽고 원인을 역추적하는 과정이 번거롭긴 했지만, 덕분에 Block Design의 외부 포트 개념과 Vivado 프로젝트 상태 관리 방식을 제대로 이해하게 됐습니다. 다음 단계는 생성된 .xsa를 Vitis에서 열어 DMA를 제어하는 C 코드를 작성하는 것입니다. 코드는 금방 나왔는데, 정작 그걸 보드에 올리는 데서 16일이 갔습니다. JTAG로 ELF를 DDR에 올리면 32비트 워드의 상위 1바이트가 계속 오염됐고, 끝내 원인을 못 밝힌 채 부팅 경로를 바꿔야 했습니다. 다음 글은 그 이야기입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RTL log #3 : Vivado IP 사용하기 - Clocking Wizard, Block Memory Generator, ILA]]></title>
            <link>https://velog.io/@young-0320/RTL-log-3-Vivado-IP-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Clocking-Wizard-Block-Memory-Generator</link>
            <guid>https://velog.io/@young-0320/RTL-log-3-Vivado-IP-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Clocking-Wizard-Block-Memory-Generator</guid>
            <pubDate>Sat, 04 Apr 2026 14:18:44 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 Zybo Z7-20 보드에서 자주 사용하게 되는 <code>Clocking Wizard</code>, <code>Block Memory Generator</code>, <code>ILA</code> IP를 정리해보겠습니다. 각 IP가 어떤 역할을 하는지, 설정 창에서 어떤 옵션을 주로 보면 되는지, 그리고 실습에서는 어떤 값을 바꾸면 되는지를 중심으로 설명하겠습니다.</p>
<h2 id="clocking-wizard-ip">Clocking Wizard IP</h2>
<p>먼저 <code>clk_wiz</code>는 Vivado의 <code>Clocking Wizard</code> IP로, Zybo Z7-20 보드에서 들어오는 125 MHz 기준 클럭을 원하는 주파수의 클럭으로 변환할 때 사용하는 블록입니다.</p>
<h3 id="1-clocking-wizard-선택">1. Clocking Wizard 선택</h3>
<ol>
<li><p>IP Catalog 열기<br><img src="https://velog.velcdn.com/images/young-0320/post/22003e58-b835-442e-9366-f1e2b379b979/image.png" alt=""></p>
</li>
<li><p><code>Clocking Wizard</code> 검색</p>
</li>
<li><p>더블클릭해서 추가<br><img src="https://velog.velcdn.com/images/young-0320/post/242eb8f5-707e-4910-b362-8ff94f41202f/image.png" alt=""></p>
</li>
</ol>
<h3 id="2-clocking-options-탭">2. Clocking Options 탭</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/3bdfbbf3-51eb-4416-9101-f26ad453ad29/image.png" alt=""></p>
<ul>
<li><p><code>Clock Monitor</code></p>
<ul>
<li>입력 클럭 이상을 감시하는 기능입니다.</li>
</ul>
</li>
<li><p><code>Primitive</code></p>
<ul>
<li><code>MMCM (Mixed-Mode Clock Manager)</code><ul>
<li>PLL 기능에 더해 위상 이동, Duty 조절, 다중 출력 제어 등 더 다양한 클럭 제어 기능을 제공합니다.</li>
</ul>
</li>
<li><code>PLL (Phase-Locked Loop)</code><ul>
<li>입력 클럭에 위상과 주파수를 맞춰 다른 주파수의 안정적인 클럭을 만들어내는 회로입니다.</li>
<li>단순한 클럭 배수/분주 용도에 적합하며 설정도 비교적 단순합니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Clocking Features</code></p>
<ul>
<li><code>Frequency Synthesis</code><ul>
<li>주파수 변환 기능입니다.</li>
<li>원하는 출력 클럭을 만들 때 사용합니다.</li>
</ul>
</li>
<li><code>Minimize Power</code><ul>
<li>전력 절약 위주 설정입니다.</li>
<li>보통은 체크하지 않아도 됩니다.</li>
</ul>
</li>
<li><code>Phase Alignment</code><ul>
<li>위상 정렬 관련 기능입니다.</li>
<li>일반적인 실습에서는 기본값으로 두어도 무방합니다.</li>
</ul>
</li>
<li><code>Dynamic Reconfig</code><ul>
<li>동작 중 클럭 설정을 바꾸는 기능입니다.</li>
</ul>
</li>
<li><code>Safe Clock Startup</code><ul>
<li>클럭이 안정적으로 시작되도록 돕는 옵션입니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Jitter Optimization</code></p>
<ul>
<li><code>Balanced</code><ul>
<li>기본 권장 옵션입니다.</li>
</ul>
</li>
<li><code>Minimize Output Jitter</code><ul>
<li>출력 지터를 최소화하는 쪽으로 최적화합니다.</li>
</ul>
</li>
<li><code>Maximize Input Jitter Filtering</code><ul>
<li>입력 지터를 더 강하게 필터링합니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Dynamic Reconfig Interface Options</code></p>
<ul>
<li>AXI나 DRP를 통한 런타임 재설정 기능입니다.</li>
</ul>
</li>
<li><p><code>Input Clock Information</code></p>
<ul>
<li><code>Primary</code>의 <code>Input Frequency (MHz)</code>는 <code>clk_in1</code>에 들어오는 입력 클럭 주파수입니다.</li>
<li>Zybo Z7-20 기준으로 125MHz에 맞춰 설정하면 됩니다.</li>
<li><code>Secondary</code>는 보조 입력 클럭을 쓰지 않으면 체크하지 않아도 됩니다.</li>
<li><code>Port Name</code>은 기본값인 <code>clk_in1</code>을 그대로 사용해도 됩니다.</li>
<li><code>Input Jitter</code>는 특별한 이유가 없다면 기본값을 사용합니다.</li>
</ul>
</li>
</ul>
<h3 id="3-output-clocks-탭">3. Output Clocks 탭</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/879e1627-83b7-4ed8-85c2-44a04323d23a/image.png" alt=""><br><img src="https://velog.velcdn.com/images/young-0320/post/203edc9f-a4af-4a0c-ba7e-06470a28d586/image.png" alt=""></p>
<ul>
<li><code>Output Freq (MHz)</code>의 <code>Requested</code> 값을 원하는 출력 주파수로 변경합니다.</li>
<li><code>Phase (degrees)</code>, <code>Duty Cycle (%)</code>, <code>power_down</code> 등은 위상 조정, 펄스 폭 조정, 고급 피드백, 저전력 제어와 관련된 고급 기능입니다.</li>
<li>일반적인 실습에서는 기본값으로 두고 주파수만 바꿔도 충분합니다.</li>
</ul>
<h3 id="4-plle2-settings-탭">4. PLLE2 Settings 탭</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/1856ab2e-2acc-4085-9da9-08c4d83c5445/image.png" alt=""></p>
<p>이 탭은 Vivado가 계산한 PLL 내부 파라미터를 요약해서 보여주는 화면입니다.</p>
<ul>
<li><p><code>CLKIN1_PERIOD = 8.000</code></p>
<ul>
<li>입력 클럭 주기가 8ns라는 뜻이며, 주파수로는 125MHz입니다.</li>
</ul>
</li>
<li><p><code>CLKFBOUT_MULT = 7</code></p>
<ul>
<li>입력 클럭을 내부 VCO 기준으로 7배 올리는 설정입니다.</li>
<li>내부 VCO 주파수 계산에 사용됩니다.</li>
</ul>
</li>
<li><p><code>DIVCLK_DIVIDE = 1</code></p>
<ul>
<li>입력 쪽 추가 분주가 없다는 뜻입니다.</li>
</ul>
</li>
<li><p><code>clk_out1</code></p>
<ul>
<li><code>Divide = 125</code></li>
<li><code>Duty Cycle = 0.500</code></li>
<li><code>Phase = 0.000</code></li>
</ul>
</li>
</ul>
<p>즉, 입력 125MHz를 내부에서 7배한 뒤 125로 나누므로 최종 출력은 7MHz가 됩니다.</p>
<h3 id="5-generate">5. Generate</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/a6909b02-b303-414c-b654-d822a019b725/image.png" alt=""><br><img src="https://velog.velcdn.com/images/young-0320/post/d6530f27-7415-4f45-92bc-26efe902fcc3/image.png" alt=""></p>
<p>설정을 마쳤다면 <code>Generate</code>를 눌러 IP를 생성하면 됩니다.</p>
<hr>
<h2 id="block-memory-generator-ip">Block Memory Generator IP</h2>
<p><code>Block Memory Generator</code>는 FPGA 내부의 BRAM(Block RAM)을 손쉽게 RAM이나 ROM 형태로 구성할 수 있게 해주는 IP입니다. 간단한 데이터 저장, 계수 테이블 저장, LUT 대체 용도 등에서 자주 사용합니다.</p>
<h3 id="1-block-memory-generator-선택">1. Block Memory Generator 선택</h3>
<ol>
<li><p>IP Catalog 열기</p>
</li>
<li><p><code>Block Memory Generator</code> 검색</p>
</li>
<li><p>더블클릭해서 추가<br><img src="https://velog.velcdn.com/images/young-0320/post/122778c0-c4c3-43c7-8410-02f951b26d0b/image.png" alt=""></p>
</li>
</ol>
<h3 id="2-basic-탭">2. Basic 탭</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/2e6258d3-dd0d-4f16-97d4-44f198185f5e/image.png" alt=""></p>
<p>특별히 바꿔야 하는 옵션이 없다면 기본값을 사용해도 됩니다.</p>
<h3 id="3-port-a-options-탭">3. Port A Options 탭</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/88d0d73c-8863-49fa-9be5-b679dc35c5c8/image.png" alt=""></p>
<p><strong>현재 화면의 의미</strong></p>
<ol>
<li>한 주소에 저장되는 데이터 폭이 8비트입니다.</li>
<li>8비트 데이터를 8개 주소까지 저장할 수 있습니다.</li>
<li>BRAM은 <code>Always Enabled</code> 상태로 두고 주소와 write enable로 제어합니다.</li>
<li>출력 레지스터는 사용하지 않습니다.</li>
</ol>
<ul>
<li><p><code>Memory Size</code></p>
<ul>
<li><code>Write Width</code><ul>
<li>한 번 쓸 때 사용하는 데이터 폭입니다.</li>
<li>값이 8이면 한 주소에 8비트를 씁니다.</li>
</ul>
</li>
<li><code>Read Width</code><ul>
<li>한 번 읽을 때 사용하는 데이터 폭입니다.</li>
<li>값이 8이면 한 주소에서 8비트를 읽습니다.</li>
</ul>
</li>
<li><code>Write Depth</code><ul>
<li>몇 개 주소에 데이터를 쓸 수 있는지입니다.</li>
<li>값이 8이면 8개의 주소를 사용할 수 있습니다.</li>
</ul>
</li>
<li><code>Read Depth</code><ul>
<li>몇 개 주소에서 데이터를 읽을 수 있는지입니다.</li>
<li>보통 <code>Write Depth</code>와 동일하게 설정합니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Enable Port Type</code></p>
<ul>
<li><code>Use ENA Pin</code><ul>
<li>BRAM에 <code>ena</code> 포트가 추가됩니다.</li>
<li><code>ena = 1</code>일 때만 해당 클럭에서 읽기/쓰기 동작을 수행합니다.</li>
</ul>
</li>
<li><code>Always Enabled</code><ul>
<li>BRAM이 항상 활성화된 상태입니다.</li>
<li>주소를 넣으면 읽고, <code>we = 1</code>이면 해당 주소에 데이터를 씁니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Port A Optional Output Registers</code></p>
<ul>
<li><code>Primitives Output Register</code><ul>
<li>BRAM 내부 출력에 레지스터를 하나 추가합니다.</li>
<li>타이밍은 더 좋아질 수 있지만 읽기 지연이 1클럭 늘어날 수 있습니다.</li>
</ul>
</li>
<li><code>Core Output Register</code><ul>
<li>BRAM 바깥에 출력 레지스터를 하나 더 추가합니다.</li>
<li>이 경우에도 출력 지연이 늘어날 수 있습니다.</li>
</ul>
</li>
<li><code>SoftECC Input Register</code><ul>
<li>ECC를 사용할 때 관련되는 옵션입니다.</li>
<li>이번 설정처럼 <code>No ECC</code>라면 크게 신경 쓰지 않아도 됩니다.</li>
</ul>
</li>
<li><code>REGCEA Pin</code><ul>
<li>출력 레지스터의 clock enable 핀입니다.</li>
<li>출력 레지스터를 사용할 때 의미가 있습니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Port A Output Reset Options</code></p>
<ul>
<li><code>RSTA Pin</code><ul>
<li>BRAM 출력 레지스터에 reset 핀을 추가합니다.</li>
</ul>
</li>
<li><code>Output Reset Value (Hex)</code><ul>
<li>reset 시 출력값을 지정합니다.</li>
</ul>
</li>
<li><code>Reset Memory Latch</code><ul>
<li>출력 래치나 레지스터 reset과 관련된 옵션입니다.</li>
</ul>
</li>
<li><code>Reset Priority</code><ul>
<li>reset과 CE가 동시에 들어왔을 때 어떤 신호를 우선할지 정하는 옵션입니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>READ Address Change A</code></p>
<ul>
<li>읽기 주소가 바뀔 때 출력 동작과 관련된 고급 옵션입니다.</li>
<li>일반적인 단순 BRAM 사용에서는 기본값을 유지해도 됩니다.</li>
</ul>
</li>
</ul>
<h3 id="4-generate">4. Generate</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/c9849b29-b497-4742-ae56-3302fb81a48b/image.png" alt=""></p>
<p>설정을 마쳤다면 <code>Generate</code>를 눌러 IP를 생성하면 됩니다.</p>
<hr>
<h2 id="ila-ip">ILA IP</h2>
<p><code>ILA (Integrated Logic Analyzer)</code>는 FPGA 내부 신호를 실시간으로 관찰할 수 있게 해주는 디버깅용 IP입니다. 시뮬레이션에서는 보이지 않는 실제 보드 동작을 확인할 때 매우 유용합니다.</p>
<h3 id="1-ila-선택">1. ILA 선택</h3>
<ol>
<li><p>IP Catalog 열기</p>
</li>
<li><p><code>ILA</code> 검색</p>
</li>
<li><p>더블클릭해서 추가<br><img src="https://velog.velcdn.com/images/young-0320/post/b4ca6cbe-ab21-4a38-8ed5-4c21d7a16b17/image.png" alt=""></p>
</li>
</ol>
<h3 id="2-general-options">2. General Options</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/bbe6dfaf-a7aa-4a5d-85d1-d4812e3772e3/image.png" alt=""></p>
<ul>
<li><p><code>Monitor Type</code></p>
<ul>
<li><code>Native</code><ul>
<li>일반 RTL 신호를 직접 관찰하는 모드입니다.</li>
</ul>
</li>
<li><code>AXI</code><ul>
<li>AXI 버스를 분석할 때 사용하는 전용 모드입니다.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Number of Probes</code></p>
<ul>
<li>ILA에 몇 개의 probe 포트를 둘지 정하는 옵션입니다.</li>
</ul>
</li>
<li><p><code>Sample Data Depth</code></p>
<ul>
<li>파형을 몇 샘플까지 저장할지 정하는 값입니다.</li>
<li>숫자가 클수록 더 긴 구간을 볼 수 있지만 FPGA 자원을 더 사용합니다.</li>
<li><code>1024</code> 정도의 기본값이면 보통 충분합니다.</li>
</ul>
</li>
<li><p><code>Same Number of Comparators for All Probe Ports</code></p>
<ul>
<li>모든 probe 포트에 같은 수의 trigger comparator를 적용할지 정하는 옵션입니다.</li>
<li>보통 체크한 상태로 둡니다.</li>
<li>comparator는 probe 값이 설정한 조건을 만족하는지 비교하는 장치입니다.</li>
</ul>
</li>
<li><p><code>Number of Comparators</code></p>
<ul>
<li>각 probe 포트에 comparator를 몇 개 둘지 정하는 값입니다.</li>
</ul>
</li>
<li><p><code>Trigger Out Port</code></p>
<ul>
<li>ILA가 트리거를 발생시켰을 때 외부로 신호를 내보내는 기능입니다.</li>
<li>여러 디버그 IP를 연동할 때 사용합니다.</li>
</ul>
</li>
<li><p><code>Trigger In Port</code></p>
<ul>
<li>외부 신호를 받아 ILA 캡처를 시작하게 하는 기능입니다.</li>
<li>고급 디버깅에서 사용합니다.</li>
</ul>
</li>
<li><p><code>Input Pipe Stages</code></p>
<ul>
<li>ILA 입력 앞에 파이프라인 레지스터를 몇 단계 둘지 설정하는 기능입니다.</li>
</ul>
</li>
<li><p><code>Trigger and Storage Settings</code></p>
<ul>
<li><code>Capture Control</code><ul>
<li>언제부터, 어떤 방식으로 저장할지 세밀하게 제어하는 옵션입니다.</li>
</ul>
</li>
<li><code>Advanced Trigger</code><ul>
<li>복잡한 조건식으로 트리거를 걸 수 있는 기능입니다.</li>
<li>예를 들어 특정 값이 나온 뒤 다른 값이 이어질 때만 캡처하도록 설정할 수 있습니다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="3-probe-ports01">3. Probe Ports(0..1)</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/0ff8b252-992a-4cc4-8d72-341cb3ee53d5/image.png" alt=""></p>
<ul>
<li><code>Probe Width</code><ul>
<li>각 probe에 연결할 신호의 비트폭을 결정하는 화면입니다.</li>
</ul>
</li>
</ul>
<p>현재 화면에서는 다음과 같이 설정되어 있습니다.
<code>probe0</code> : 40비트 신호 연결 가능
<code>probe1</code> : 1비트 신호 연결 가능</p>
<ul>
<li><code>Probe Trigger or Data</code><ul>
<li>DATA AND TRIGGER<ul>
<li>파형 데이터로도 보고 트리거 조건에도 쓸 수 있음<ul>
<li>DATA</li>
</ul>
</li>
<li>파형으로는 볼 수 있고 트리거 조건으로는 못 씀<ul>
<li>TRIGGER</li>
</ul>
</li>
<li>트리거 조건으로만 쓰고 파형 데이터로는 못 봄</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="4-generate-1">4. Generate</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/1f86295a-ecde-4da6-bca0-7a889fa3147f/image.png" alt=""></p>
<p>설정을 마쳤다면 <code>Generate</code>를 눌러 ILA IP를 생성하면 됩니다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>이번 글에서는 Zybo Z7-20 보드에서 자주 사용하게 되는 <code>Clocking Wizard</code>, <code>Block Memory Generator</code>, <code>ILA</code> IP를 정리하였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RTL log #2 : 64-bit adder — Vivado 실습]]></title>
            <link>https://velog.io/@young-0320/RTL-log-2-64-bit-adder-Vivado-%EC%8B%A4%EC%8A%B5</link>
            <guid>https://velog.io/@young-0320/RTL-log-2-64-bit-adder-Vivado-%EC%8B%A4%EC%8A%B5</guid>
            <pubDate>Wed, 25 Feb 2026 02:32:41 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 Vivado에서 64-bit Binary Adder(RCA) RTL 프로젝트를 합성(Synthesis)하는 과정을 정리했습니다.</p>
<h2 id="1-프로젝트-종류-선택">1. 프로젝트 종류 선택</h2>
<p><img src="https://velog.velcdn.com/images/young-0320/post/1a75ee16-b32c-4244-b923-579f499a4e08/image.png" alt=""></p>
<p>64-bit adder RTL 합성에서는 보통 RTL Project 선택을 합니다.</p>
<p>**  1. RTL Project**</p>
<ul>
<li>가장 일반적인 HDL 설계용.</li>
<li>Verilog/VHDL 소스 추가, 합성, 구현, 타이밍 분석까지 가능</li>
</ul>
<p>추가 체크박스( RTL Project 아래 ):</p>
<ul>
<li>Do not specify sources at this time: 지금은 소스 안 넣고 빈 프로젝트만 생
성.</li>
<li>Project is an extensible Vitis platform: Vitis 플랫폼 개발용(일반 RTL 합성
에서는 보통 체크 안 함).</li>
</ul>
<p>**  2. Post-synthesis Project**</p>
<ul>
<li>RTL이 아니라 이미 합성된 결과물(netlist/DCP) 기준으로 여는 프로젝트</li>
<li>디바이스 리소스/배치/타이밍 분석 용도</li>
<li>보통 초반 설계 단계에서는 잘 안 씀</li>
</ul>
<p>**  3. I/O Planning Project**</p>
<ul>
<li>소스 없이 핀맵/패키지(I/O) 계획만 먼저 할 때 사용</li>
<li>XDC 핀 배치 준비 용도</li>
</ul>
<p>**  4. Imported Project**</p>
<ul>
<li>Synplify 같은 외부 툴 프로젝트를 Vivado로 가져올 때 사용</li>
<li>일반 RTL 직접 작성 과제에서는 거의 안 씀</li>
</ul>
<p>**  5. Example Project**</p>
<ul>
<li>Vivado 예제 템플릿으로 시작</li>
<li>학습용/샘플 확인용</li>
</ul>
<h2 id="2-프로젝트-소스-등록">2. 프로젝트 소스 등록</h2>
<p>이미 rtl 설계 파일이 있으니 Add Files을 눌러 파일(*.v)을 추가합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/cf06c986-c7ff-4f65-bf4b-b822b3f2aee5/image.png" alt="">
물리/타이밍 제약 사항을 넣는 페이지입니다. Next 눌러줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/3b41c056-bd7b-40bf-a3d9-6a540c15877c/image.png" alt=""></p>
<h2 id="3-보드-선택">3. 보드 선택</h2>
<p>가장 많이 이용하는 Basys3 보드를 선택했습니다. Finish 눌러줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/fb27add1-6a9f-46c9-8e12-b60e0ad23d51/image.png" alt=""></p>
<h2 id="4-run-synthesis">4. Run Synthesis</h2>
<p>합성할 모듈이 Top 모듈인지 확인하고 좌측에서 Run Synthesis를 눌러줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/0b81fff6-ee26-426a-b4ec-f1b57a9d0381/image.png" alt="">
XDC(제약 사항)가 없는 합성이었기에 Open Synthesized Design을 선택합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/7193900e-fc2e-45bd-aa96-d53b102fd87d/image.png" alt=""></p>
<h2 id="5-결과-확인">5. 결과 확인</h2>
<h3 id="1-로그-확인">1. 로그 확인</h3>
<p><code>24 Infos, 1 Warnings, 0 Critical Warnings and 0 Errors encountered.</code></p>
<p><code>synth_design completed successfully</code> </p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/0e5ac842-3868-48a3-8c7d-f5f8192b095a/image.png" alt=""></p>
<h3 id="2-report-utilization으로-사용-lutiob-확인">2. Report Utilization으로 사용 LUT/IOB 확인</h3>
<p>LUT: 108/20800
IOB: 193/ 106 (Top 포트가 a(64)+b(64)+sum(64)+c_out(1)=193비트라 Basys3 가용 I/O(106)를 초과합니다.)
<img src="https://velog.velcdn.com/images/young-0320/post/2446013e-a9a4-4112-bedf-e1a168a91804/image.png" alt=""></p>
<h3 id="3-open-elaborated-design-----확인">3. Open Elaborated Design     확인</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/18da32d2-888e-43d8-9d36-43379d39335e/image.png" alt="">
rca_32bit 모듈 두 개를 사용하여 계층적으로 모델링 된 것과 입출력 포트가 제대로 합성된 것을 확인할 수 있습니다.</p>
<h3 id="4-device-view-확인">4. Device View 확인</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/9490fcab-cc9d-4665-a40b-2d1d129de455/image.png" alt=""></p>
<ol>
<li><code>[Run Synthesis]</code> 버튼을 눌러 실제 합성을 진행합니다.</li>
<li><code>Open Synthesized Design</code>를 눌러 합성된 결과물을 확인합니다.</li>
</ol>
<p>중앙의 흰 사각형/분홍 점 : 현재 배치된 셀, 즉 합성 결과물이 올라간 위치</p>
<p><strong>상태 해석:</strong>
중앙에 배치된 셀이 매우 적고(흰 사각형 몇 개) 대부분의 칩 면적이 비어 있습니다. 이는 64비트 가산기 회로가 LUT 사용량이 낮아 칩의 극히 일부만 점유하고 있다는 것을 시각적으로 보여줍니다. I/O가 193비트로 핀 수를 초과했기 때문에 IOB 쪽에는 제대로 매핑이 되지 않은 상태로 보입니다.</p>
<h3 id="5-데이터-패스-지연-시간-확인">5. 데이터 패스 지연 시간 확인</h3>
<ol>
<li>Tcl console 창에 <code>report_timing -from [get_ports a[*]] -to [get_ports sum[*]] -setup</code> 해당 명령어를 입력합니다.</li>
<li>해당 명령어는 Netlist을 그래프로 인식하고, 지정된 시작점($a$)에서 끝점($sum$)까지의 모든 경로 중 가장 느린 최악의 경로를 찾아 지연 시간을 합산합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/ca004e8a-98ac-4afe-8f1e-8ad20ed5e3cd/image.png" alt="">
최악의 경로</li>
</ol>
<ul>
<li>시작점 : a[2]</li>
<li>끝점 : sum[61]</li>
<li>Data Path Delay : 19.706ns(logic delay 6.708ns, route delay 12.998ns)</li>
<li>Logic Levels : 28 (신호가 통과한 소자의 개수)</li>
<li>최대 주파수 : $1 / 19.706ns \approx 50.74MHz$</li>
</ul>
<p><strong>결론 :</strong> 
Half adder에서부터 RCA 64-bit adder까지의 계층적 구조적 한계, 즉 Ripple Carry 구조에서는 carry가 LSB에서 MSB 방향으로 직렬 전파되기 때문에 비트 수에 비례해 critical path가 길어지는 문제로 인해 net delay가 19.706ns 중 12.998ns를 차지할 만큼 배선 지연이 압도적으로 커졌습니다. 이로 인해 최대 동작 주파수가 약 50.74MHz에 그쳐 고속 설계 요구사항을 만족하기 어려우므로 하드웨어적 보완이 필요합니다.</p>
<p><strong>해결 :</strong> </p>
<ul>
<li>게이트 레벨 모델링 대신 데이터 플로우 모델링(assign 할당문)으로 재합성하여, 합성 툴이 carry look-ahead 등 최적화된 내부 구조를 자동으로 선택할 수 있도록 한다.</li>
<li>carry chain의 중간에 pipeline register를 삽입해 critical path를 절반으로 단축한다.</li>
</ul>
<h2 id="마무리하며">마무리하며</h2>
<p>합성은 에러 없이 완료되었고, LUT 사용량도 낮은 수준으로 확인되었습니다. 다만 Top I/O가 193비트로 Basys3의 가용 I/O(106핀)를 초과하여 그대로 구현하기는 어려운 상황이며, 실제 FPGA에 올리려면 I/O 수를 줄이는 설계 수정이 선행되어야 합니다. 또한 이번 실습에서는 XDC 제약을 적용하지 않았기 때문에 타이밍 리포트는 참고 수준으로만 해석하였습니다.</p>
<p>다음 실습에서는 XDC 제약을 적용하여 실제 타이밍 에러를 직접 확인하고 디버깅하는 과정을 진행할 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #16 : Vivado 설치 가이드]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-16-Vivado-%EC%84%A4%EC%B9%98-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-16-Vivado-%EC%84%A4%EC%B9%98-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Tue, 24 Feb 2026 15:32:18 GMT</pubDate>
            <description><![CDATA[<p>Vivado 설치 과정을 단계별로 정리했습니다.</p>
<h2 id="설치-전-os-버전-확인">설치 전 OS 버전 확인</h2>
<p><img src="https://velog.velcdn.com/images/young-0320/post/b9d0a400-d108-4110-be82-d5e2204fc457/image.png" alt="">
fastfetch 프로그램으로 확인했을 때 OS 버전은 Ubuntu 24.04.4 LTS x86_64입니다.</p>
<h2 id="1-amd-회원가입-및-로그인">1. AMD 회원가입 및 로그인</h2>
<h2 id="2-20242-버전-설치">2. 2024.2 버전 설치</h2>
<p>AMD 공식 다운로드 페이지에 접속해 2024.2 버전을 선택합니다. <a href="https://www.xilinx.com/support/download/index.html/content/xilinx/en/downloadNav/vivado-design-tools/2024-2.html">https://www.xilinx.com/support/download/index.html/content/xilinx/en/downloadNav/vivado-design-tools/2024-2.html</a>
<img src="https://velog.velcdn.com/images/young-0320/post/8fa0b2c0-35af-412b-915c-21a50c499653/image.png" alt="">
스크롤을 내려 Linux용 설치 파일을 선택합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/879feb51-52d2-4006-b1a6-e7fff62e98a7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/36c2ed65-b8cd-43a1-8811-2150da1f022e/image.png" alt=""></p>
<h2 id="3-무결성-확인">3. 무결성 확인</h2>
<p>해당 파일이 있는 디렉토리로 이동 후 아래의 명령어를 입력해 파일의 해시값을 추출합니다.</p>
<pre><code>md5sum FPGAs_AdaptiveSoCs_Unified_2024.2_1113_2356_Lin64.bin</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/bf45b228-d6b9-40ce-b838-7c3ce8620d2d/image.png" alt=""></p>
<pre><code># 32자리의 16진수 값 출력
6be7a694bc631e02862e173f94165f88</code></pre><p>출력된 값을 AMD 공식 홈페이지에 명시된 해시값과 비교해 일치 여부를 확인합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/cc605e45-c363-4216-994e-71b3dc71492a/image.png" alt="">
일치함을 확인할 수 있습니다.</p>
<h2 id="4-파일-실행">4. 파일 실행</h2>
<p>아래의 명령어를 순서대로 입력해 설치 파일을 실행합니다.</p>
<pre><code># 작업 디렉토리를 파일이 설치된 곳으로 이동
cd ~/Downloads

# 실행 권한 부여
young@young-PC:~/Downloads$ chmod +x FPGAs_AdaptiveSoCs_Unified_2024.2_1113_2356_Lin64.bin

# 파일 실행
./FPGAs_AdaptiveSoCs_Unified_2024.2_1113_2356_Lin64.bin</code></pre><p><strong>실행 권한 부여:</strong> <code>chmod</code>(Change Mode) 명령어에 <code>+x</code>(eXecute) 옵션을 주어 해당 파일에 실행 권한을 부여합니다.</p>
<p><strong>파일 실행:</strong> 리눅스는 보안상 현재 디렉토리에 있는 파일을 파일명만으로 실행할 수 없습니다. 현재 위치를 나타내는 <code>./</code>를 앞에 붙여야 합니다.</p>
<h2 id="5-설치-과정">5. 설치 과정</h2>
<h3 id="1-continue-선택">1. Continue 선택</h3>
<p>Zynq-7000(Pynq-Z2)이나 Artix-7(Basys 3) 같은 7 Series 칩을 사용하는 경우, 2024.2와 2025.2 간의 성능 차이는 사실상 없습니다. 굳이 최신 버전을 고집하기보다는 어느 정도 검증된 2024.2 버전을 사용하는 것이 안정성 면에서 더 낫습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/3c8f9a0f-4cdd-4274-b8de-32ff3afdea3d/image.png" alt=""></p>
<h3 id="2-로그인">2. 로그인</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/c24dfa80-a97b-432b-aa77-6600f9be8a96/image.png" alt=""></p>
<h3 id="3-download-and-install-now-선택">3. <code>Download and Install Now</code> 선택</h3>
<p>필요한 파일만 선택적으로 다운로드할 수 있게하는 옵션입니다. 
<img src="https://velog.velcdn.com/images/young-0320/post/711f910f-4b30-432c-be4c-1d619aa7e9c7/image.png" alt=""></p>
<h3 id="4-vivado-선택">4. Vivado 선택</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/8510d8b2-00c2-485a-925a-c3b8cbf7c8d3/image.png" alt=""></p>
<h3 id="5-vivado-ml-standard-선택">5. Vivado ML Standard 선택</h3>
<p>무료 버전입니다. 타겟으로 하는 Pynq-Z2(Zynq-7000 계열)와 Basys 3(Artix-7 계열)는 모두 이 범주에 들어갑니다.
<img src="https://velog.velcdn.com/images/young-0320/post/5c37b2f9-5f9e-4dd0-897f-e60cd98e07f6/image.png" alt=""></p>
<h3 id="6-design-tools-설계-도구">6. Design Tools (설계 도구)</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/905d5edd-b3b1-4b6b-b4ec-75334b521cc4/image.png" alt=""></p>
<ul>
<li><p><code>Vivado</code>와 <code>Vitis HLS</code> 기본 선택 유지</p>
</li>
<li><p><code>Vitis Networking P4</code>: 데이터센터 스위치 라우터에서 패킷을 처리하는 특수 언어(P4)를 컴파일하는 엔진입니다. 일반적인 FPGA 설계와는 완전히 무관합니다.</p>
</li>
<li><p><code>Vitis Model Composer</code>: MATLAB에서 블록 다이어그램을 그리면 하드웨어 코드로 자동 변환해 주는 도구입니다.</p>
<ul>
<li><code>Vitis Embedded Development</code>: Zynq 칩 내부의 ARM 프로세서에서 돌아갈 소프트웨어(펌웨어)를 C/C++로 개발하기 위한 통합 개발 환경(IDE)입니다.
Pynq-Z2 보드에 미리 컴파일된 리눅스를 올리고 웹브라우저(Jupyter)에서 파이썬으로 보드를 제어할 것이면 필요 없습니다.</li>
</ul>
</li>
<li><p><code>Power Design Manager (PDM)</code>: 칩이 소모하는 전력을 계산하는 도구인데, 주로 최신 공정(UltraScale+, Versal)을 타겟으로 하므로 구형인 7 Series 칩에는 거의 쓰이지 않습니다.</p>
</li>
<li><p><code>DocNav</code>: PDF 문서 모음집입니다. 필요한 문서는 Xilinx 공식 사이트나 구글에서 직접 검색하는 편이 더 효율적입니다</p>
</li>
</ul>
<h3 id="7-devices">7. Devices</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/d8fe8fa2-0b14-4765-afe8-f0fa813cf52e/image.png" alt="">
타겟으로 하는 장치만 선별적으로 선택합니다.</p>
<ul>
<li><p><code>Zynq-7000</code>: Pynq-Z2 보드에 탑재된 칩입니다. ARM CPU(프로세서)와 FPGA(프로그래머블 로직)가 하나의 칩 안에 통합된 SoC(System-on-Chip) 구조로, 하드웨어 가속기를 구현하고 제어할 때 핵심적인 역할을 합니다.</p>
</li>
<li><p><code>Artix-7</code> : Basys 3 보드에 탑재된 칩입니다. 가격 대비 성능이 우수해 전 세계 대학의 디지털 논리 회로 실습에서 가장 널리 사용되는 칩입니다.</p>
</li>
<li><p><code>Kintex-7, Virtex-7</code>: <code>Artix-7</code>보다 규모가 크고 고성능인 칩들로, 주로 통신 기지국이나 방송 장비 등 전문 산업 분야에서 활용됩니다.</p>
</li>
<li><p><code>UltraScale, UltraScale+, Versal ACAP</code>: 7 Series 이후 세대의 칩들로, 데이터센터 AI 가속이나 5G 통신 인프라처럼 고성능·대규모 처리가 요구되는 환경에서 사용됩니다. 가격은 수백만 원에서 수천만 원대에 이릅니다.</p>
</li>
<li><p><code>Engineering Sample (ES) Devices</code>: 정식 출시 전 검증 목적으로 일부 기업에만 제공되는 프로토타입 칩입니다. 일반 사용자가 접할 일은 거의 없습니다.</p>
</li>
</ul>
<h3 id="8-용량-확인">8. 용량 확인</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/a6140374-a07f-4557-aaf1-033db11120d0/image.png" alt="">
필요한 옵션만 선택해도 설치 용량이 60.97GB에 달합니다. 불필요한 옵션까지 포함하면 100~200GB를 쉽게 넘길 수 있으니, 선택 항목을 다시 한번 확인하고 설치 전 디스크 여유 공간을 점검해 두세요.</p>
<h3 id="9-약관-동의">9. 약관 동의</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/41bd4c4e-4fde-4550-89a1-e7461aa8e857/image.png" alt=""></p>
<h3 id="10-설치-경로-설정">10. 설치 경로 설정</h3>
<p>기본 설치 경로인 <code>/tools/Xilinx</code>는 루트 디렉토리 하위에 있어 sudo 권한이 필요합니다. 설치 프로그램을 일반 권한으로 실행했기 때문에 이 경로에는 설치할 수 없습니다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/d2085b4c-b296-4826-902a-801c242124b1/image.png" alt="">
사용자 홈 디렉토리 아래에 <code>Xilinx</code> 폴더를 생성하고 설치 경로를 해당 폴더로 변경합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/edc33bf2-9ac6-43ec-a8a1-78876b59f743/image.png" alt=""></p>
<h3 id="11-환경-변수-등록">11. 환경 변수 등록</h3>
<p>설치 후 터미널에서 <code>vivado</code> 명령어를 사용하려면 환경 변수를 등록해야 합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/243ab892-ee9d-4d6f-a88e-43c6c839040a/image.png" alt=""></p>
<pre><code># 설치 경로 확인
ls -l /home/young/Xilinx/Vivado/2024.2/settings64.sh

# .bashrc 파일 끝에 환경 변수 추가
echo &quot;source /home/young/Xilinx/Vivado/2024.2/settings64.sh&quot; &gt;&gt; ~/.bashrc

# 변경사항 즉시 적용
source ~/.bashrc</code></pre><h3 id="12-실행">12. 실행</h3>
<pre><code># 터미널로 실행 가능
vivado</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/e97bc8c7-4ffe-4dc7-992f-ace0dc338e63/image.png" alt="">
성공!</p>
<h2 id="마무리하며">마무리하며</h2>
<p>이것으로 Vivado 설치가 완료되었습니다. 다음 글에서는 Vivado 기본 사용법과 간단한 RTL 합성 과정을 다뤄보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RTL log #1 : Half Adder 설계부터 검증까지 — iverilog 실습]]></title>
            <link>https://velog.io/@young-0320/Half-Adder-%EC%84%A4%EA%B3%84%EB%B6%80%ED%84%B0-%EA%B2%80%EC%A6%9D%EA%B9%8C%EC%A7%80-iverilog-%EC%8B%A4%EC%8A%B5</link>
            <guid>https://velog.io/@young-0320/Half-Adder-%EC%84%A4%EA%B3%84%EB%B6%80%ED%84%B0-%EA%B2%80%EC%A6%9D%EA%B9%8C%EC%A7%80-iverilog-%EC%8B%A4%EC%8A%B5</guid>
            <pubDate>Mon, 23 Feb 2026 09:43:54 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 가장 기초적인 Half Adder를 통해 RTL 설계의 기본기와 일반적인 검증 워크플로우를 정리해 보겠습니다.</p>
<p>Half Adder를 첫 예제로 고른 이유는 간단합니다. 로직 자체가 단순해서 설계보다 &quot;내가 만든 게 정말 맞게 동작하는지 어떻게 확인하지?&quot; 라는 검증의 흐름에 집중하기 좋기 때문입니다. 
처음 RTL을 접했을 때 Verilog 문법이나 시뮬레이터 사용법부터 막막하게 느껴졌습니다.
이 글이 같은 고민을 하고 있는 분들께 조금이나마 도움이 됐으면 좋겠습니다.</p>
<h2 id="rtl-설계-및-검증-프로세스">RTL 설계 및 검증 프로세스</h2>
<p>RTL(Register Transfer Level) 설계는 단순히 코드를 짜는 것에 그치지 않고, 의도한 대로 동작하는지 증명하는 과정이 핵심입니다. 일반적인 순서는 다음과 같습니다.</p>
<p><strong>1. RTL 작성 (Design):</strong> 하드웨어 로직 구현</p>
<p><strong>2. 테스트벤치 작성 (Testbench):</strong> 입력 시나리오 및 검증 로직 작성</p>
<p><strong>3. 컴파일 (Compilation):</strong> 설계 및 테스트벤치 코드의 문법 체크 및 실행 파일 생성</p>
<p><strong>4. 검증 (Verification):</strong> 파형(Waveform) 분석 또는 로그 확인</p>
<h2 id="1-rtl-작성-design-half-adder">1. RTL 작성 (Design): Half Adder</h2>
<p>반가산기는 가장 기본적인 산술 연산 블록으로, 2진수 1비트 두 개를 더해 합(Sum)과 올림수(C_out)를 출력합니다. </p>
<pre><code>`timescale 1ns / 1ps

module half_adder (
    input  a, b,
    output c_out, sum
);
  // XOR 게이트를 이용한 Sum 구현
  xor g1 (sum, a, b);
  // AND 게이트를 이용한 Carry 구현
  and g2 (c_out, a, b);
endmodule</code></pre><h2 id="2-테스트벤치testbench-작성">2. 테스트벤치(Testbench) 작성</h2>
<p>테스트벤치는 설계한 모듈(DUT, Design Under Test)에 가상의 입력을 넣어주는 실험 환경입니다. 여기서는 두 가지 방식을 비교합니다.</p>
<h3 id="a-directed-testbench-수동-검증">A. Directed Testbench (수동 검증)</h3>
<p>입력값을 하나하나 직접 인가하고, 터미널의 로그나 파형을 보고 눈으로 직접 결과를 확인하는 방식입니다.</p>
<ul>
<li><p>장점: 직관적이며 소규모 회로의 동작을 빠르게 확인할 때 유리합니다.</p>
</li>
<li><p>단점: 입력 조합이 많아질수록 누락되는 케이스가 생기기 쉽고, 하나하나 확인하기 번거롭습니다.</p>
</li>
</ul>
<pre><code>// directed testbench for half adder
`timescale 1ns / 1ps

module test_half_adder ();
  reg t_a, t_b;
  wire t_c_out, t_sum;
  half_adder uut (
      .a(t_a),
      .b(t_b),
      .c_out(t_c_out),
      .sum(t_sum)
  );

  initial #100 $finish;
  initial begin
    $dumpfile(&quot;sim/build/output/iverilog/test_half_adder.vcd&quot;);
    $dumpvars(0, test_half_adder);
    $display(&quot;a b | c_out sum&quot;);
    $display(&quot;-------------&quot;);
    t_a = 0;
    t_b = 0;
    #10;
    $display(&quot;%b %b | %b     %b&quot;, t_a, t_b, t_c_out, t_sum);
    t_a = 0;
    t_b = 1;
    #10;
    $display(&quot;%b %b | %b     %b&quot;, t_a, t_b, t_c_out, t_sum);
    t_a = 1;
    t_b = 0;
    #10;
    $display(&quot;%b %b | %b     %b&quot;, t_a, t_b, t_c_out, t_sum);
    t_a = 1;
    t_b = 1;
    #10;
    $display(&quot;%b %b | %b     %b&quot;, t_a, t_b, t_c_out, t_sum);

  end
endmodule</code></pre><h3 id="b-self-checking-testbench-자동-검증">B. Self-checking Testbench (자동 검증)</h3>
<p>입력값을 랜덤으로 인가하거나 모든 조합을 돌리면서, <strong>Reference Model(정답지)</strong>과 실제 출력값을 비교하여 자동으로 PASS/FAIL을 판정하는 방식입니다.</p>
<ul>
<li>눈으로 직접 확인하는 대신, 코드 내부의 if문과 ref_model이 결과를 실시간으로 대조합니다.</li>
<li>repeat 문을 통해 수천 번의 랜덤 테스트를 수행할 수 있어, 설계자가 예상치 못한 코너 케이스(Corner Case)를 잡기에 유리합니다.</li>
<li>seen 변수 등을 활용해 모든 입력 조합이 한 번씩은 들어갔는지(Coverage)를 확인할 수 있습니다.</li>
</ul>
<p>보통 6개의 블럭으로 구성됩니다.</p>
<ol>
<li><p>DUT 연결 (u_dut)</p>
</li>
<li><p>입력 인가 (apply stimulus)</p>
</li>
<li><p>기대값 계산 (reference model)</p>
</li>
<li><p>자동 비교 (if (got !== exp))</p>
</li>
<li><p>에러 카운트/최종 PASS-FAIL</p>
</li>
<li><p>타임아웃(TIMEOUT) 종료</p>
<p>시스템 베릴로그 문법을 사용하여 작성했습니다.</p>
<pre><code>// self-checking testbench for half_adder
`timescale 1ns / 1ps
module test_half_adder;
reg t_a, t_b;
reg [1:0] ref_out;
wire t_c_out, t_sum;
integer i;
integer err_cnt;
reg [3:0] seen;  // 입력 조합 관측 체크용
// 1. DUT 연결
half_adder u_dut (
 .a(t_a),
 .b(t_b),
 .c_out(t_c_out),
 .sum(t_sum)
);

// 3. reference model
function [1:0] ref_half_adder;
input a_i, b_i;
begin
 ref_half_adder = a_i + b_i;  // {c_out, sum}
end
endfunction

initial begin
err_cnt = 0;
seen    = 4&#39;b0000;

// ── 2. 입력 인가 ──
repeat (50) begin
 t_a = $urandom_range(0, 1);
 t_b = $urandom_range(0, 1);
 #1;
 seen[{t_a, t_b}] = 1&#39;b1;
 // ── 4. 자동 비교: 랜덤 50회 ──
 ref_out = ref_half_adder(t_a, t_b);
 if ({t_c_out, t_sum} !== ref_out) begin
   $error(&quot;Mismatch: a=%0b b=%0b got=%0b%0b exp=%0b%0b&quot;, t_a, t_b, t_c_out, t_sum, ref_out[1],
          ref_out[0]);
   err_cnt = err_cnt + 1;
 end
end

// ── 미커버 조합 강제 인가 (전수 커버리지 보장) ─────────
for (i = 0; i &lt; 4; i = i + 1) begin
 if (!seen[i]) begin
   {t_a, t_b} = i[1:0];
   #1;
   seen[{t_a, t_b}] = 1&#39;b1;
   ref_out = ref_half_adder(t_a, t_b);
   if ({t_c_out, t_sum} !== ref_out) begin
     $error(&quot;Mismatch: a=%0b b=%0b got=%0b%0b exp=%0b%0b&quot;, t_a, t_b, t_c_out, t_sum,
            ref_out[1], ref_out[0]);
     err_cnt = err_cnt + 1;
   end
 end
end

// ── 5. 최종 판정 ──
if (seen !== 4&#39;b1111) $fatal(1, &quot;FAIL: not all input combos covered, seen=%b&quot;, seen);

if (err_cnt == 0) $display(&quot;PASS: self-checking random TB&quot;);
else $fatal(1, &quot;FAIL: err_cnt=%0d&quot;, err_cnt);

$finish;
end

initial begin
#1000;
$fatal(1, &quot;TIMEOUT&quot;);
end
</code></pre></li>
</ol>
<p>endmodule</p>
<pre><code>

## 3. 컴파일 (Compilation): iverilog 가이드
컴파일 단계에서는 iverilog를 사용합니다. 각 옵션의 의미를 정확히 아는 것이 중요합니다.</code></pre><p>// iverilog 컴파일 명령어 예시
iverilog -g2012 -Wall -s test_half_adder -o sim/build/output/iverilog/test_half_adder.vvp <br>rtl/half_adder.v sim/test/test_half_adder.v</p>
<pre><code>**옵션 상세 설명**
```-g2012```: iverilog가 사용할 Verilog 표준을 지정합니다. 
g2005, g2001 등 여러 버전이 있는데, -g2012는 IEEE 1800-2012(SystemVerilog) 표준으로 $urandom_range 같은 최신 시스템 함수를 사용하려면 이 옵션이 필요합니다. 
명시하지 않으면 iverilog 기본값으로 컴파일되어 최신 문법에서 에러가 날 수 있습니다.

```-Wall```: 모든 경고(Warning) 메시지를 출력합니다. 사소한 문법 실수나 포트 연결 누락처럼 에러는 아니지만 잠재적으로 문제가 될 수 있는 부분을 알려줍니다.

```-s test_half_adder```: 시뮬레이션의 최상위 모듈(Top-level module)을 지정합니다. 보통 테스트벤치 모듈 이름인 ```test_half_adder```가 들어갑니다.

```-o [출력파일경로]```: 컴파일 결과로 생성될 실행 파일의 경로와 이름을 지정합니다. iverilog의 출력물은 .vvp 확장자를 사용하며, 이후 vvp 명령어로 실행합니다. 경로를 지정하지 않으면 현재 디렉토리에 a.out으로 생성됩니다.

```소스 파일 나열```: 마지막에는 컴파일에 필요한 모든 .v 파일을 나열합니다. 설계 파일(```half_adder.v```)을 먼저, 테스트벤치 파일(```test_half_adder.v)```을 그 뒤에 적는 것이 일반적입니다. 파일을 빠뜨리면 &quot;module not found&quot; 에러가 발생하니 주의하세요.

## 4. 검증 (Verification)
컴파일이 성공했다면 vvp 명령어로 시뮬레이션을 실행합니다.</code></pre><p>// 시뮬레이션 실행
vvp sim/build/output/iverilog/test_half_adder.vvp</p>
<p>// 출력 예시
PASS: self-checking random TB</p>
<pre><code>### 결과 분석 방법

### 1. Directed Testbench 방식
- 터미널에 출력된 진리표로 직접 확인합니다.
![](https://velog.velcdn.com/images/young-0320/post/3b160ce2-6ae6-4ea7-83af-7639be32695e/image.png)
-  혹은 아래 명령어로 파형을 직접 확인할 수도 있습니다.
![](https://velog.velcdn.com/images/young-0320/post/dffdc6e5-27bd-4082-b22b-173dd5d0a831/image.png)
</code></pre><p>// gtkwave를 이용하여 파형 확인
gtkwave sim/build/output/iverilog/test_half_adder.vcd</p>
<pre><code>### 2. Self-checking Testbench 방식
아래 메시지가 뜨면 모든 테스트를 통과한 것입니다.</code></pre><p>PASS: self-checking random TB</p>
<pre><code>![](https://velog.velcdn.com/images/young-0320/post/52496ee8-1d9a-4d63-9d78-1cde1aaca428/image.png)
만약 실패했다면 $error와 $fatal 메시지가 함께 출력됩니다. 
$error는 어떤 입력값에서 출력이 틀렸는지 알려주고, $fatal은 최종적으로 몇 개의 오류가 발생했는지 요약해줍니다. 로그를 위에서부터 읽으면서 첫 번째 ERROR가 찍힌 시점을 찾는 것이 디버깅의 시작입니다.</code></pre><p>ERROR: Mismatch: a=1 b=1 got=01 exp=10
FAIL: err_cnt=1</p>
<pre><code>
## 마무리하며 
이번 포스팅에서는 Half Adder를 통해 RTL 설계부터 iverilog를 이용한 컴파일, 검증까지 전체 흐름을 정리해봤습니다. 단순한 회로지만 막상 직접 테스트벤치를 짜고 시뮬레이션을 돌려보면 생각보다 알아야 할 것이 많다고 느꼈습니다. 

다음 포스팅에서는 베릴레이터를 이용하여 64bit adder를 검증해 볼 예정입니다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #14 : 가장 깔끔한 파이썬 개발 환경: Ubuntu + uv ]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-13-%EA%B0%80%EC%9E%A5-%EA%B9%94%EB%81%94%ED%95%9C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-Ubuntu-uv</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-13-%EA%B0%80%EC%9E%A5-%EA%B9%94%EB%81%94%ED%95%9C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-Ubuntu-uv</guid>
            <pubDate>Sun, 15 Feb 2026 12:41:15 GMT</pubDate>
            <description><![CDATA[<p>기존 파이썬 환경 설정(pyenv, pip 등)은 꽤나 번거롭고 속도가 느렸습니다. 또한 의존성 충돌도 발생할 수 있기에 고민이 되었습니다.
Rust로 작성된 차세대 패키지 매니저 uv는 기존 도구들을 대체하는 압도적인 속도와 cargo 스타일의 통합된 워크플로우를 제공합니다. </p>
<p>이번 포스팅에서는 Ubuntu 환경에서 uv를 설치하고, 가장 빠르고 안전하게 파이썬 개발 환경을 구축하는 방법을 정리했습니다.</p>
<h2 id="왜-poetry-대신-uv인가">왜 Poetry 대신 uv인가?</h2>
<p>파이썬 프로젝트를 시작할 때 패키지 관리 도구로 가장 먼저 떠오르는 것은 Poetry입니다. 하지만 저는 이번 프로젝트에서 Rust 기반의 차세대 도구인 uv를 선택했습니다. 그 이유는 다음과 같습니다.</p>
<p><strong>Poetry: 기존의 강력한 표준</strong>
의존성 관리, 패키징, 배포를 위한 올인원 도구로 오랫동안 사랑받아 왔습니다.
하지만 프로젝트 규모가 커질수록 의존성 해결 속도가 느려지는 단점이 있습니다.</p>
<p><strong>uv: 압도적인 속도</strong>
Rust로 작성되어 기존 pip이나 pip-tools보다 10~100배 빠른 압도적인 속도를 보여줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/1c42bc6b-4f17-432a-be0f-ce0c00f35900/image.png" alt=""></p>
<p>단순히 패키지만 설치하는 것이 아니라, <strong>Python 버전 관리(pyenv 대체)</strong>부터 <strong>가상 환경 관리(venv 대체)</strong>까지 하나의 툴로 끝낼 수 있습니다.</p>
<p><strong>&quot;속도&quot;와 &quot;통합성&quot;</strong></p>
<ul>
<li><p>Rust 기반의 빠른 설치 및 해석 속도로 개발 생산성을 극대화할 수 있습니다.</p>
<ul>
<li>pyenv로 파이썬 버전을 깔고, venv로 가상 환경을 만들고, pip으로 패키지를 설치하는 복잡한 과정 없이, uv 명령어 하나로 모든 환경 설정이 끝납니다.</li>
</ul>
</li>
</ul>
<h1 id="uv-설치-가이드">uv 설치 가이드</h1>
<h2 id="1-바이너리-파일-설치">1. 바이너리 파일 설치</h2>
<p><img src="https://velog.velcdn.com/images/young-0320/post/7332e6b4-6252-4053-9c9c-1c8be2018d17/image.png" alt=""></p>
<pre><code># 공식 설치 스크립트
curl -LsSf https://astral.sh/uv/install.sh | sh</code></pre><h2 id="2-설치-확인">2. 설치 확인</h2>
<p>uv는 자동으로 환경 변수를 설정하기에 단순히 터미널을 껐다 킨 뒤
<code>uv --version</code> 명령어로 버전을 확인합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/9db916a8-a193-496d-bf30-e81dcf108d7d/image.png" alt=""></p>
<p>성공!</p>
<h2 id="3-실행">3. 실행</h2>
<p>프로젝트의 루트 디렉토리로 이동해 <code>uv init</code> 명령어로 uv를 실행시킵니다.
<img src="https://velog.velcdn.com/images/young-0320/post/f9bda4a6-2929-4b7f-964e-6b7d52bf83a1/image.png" alt=""></p>
<p>생성되는 파일 목록:</p>
<ul>
<li><code>.gitignore</code> : Git이 추적하지 않을 파일 혹은 폴더 지정</li>
<li><code>.ptrhon-version</code> : 파이썬 버전 명시</li>
<li><code>main.py</code> : 파이썬 프로그램 실행 진입점</li>
<li><code>pyproject.toml</code> : 프로젝트 통합 설정 파일</li>
<li><code>README.md</code> : 프로젝트 설명서</li>
</ul>
<p><img src="https://velog.velcdn.com/images/young-0320/post/1cc41fcb-9fa6-41d7-bb9c-3299b5d6d47d/image.png" alt=""></p>
<pre><code>uv run python --version</code></pre><p>명령어를 통해 적용되는 파이썬 버전을 확인할 수 있습니다.</p>
<p><strong>이때 생성되는 파일:</strong></p>
<ul>
<li><code>.venv</code> :가상 환경 폴더. 파이썬 인터프리터와 라이브러리 저장</li>
<li><code>uv.lock</code> : 실제로 설치된 패키지들의 모든 정보가 기록</li>
</ul>
<h2 id="4-사용법">4. 사용법</h2>
<h3 id="패키지-관리">패키지 관리</h3>
<ol>
<li><p>설치</p>
<pre><code># pyproject.toml, uv.lock에 기록
uv add &lt;라이브러리 명&gt;</code></pre><p>또는 ,</p>
<pre><code># 기록 없이 임시로 설치할 라이브러리
uv pip install &lt;라이브러리 명&gt;</code></pre></li>
<li><p>삭제</p>
<pre><code>uv remove &lt;라이브러리 명&gt;</code></pre></li>
<li><p>패키지 환경 구성</p>
<pre><code>uv sync</code></pre><p><code>pyproject.toml</code> 파일과 <code>uv.lock</code> 파일만 있다면 위의 명령어를 이용하여 완벽하게 동일한 가상 환경 폴더를 생성해 낼 수 있습니다.</p>
</li>
</ol>
<p>다만 <code>uv run &lt;파일 명.py&gt;</code> 명령어를 입력하면 자동으로 <code>uv sync</code>를 실행시켜줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/bae71d20-34d3-43b4-a23f-654791881b5b/image.png" alt=""></p>
<h2 id="마무리하며">마무리하며</h2>
<p>이제 더 이상 복잡한 가상 환경 활성화나 의존성 꼬임 문제로 시간을 낭비할 필요가 없습니다. </p>
<p>uv run 명령어 하나로 언제 어디서든 동일한 개발 환경을 재현할 수 있게 되었습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #15 : CMake Tools로 C/C++ 빌드 환경 설정하기]]></title>
            <link>https://velog.io/@young-0320/c-%EB%B9%8C%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@young-0320/c-%EB%B9%8C%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 14 Feb 2026 16:11:18 GMT</pubDate>
            <description><![CDATA[<p>일관된 C/C++ 빌드 세팅을 유지하는 것이 가장 중요합니다.
이번 포스팅에서는 CMakePresets를 활용한 환경 표준화부터 사이드바 제어를 이용해 VS Code 기반의 CMake 워크플로우 구축 방법을 단계별로 알아보겠습니다.</p>
<h2 id="1-개발-환경-표준화">1. 개발 환경 표준화</h2>
<p>VS Code에서 CMake를 사용할 때 가장 먼저 해야 할 일은 빌드 환경을 고정하는 것입니다. 
<code>CMakePresets.json</code>을 프로젝트 최상위 경로에 작성하면, 팀원 간의 환경 차이나 Windows/Ubuntu 등 운영체제 변환 시에도 동일한 빌드 폴더 구조와 컴파일러 설정을 유지할 수 있습니다.</p>
<p>이 파일은 크게 빌드 환경을 세팅하는 <strong>configurePresets</strong>와 실제 컴파일 대상을 지정하는 <strong>buildPresets</strong> 두 부분으로 나뉩니다.</p>
<h3 id="a-구성-사전-설정-configurepresets">A. 구성 사전 설정 (configurePresets)</h3>
<p>CMake가 빌드 파일(Makefile 등)을 어디에, 어떤 설정으로 생성할지 정의합니다. 
VS Code가 기본으로 생성해 주는 더미 템플릿은 삭제하고, 해당 프로젝트에 맞는 구조로 직접 작성합니다.</p>
<pre><code>{
    &quot;version&quot;: 3,
    &quot;configurePresets&quot;: [
        {
            &quot;name&quot;: &quot;default-build&quot;,
            &quot;displayName&quot;: &quot;Default Build (Project Root)&quot;,
            &quot;description&quot;: &quot;프로젝트 최상위 루트에 build 폴더를 생성하는 표준 방식&quot;,
            &quot;binaryDir&quot;: &quot;${sourceDir}/build&quot;,
            &quot;cacheVariables&quot;: {
                &quot;CMAKE_BUILD_TYPE&quot;: &quot;Debug&quot;,
                &quot;BUILD_FIR_TESTS&quot;: &quot;ON&quot;
            }
        },
        {
            &quot;name&quot;: &quot;fir-1d-custom&quot;,
            &quot;displayName&quot;: &quot;1D FIR Filter (Sub-folder Build)&quot;,
            &quot;description&quot;: &quot;FIR 필터 시뮬레이션 관련 빌드 파일만 하위 폴더로 격리&quot;,
            &quot;binaryDir&quot;: &quot;${sourceDir}/fir_1d/sim/build&quot;,
            &quot;cacheVariables&quot;: {
                &quot;CMAKE_BUILD_TYPE&quot;: &quot;Debug&quot;,
                &quot;BUILD_FIR_TESTS&quot;: &quot;ON&quot;
            }
        }
    ],</code></pre><p><strong>default-build preset:</strong> 프로젝트 최상위 루트에 build 폴더를 만들고 모든 구성 파일을 이곳에 저장하는 가장 표준적인 방식입니다.</p>
<p><strong>fir-1d-custom preset:</strong> 특정 하위 모듈(1D FIR 필터)의 시뮬레이션 및 검증만을 위해 독립적인 build 폴더를 구축하는 맞춤형 설정입니다.</p>
<ul>
<li>binaryDir: 빌드 산출물이 저장될 경로입니다. ${sourceDir} 매크로를 사용하여 절대 경로 하드코딩으로 인한 에러를 방지합니다.</li>
</ul>
<p>전체 프로젝트를 제어하는 default-build와 달리, fir-1d-custom은 빌드 폴더를 fir_1d/sim/build로 깊게 지정하여 다른 모듈의 목적 파일(.o)과 섞이지 않도록 관리합니다.</p>
<h3 id="b-빌드-사전-설정buildpresets">B. 빌드 사전 설정(buildPresets)</h3>
<p>앞서 만든 <code>configurePreset</code>를 바탕으로, 소스 코드 중 어떤 타겟(Target)을 컴파일할지 구체적인 대상을 지정합니다.</p>
<pre><code>&quot;buildPresets&quot;: [
        {
            &quot;name&quot;: &quot;build-all-default&quot;,
            &quot;configurePreset&quot;: &quot;default-build&quot;
        },
        {
            &quot;name&quot;: &quot;build-test-fir-1d-default&quot;,
            &quot;configurePreset&quot;: &quot;default-build&quot;,
            &quot;targets&quot;: [
                &quot;test_fir_1d&quot;
            ]
        },
        {
            &quot;name&quot;: &quot;build-test-fir-1d-custom&quot;,
            &quot;configurePreset&quot;: &quot;fir-1d-custom&quot;,
            &quot;targets&quot;: [
                &quot;test_fir_1d&quot;
            ]
        }
    ]
}</code></pre><p><strong>build-all-default:</strong> default-build 프리셋을 사용합니다. 기본 디폴트 설정입니다.</p>
<p><strong>build-test-fir-1d-default:</strong> default-build 폴더를 사용하되 targets를 test_fir_1d로 명시합니다. 해당 타겟만 빌드하므로, 컴파일 및 디버깅 속도가 상승합니다.</p>
<p><strong>build-test-fir-1d-custom:</strong> fir-1d-custom 프리셋을 사용하여 격리된 빌드 디렉토리를 사용합니다. 마찬가지로 targets 명시를 통해 빠른 빌드와 검증 속도를 확보합니다.</p>
<h3 id="c-프리셋preset-적용-및-타겟-제어">C. 프리셋(Preset) 적용 및 타겟 제어</h3>
<p>작성한 <code>CMakePresets.json</code>을 VS Code에 실제로 적용하는 단계입니다. 
CMake는 <strong>구성(Configure)</strong>과 <strong>빌드(Build)</strong>를 철저히 분리하여 관리하므로, 두 단계를 순서대로 진행해야 합니다.</p>
<h4 id="1-configure-preset-선택-빌드-환경-선택">1. Configure Preset 선택 (빌드 환경 선택)</h4>
<p><code>Ctrl + Shift + P</code>를 눌러 명령 팔레트를 열고 <code>CMake: Select Configure Preset</code>을 실행합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/3d3ae852-9b7e-4888-aa81-896efef637f8/image.png" alt=""></p>
<p>이 단계는 &quot;어떤 폴더에서, 어떤 컴파일러로 빌드 환경을 구성할 것인가?&quot;를 결정하는 단계입니다.</p>
<h4 id="2-build-preset-선택-컴파일-타겟-지정">2. Build Preset 선택 (컴파일 타겟 지정)</h4>
<p>환경 구성이 끝났다면 <code>CMake: Select Build Preset</code>을 실행하여 실제 컴파일 대상을 고릅니다.</p>
<p>하나의 구성 항목 중 어떤 종류의 빌드 명령을 수행할지 선택하는 단계입니다. 즉, 어떤 타겟을 어떤 방식으로 컴파일할지 결정하는 설정입니다.</p>
<p><strong>default preset (Default Build 선택 시)</strong>
<img src="https://velog.velcdn.com/images/young-0320/post/28860552-ce57-4fdf-841f-cc1adce5ef02/image.png" alt=""></p>
<p><strong>custom preset (1D FIR Filter 선택 시)</strong>
<img src="https://velog.velcdn.com/images/young-0320/post/11e10819-db5d-42c2-a363-99a10081e97d/image.png" alt=""></p>
<p>원하는 프리셋을 선택하여 적용합니다.</p>
<h2 id="2-cmake-tools-사이드바-활용하기">2. CMake Tools 사이드바 활용하기</h2>
<p>VS Code 좌측의 삼각형 아이콘(CMake)을 누르면 나타나는 사이드바의 핵심 기능을 정리합니다.</p>
<h3 id="a-프로젝트-상태-확인">A. 프로젝트 상태 확인</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/e61bfdad-17fa-458e-8faa-cb724f5e8f29/image.png" alt="">
현재 프로젝트의 상태를 시각적으로 보여줍니다. 
현재 어떤 타겟이 &#39;빌드&#39; 및 &#39;실행&#39; 대상으로 설정되어 있는지 실시간으로 확인할 수 있습니다.</p>
<h3 id="b-타겟별-개별-제어build-clean-debug">B. 타겟별 개별 제어(Build, Clean, Debug)</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/21c2646d-d670-40e0-81cb-c2aa445c9655/image.png" alt=""></p>
<p>현재 선택된 타겟을 나타냅니다. 리스트의 특정 타겟에 마우스를 올리면 작은 아이콘들이 나타납니다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/baac60ed-fa88-410f-a5cd-6895e1fd0408/image.png" alt="">
빌드 타겟에 마우스를 올리면 나타나는 작은 아이콘을 통해 빌드 대상을 변경할 수 있습니다. 
<img src="https://velog.velcdn.com/images/young-0320/post/979d6b11-7cff-42e0-9476-d64e60079993/image.png" alt=""></p>
<p>마찬가지로 해당 아이콘으로 실행 타겟도 변경할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/c7a6520a-0cbc-4ebc-88cc-211d7977aa7b/image.png" alt=""></p>
<p>사이드바에서 설정된 타겟은 하단 상태 표시줄의 망치 및 화살표 아이콘과 즉시 동기화됩니다. 덕분에 한 번 타겟을 잡아두면, 이후에는 단축키나 하단 버튼 클릭만으로 반복적인 컴파일과 실행을 빠르게 수행할 수 있습니다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>이번 글에서는 CMakePresets로 빌드 환경을 표준화하고, VS Code CMake Tools에서 Configure와 Build를 분리해 운영하는 흐름을 정리했습니다.
이렇게 세팅해두면 Windows와 Ubuntu를 오가더라도 동일한 절차로 빌드와 디버깅을 반복할 수 있습니다.</p>
<p>다음 글에서는 generator/toolchain/컴파일러를 preset에 명시적으로 고정하고, CTest preset까지 연결해 자동 검증 루틴을 완성해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #12 : VS Code의 생산성을 높여주는 Extensions]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-12-VS-Code%EC%9D%98-%EC%83%9D%EC%82%B0%EC%84%B1%EC%9D%84-%EB%86%92%EC%97%AC%EC%A3%BC%EB%8A%94-Extensions</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-12-VS-Code%EC%9D%98-%EC%83%9D%EC%82%B0%EC%84%B1%EC%9D%84-%EB%86%92%EC%97%AC%EC%A3%BC%EB%8A%94-Extensions</guid>
            <pubDate>Thu, 12 Feb 2026 05:40:33 GMT</pubDate>
            <description><![CDATA[<p>제대로 된 익스텐션 몇 개만 설치해도 코딩 생산성이 눈에 띄게 달라집니다.
이 글에서는 실제로 사용하면서 작업 효율을 크게 높여준 익스텐션들을 정리해봤습니다.</p>
<h2 id="1-gitlens">1. GitLens</h2>
<p>Git 커밋 기록을 누가 언제 어떤 이유로 수정했는지를 코드 위에서 바로 보여주는 익스텐션입니다.
<img src="https://velog.velcdn.com/images/young-0320/post/efd135e3-59a6-4338-beb6-7993a114044d/image.png" alt="">
이런 식으로 코드 라인 끝에 회색 글씨로 누가, 언제, 왜 이 코드를 작성했는지를 보여줍니다.</p>
<p>해당 코드 라인에 마우스를 올리면 상세한 커밋 정보가 팝업으로 나타납니다.
<img src="https://velog.velcdn.com/images/young-0320/post/9d214d49-15ae-406f-901e-525a68afac72/image.png" alt=""></p>
<p>사이드바의 GitLens 아이콘을 클릭하면</p>
<ul>
<li>현재 파일의 모든 수정 기록</li>
<li>누가 몇 번 수정했는지</li>
<li>각 커밋에서 어떤 내용이 바뀌었는지</li>
</ul>
<p>등을 시각적으로 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/9356427d-b7f1-496f-a355-39ea21d89175/image.png" alt=""></p>
<h2 id="2-remote---tunnels-remote-explorer">2. Remote - Tunnels, Remote Explorer</h2>
<p><img src="https://velog.velcdn.com/images/young-0320/post/30e4b818-6b81-4a9c-8dac-14d79060810b/image.png" alt=""></p>
<p>인터넷만 있으면 어디서든 다른 컴퓨터의 VSCode에 접속할 수 있게 해주는 익스텐션입니다.</p>
<p>일반적인 원격 접속 방식은 방화벽 설정, 포트포워딩 등 복잡한 네트워크 설정이 필요하지만, Remote - Tunnels는 Microsoft 서버를 중간다리로 활용하는 방식이라 이런 번거로운 과정이 전혀 필요 없습니다.</p>
<p>집 데스크탑에 개발 환경을 구축해두고, 카페에서 노트북으로 접속해 마치 데스크탑 앞에 앉아있는 것처럼 작업할 수 있습니다. 노트북의 제한된 성능이 아닌 데스크탑의 강력한 성능을 그대로 활용할 수 있죠.</p>
<p><strong>필수 설정 명령어</strong></p>
<pre><code>code tunnel service install</code></pre><p>데스크탑을 원격 서버로 등록하는 명령어로, 
이걸 실행해야 데스크탑이 백그라운드에서 항상 접속 대기 상태가 됩니다.
컴퓨터를 켜두기만 하면 언제든지 원격 접속이 가능해 집니다.</p>
<p><strong>Remote Explorer</strong>
Remote - Tunnels의 사이드바 UI 역할을 하는 익스텐션입니다.</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li>등록된 원격 컴퓨터 목록 표시</li>
<li>클릭 한 번으로 원격 접속</li>
<li>연결 상태 확인</li>
<li>SSH, 터널, 컨테이너 등 다양한 원격 환경 관리</li>
</ul>
<p><img src="https://velog.velcdn.com/images/young-0320/post/5be0f0d8-5931-4775-984e-4b60737d9d03/image.png" alt="">
해당 이미지와 같이 사이드바에 UI가 나타나고 Microsoft나 GitHub에 등록된 터널로 접속할 수 있습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/ed2a6360-2b45-4279-a171-68e84a01a4e3/image.png" alt="">
하단 상태 표시줄 가장 왼쪽 버튼을 클릭하여 터널을 선택할 수도 있습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/f7bc299f-759a-435c-9aef-10bead391e5a/image.png" alt=""></p>
<h2 id="3-markdown-extensions">3. Markdown extensions</h2>
<h3 id="a-markdown-all-in-one">A. Markdown All in one</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/bd4a9d58-6280-4dd2-ab70-94eccd4e2736/image.png" alt="">
마크다운 작성을 위한 도구 모음입니다.</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li>키보드 단축키로 빠른 서식 적용 (굵게, 기울임, 취소선 등)</li>
<li>자동 목차(TOC) 생성 및 업데이트</li>
<li>자동 리스트 넘버링</li>
<li>표 자동 정렬 및 포맷팅</li>
<li>섹션 번호 자동 매기기</li>
<li>미리보기 스크롤 동기화<h3 id="b-markdown-preview-enhanced">B. Markdown Preview Enhanced</h3>
<img src="https://velog.velcdn.com/images/young-0320/post/c8d1bb2e-d1b2-40e6-97e3-40d5ef48c7c7/image.png" alt="">
강력한 미리보기 및 내보내기 기능을 제공하는 익스텐션입니다.</li>
</ul>
<p><strong>주요 기능:</strong></p>
<ul>
<li>실시간 미리보기 (수학 수식, 다이어그램 지원)</li>
<li>Mermaid, PlantUML 등 다이어그램 렌더링</li>
<li>코드 청크 실행 기능</li>
<li>PDF, HTML, PNG 등 다양한 포맷으로 내보내기</li>
<li>프레젠테이션 모드</li>
<li>커스터마이징 가능한 CSS 스타일</li>
</ul>
<h3 id="c-markdown-pdf">C. Markdown PDF</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/bd5bd25c-f974-4837-a1fa-a9b2209b5e0a/image.png" alt=""></p>
<p>마크다운 파일을 PDF로 간편하게 변환하는 익스텐션입니다.</p>
<h2 id="4-office-viewer">4. Office Viewer</h2>
<p><img src="https://velog.velcdn.com/images/young-0320/post/47b88485-26b9-4e7a-bee9-1bf117c3bed2/image.png" alt=""></p>
<p>Microsoft Office 파일을 VSCode 내에서 직접 열람할 수 있는 익스텐션입니다.</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li>Word(.docx), Excel(.xlsx), PowerPoint(.pptx) 파일을 VSCode에서 바로 보기</li>
<li>PDF 파일도 함께 지원</li>
<li>별도의 Office 프로그램 실행 없이 파일 내용 확인 가능</li>
<li>편집 불가</li>
</ul>
<h2 id="5-hex-editor">5. Hex Editor</h2>
<p><img src="https://velog.velcdn.com/images/young-0320/post/71945ca5-30ce-4798-b4a1-8e569eedde71/image.png" alt="">
파일의 바이너리 데이터를 16진수(Hexadecimal)로 직접 보고 편집할 수 있는 익스텐션입니다.</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li>모든 파일을 바이트 단위로 16진수로 표시</li>
<li>바이너리 데이터 직접 편집 가능</li>
<li>ASCII/UTF-8 텍스트와 16진수 동시 표시</li>
<li>파일 구조 분석 (이미지, 실행파일 등)</li>
<li>대용량 파일도 효율적으로 처리</li>
<li>특정 바이트 검색 및 이동</li>
</ul>
<h2 id="6-가독성-향상">6. 가독성 향상</h2>
<h3 id="a-material-icon-theme">A. Material Icon Theme</h3>
<p>파일과 폴더에 직관적이고 예쁜 아이콘을 적용해주는 테마입니다.</p>
<p><strong>세부 설정 방법</strong>
기본 아이콘만으로도 충분하지만, 특정 폴더에 원하는 아이콘을 직접 지정할 수도 있습니다.</p>
<h4 id="1-설정-파일-열기">1. 설정 파일 열기</h4>
<p><code>F1</code> 또는 <code>Ctrl+Shift+P</code>를 눌러 명령 팔레트를 연 후, Preferences: Open User Settings (JSON)을 입력하고 선택합니다.</p>
<p>프로젝트별로 다르게 설정하고 싶다면 .vscode/settings.json 파일을 사용하세요.</p>
<h4 id="2-폴더-아이콘-매핑-추가">2. 폴더 아이콘 매핑 추가</h4>
<p>아래 코드를 <code>settings.json</code> 파일 안에 추가합니다. 이미 material-icon-theme.folders.associations 항목이 있다면, 그 안에 원하는 항목만 추가하면 됩니다.</p>
<pre><code># &quot;rtl&quot; 이름의 폴더 아이콘을 &quot;core&quot; 폴더 아이콘과 매칭
json&quot;material-icon-theme.folders.associations&quot;: {
    &quot;rtl&quot;: &quot;core&quot;,
    &quot;components&quot;: &quot;component&quot;,
    &quot;utils&quot;: &quot;helper&quot;
}</code></pre><h4 id="3-저장-및-확인">3. 저장 및 확인</h4>
<p>Ctrl+S로 저장하면 즉시 아이콘이 변경된 것을 확인할 수 있습니다.</p>
<ul>
<li>설정 적용 전
<img src="https://velog.velcdn.com/images/young-0320/post/91fa47cd-eea8-4a95-be8d-229ef1484276/image.png" alt=""></li>
<li>설정 적용 후
<img src="https://velog.velcdn.com/images/young-0320/post/cae658a4-7c48-4fea-953e-ca0b4cbaf32a/image.png" alt=""></li>
</ul>
<h3 id="b-verilog-hdlsystemverilogbluespec-systemverilog">B. Verilog-HDL/SystemVerilog/Bluespec SystemVerilog</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/17012e05-4b02-4fd4-839a-df37aea01f23/image.png" alt="">
하드웨어 설계 언어(HDL)를 지원하는 VS Code 익스텐션입니다.</p>
<p>다만 이 익스텐션은 린터나 포매터 그 자체가 아니라, 외부 엔진을 VS Code에 연결해주는 프론트엔드(대시보드) 역할을 합니다. 따라서 익스텐션만 설치한다고 해서 자동 포맷이나 의미 있는 진단이 바로 동작하지는 않습니다. </p>
<p>제대로 활용하려면 프로젝트에 맞는 <strong>린터(Linter)</strong>와 <strong>포매터(Formatter)</strong>를 별도로 설치한 뒤, VS Code가 해당 바이너리를 정확히 호출할 수 있도록 경로와 옵션을 직접 설정해줘야 합니다.</p>
<ul>
<li><p><strong>린터(Linter):</strong>
소스 코드를 “컴파일러처럼” 읽어서 문법 오류, 잠재 버그를 잡아주는 검사기입니다.</p>
</li>
<li><p><strong>포매터(Formatter):</strong>
코드의 기능은 바꾸지 않고 들여쓰기, 개행, 정렬, 공백 규칙같은 포맷을 맞춰주는 도구입니다.</p>
</li>
</ul>
<p><strong>세부 설정 방법</strong>
린터/포매터는 Verible을 선택했습니다. </p>
<h4 id="1--github-릴리즈-페이지에서-다운로드">1.  GitHub 릴리즈 페이지에서 다운로드</h4>
<p><a href="https://github.com/chipsalliance/verible/releases">https://github.com/chipsalliance/verible/releases</a>
<img src="https://velog.velcdn.com/images/young-0320/post/72cb05c7-18f6-4a46-825b-25b1c6ca7e0b/image.png" alt=""></p>
<h4 id="2-릴리즈-파일-다운로드-및-압축-해제">2. 릴리즈 파일 다운로드 및 압축 해제</h4>
<p>Ubuntu 22.04 x86_64 환경에 맞는 tar.gz 파일을 선택해 다운로드합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/484f9fef-d7f5-4630-8f53-917e6889b127/image.png" alt=""></p>
<p>다운로드가 완료되면 ~/Downloads 디렉토리에서 압축을 해제합니다.</p>
<pre><code># 압축 해제
cd ~/Downloads
tar -xzf verible-*.tar.gz</code></pre><h4 id="3-시스템-경로에-설치">3. 시스템 경로에 설치</h4>
<p>압축 해제된 파일을 시스템 전역에서 사용할 수 있도록 <code>/opt/verible</code> 디렉토리를 생성하고 복사합니다.</p>
<pre><code>sudo mkdir -p /opt/verible
sudo rsync -a --delete verible-*/ /opt/verible/</code></pre><p><code>rsync -a</code> 옵션은 파일의 권한·타임스탬프 등 속성을 그대로 유지하며 복사하고, <code>--delete</code>는 대상 디렉토리에 남아 있는 불필요한 구버전 파일을 자동으로 제거합니다. 
덕분에 향후 버전을 업그레이드할 때도 동일한 명령어를 재사용할 수 있습니다.</p>
<h4 id="4-심볼릭-링크-설정">4. 심볼릭 링크 설정</h4>
<p><code>/usr/local/bin</code>에 심볼릭 링크를 생성하여 터미널 어디서든 Verible 명령어를 바로 호출할 수 있도록 합니다.</p>
<pre><code>sudo ln -sf /opt/verible/bin/verible-verilog-format /usr/local/bin/verible-verilog-format
sudo ln -sf /opt/verible/bin/verible-verilog-lint   /usr/local/bin/verible-verilog-lint
sudo ln -sf /opt/verible/bin/verible-verilog-ls     /usr/local/bin/verible-verilog-ls</code></pre><p>-s는 심볼릭 링크 생성, -f는 동일한 이름의 링크가 이미 존재할 경우 덮어쓰기를 의미합니다. 
버전 업그레이드 시 <code>/opt/verible</code> 내용만 교체하면 링크를 다시 만들 필요 없이 자동으로 새 버전을 가리키게 됩니다.</p>
<h4 id="5-설치-확인">5. 설치 확인</h4>
<p>아래 명령어로 설치가 정상적으로 완료되었는지 검증합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/b936c357-1fb8-49c0-a835-6ff70cfcfed2/image.png" alt=""></p>
<pre><code>which verible-verilog-format verible-verilog-lint verible-verilog-ls
readlink -f /usr/local/bin/verible-verilog-format
readlink -f /usr/local/bin/verible-verilog-lint
verible-verilog-format --version
verible-verilog-lint --version</code></pre><p>which 명령이 <code>/usr/local/bin/...</code> 경로를 반환하고, readlink -f가 <code>/opt/verible/bin/...</code>로 실제 바이너리 경로를 추적하며, --version이 설치된 버전 문자열을 출력하면 정상 설치가 완료된 것입니다.</p>
<h4 id="6-vs-code에서-세부-설정">6. VS Code에서 세부 설정</h4>
<p><strong>6.1 Formatting 설정</strong>
<code>Ctrl + ,</code>로 익스텐션 설정 창을 열어 아래 항목들을 순서대로 설정합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/adf83d33-59df-489d-8602-c9d0f8f894cd/image.png" alt=""></p>
<p><strong>6.1.1 System Verilog(.sv) 포매터를 지정</strong>
<code>Verilog &gt; Formatting &gt; SystemVerilog: Formatter → verible-verilog-format</code>
<img src="https://velog.velcdn.com/images/young-0320/post/be7eb491-f47f-493f-a1e6-a049c8ec74e7/image.png" alt=""></p>
<p><strong>6.1.2 바이너리 경로 지정</strong>
<code>Verilog &gt; Formatting &gt; Verible Verilog Formatter: Path</code>
<img src="https://velog.velcdn.com/images/young-0320/post/7f09e58d-f61d-4381-8873-2bc29231ef10/image.png" alt=""></p>
<p>경로란에는 <code>verible-verilog-format</code>을 입력합니다. 
만약 이 방식으로 동작하지 않는다면 <code>/usr/local/bin/verible-verilog-format</code>과 같이 절대 경로를 직접 입력합니다.</p>
<p><strong>6.1.3 Verilog HDL(.v) 포매터 지정</strong>
<code>Verilog &gt; Formatting &gt; Verilog HDL: Formatter → verible-verilog-format</code>
<img src="https://velog.velcdn.com/images/young-0320/post/4be5d1dd-63b6-4790-a250-ccda2dc593bd/image.png" alt=""></p>
<p><strong>6.2 linting 설정</strong>
<strong>6.2.1 linter 지정</strong>
<code>Verilog &gt; Linting: Linter → verible-verilog-lint</code>
<img src="https://velog.velcdn.com/images/young-0320/post/73ae8d5e-8dde-4cc5-a1a1-f0b738b8ee83/image.png" alt=""></p>
<p><strong>6.3 Format On Save 활성화</strong>
<code>Ctrl+S</code> 저장 시 자동으로 포매팅이 실행되도록 VS Code 기본 설정에서 <code>Editor: Format On Save</code>(<code>editor.formatOnSave</code>)를 활성화합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/51f3312e-4c8e-463f-943d-e959503dbb10/image.png" alt=""></p>
<h3 id="c-mips-support-rainbow-csv">C. MIPS Support, Rainbow CSV</h3>
<p><strong>MIPS Support:</strong>
MIPS 어셈블리 언어를 위한 익스텐션입니다.</p>
<p><strong>Rainbow CSV:</strong>
CSV 파일을 컬럼별로 다른 색으로 표시해주는 익스텐션입니다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>VSCode의 생산성을 높여주는 주요 익스텐션들을 소개했습니다.</p>
<p>처음부터 익스텐션들을 전부 설치할 필요는 없고, 자신의 작업 환경에 맞춰 필요한 것들만 골라 사용하면 됩니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #13 : Ubuntu에서 생산성 극대화하기]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-12-Ubuntu%EC%97%90%EC%84%9C-%EC%83%9D%EC%82%B0%EC%84%B1-%EA%B7%B9%EB%8C%80%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-12-Ubuntu%EC%97%90%EC%84%9C-%EC%83%9D%EC%82%B0%EC%84%B1-%EA%B7%B9%EB%8C%80%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 09 Feb 2026 11:48:40 GMT</pubDate>
            <description><![CDATA[<p>우분투 24.04 환경에서 유용하게 사용하고 있는 도구를 소개합니다. </p>
<p>Rust와 Zig 기반의 고성능 설계로 하드웨어 리소스를 효율적으로 사용하는 Ghostty, 그리고 네트워크만 연결되어 있다면 어떤 기기와도 즉시 연결되는 LocalSend입니다.</p>
<h2 id="1-터미널-선택">1. 터미널 선택</h2>
<p><strong>고성능 중심의 터미널</strong> </p>
<ul>
<li>Ghostty: 균형 잡힌 고성능 터미널</li>
<li>Alacritty: 세상에서 가장 빠른 터미널을 표방</li>
</ul>
<p><strong>사용자 편의성 및 AI 통합 중심의 터미널</strong></p>
<ul>
<li>Warp: AI 에이전트가 내장된 Rust 기반 터미널</li>
<li>Wave: 오픈 소스 기반의 현대적 터미널</li>
</ul>
<h3 id="선택한-터미널--ghostty">선택한 터미널 : Ghostty</h3>
<p>텍스트 출력이 많은 작업을 하고 있고 렌더링 레이턴시가 적은 것을 선호하기에 해당 터미널을 선택했습니다. 또한 작업 환경이 원격 데스크탑에 접속하는 일이 잦기에 미세한 이질감을 줄여준다고 생각했습니다.</p>
<p><strong>개발 환경 요약 : Ghostty + 5.3 Codex + 최소한의 도구</strong></p>
<h3 id="ghostty-설치">Ghostty 설치</h3>
<h4 id="ghostty-다운로드-가이드-확인">Ghostty 다운로드 가이드 확인</h4>
<p>공식 홈페이지 : <a href="https://ghostty.org/docs">https://ghostty.org/docs</a></p>
<p><strong>다운로드 방식</strong></p>
<ul>
<li>소스 코드를 직접 zig로 빌드하여 사용</li>
<li>배포된 빌드 파일을 설치하여 사용</li>
</ul>
<p>일반 사용자에게는 소스 코드를 직접 빌드하는 것이 권장되지 않기에 바이너리 파일을 설치하겠습니다.</p>
<p>홈페이지를 확인해 보면 우분투 24.04는 공식 바이너리 파일이 없어 GitHub 커뮤니티에서 만든 빌드 파일을 설치해야 합니다.</p>
<p>이미지에서 볼 수 있듯 공식 릴리즈가 아니므로 업데이트 시 호환성 이슈가 발생할 수 있습니다. 
<img src="https://velog.velcdn.com/images/young-0320/post/43b8b5aa-a769-4996-8064-7ce910ccfa1d/image.png" alt="">
해당 깃허브 페이지에 방문하여 README를 읽어 설치 방법을 확인합니다.
<a href="https://github.com/mkasberg/ghostty-ubuntu">https://github.com/mkasberg/ghostty-ubuntu</a>
<img src="https://velog.velcdn.com/images/young-0320/post/29f56c8b-cd7f-4f32-8be1-3831f2fd6b55/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/42f08eec-97d5-4db3-8411-68cee40fa448/image.png" alt="">
Ubuntu 24.04 + x86_64 버전에 맞는 링크를 클릭해 설치해 줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/3de2b1a3-ca51-4977-8a1f-15df858fab1b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/22ffe2dd-3c75-47c3-8bc3-cef8b88345ed/image.png" alt=""></p>
<pre><code>sudo dpkg -i ghostty_1.2.3-0.ppa1_amd64_24.04.deb
sudo apt -f install</code></pre><p>해당 명령어를 이용하여 설치를 완료해 줍니다.</p>
<p><strong>설치 확인</strong>
<img src="https://velog.velcdn.com/images/young-0320/post/09af2f36-ca6e-458c-9107-83b8aea1782a/image.png" alt="">
성공!</p>
<p>*<em>실행 *</em></p>
<pre><code>ghostty</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/f965c2af-87a7-40fd-b971-62a475b5df14/image.png" alt=""></p>
<h2 id="2-localsend">2. LocalSend</h2>
<p>에어드롭의 크로스플랫폼 버전 프로그램입니다.</p>
<pre><code># 설치
sudo snap install localsend

# 실행
localsend</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/9e9fbd5f-653c-4342-8bde-9a4436c8d18e/image.png" alt=""></p>
<h3 id="사용-방법">사용 방법</h3>
<h4 id="1-우분투에서-localsend-실행">1. 우분투에서 LocalSend 실행</h4>
<ul>
<li>앱 메뉴에서 &quot;LocalSend&quot; 검색해서 실행
-또는 터미널에서 localsend 입력</li>
</ul>
<h4 id="2-아이패드아이폰에-앱-설치">2. 아이패드/아이폰에 앱 설치</h4>
<ul>
<li>App Store에서 &quot;LocalSend&quot; 검색 후 설치</li>
<li>안드로이드는 Play Store에서 설치</li>
</ul>
<h4 id="3-같은-wifi에-연결">3. 같은 WiFi에 연결</h4>
<ul>
<li>우분투와 아이패드가 같은 WiFi에 연결되어 있어야 함</li>
</ul>
<h4 id="4-파일-전송">4. 파일 전송</h4>
<ul>
<li><strong>아이패드 → 우분투:</strong></li>
</ul>
<ol>
<li>아이패드에서 LocalSend 앱 실행</li>
<li>근처 기기에서 우분투 컴퓨터가 보임</li>
<li>보낼 파일 선택 → 우분투 선택 → 전송</li>
<li>우분투에서 수락하면 다운로드됨 (기본 위치: ~/Downloads)</li>
</ol>
<ul>
<li><strong>우분투 → 아이패드:</strong></li>
</ul>
<ol>
<li>우분투 LocalSend에서 파일 선택 또는 드래그 앤 드롭</li>
<li>근처 기기 목록에서 아이패드 선택</li>
<li>아이패드에서 수락</li>
</ol>
<h2 id="마무리하며">마무리하며</h2>
<p><strong>기본 터미널과 카카오톡 전송을 졸업하며</strong>
이제 세련된 고성능 터미널과 OS 간 장벽 없는 파일 전송 환경이 갖춰졌습니다. 
불필요한 레이턴시와 연결의 번거로움을 덜어낸 이 미니멀한 셋업이, 앞으로의 개발 몰입도를 한 층 높여줄 것이라 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #11 : PSpice 설치 가이드]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-11-PSpice-%EC%B2%98%EC%9D%8C-%EC%84%A4%EC%B9%98%ED%95%98%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%84-%EC%9C%84%ED%95%9C-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-11-PSpice-%EC%B2%98%EC%9D%8C-%EC%84%A4%EC%B9%98%ED%95%98%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%84-%EC%9C%84%ED%95%9C-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Mon, 09 Feb 2026 11:20:05 GMT</pubDate>
            <description><![CDATA[<p>이번 포스트에서는 Windows에 TEXAS INSTRUMENTS에서 제공하는 무료 버전 PSpice를 다운로드해 보겠습니다.</p>
<p>PSpice는 Cadence에서 제공하는 회로 시뮬레이션 도구입니다.</p>
<h3 id="윈도우-사용자-이름-확인">윈도우 사용자 이름 확인</h3>
<p>PSpice 설치 전 윈도우 사용자 이름이 영문인지와 윈도우 버전을 확인해야 합니다.</p>
<ol>
<li><p>Win + R 키를 누른 뒤 <code>cmd</code>를 입력하고 엔터를 칩니다.</p>
</li>
<li><p>입력창에 <code>whoami</code>를 입력합니다.</p>
</li>
<li><p>출력되는 이름이 한글이라면 문제가 발생할 확률이 높고, 영문이라면 안전합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/bb61cb06-6f11-43c3-a118-ebd02128a4de/image.png" alt=""></p>
</li>
</ol>
<p><strong>만약 사용자 이름이 한글인 경우</strong>:
기존 계정 이름을 바꾸는 것은 레지스트리 충돌 위험이 큽니다. 일반적으로 활용되는 해결 방법은 <strong>&#39;설정 &gt; 계정 &gt; 다른 사용자&#39;</strong>에서 영문 이름으로 된 새로운 로컬 계정을 하나 생성하여 툴 전용 계정으로 사용하는 것입니다.</p>
<h3 id="윈도우-버전-확인">윈도우 버전 확인</h3>
<ol>
<li><p>Win + R 키를 누른 뒤 <code>cmd</code>를 입력하고 엔터를 칩니다.</p>
</li>
<li><p>입력창에 <code>winver</code>를 입력합니다.</p>
</li>
<li><p>팝업창에 해당 Windows 버전(예: 22H2, 23H2)과 빌드 번호를 즉시 확인할 수 있습니다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/young-0320/post/880b6244-eb50-4ab7-83ca-ff4e7697b531/image.png" alt=""></p>
<h2 id="pspice-for-ti-설치-가이드">PSpice for TI 설치 가이드</h2>
<h3 id="1-texas-instruments의-공식-홈페이지에-방문하여-request-버튼을-누릅니다">1. TEXAS INSTRUMENTS의 공식 홈페이지에 방문하여 Request 버튼을 누릅니다.</h3>
<p>공식 홈페이지 <a href="https://www.ti.com/tool/PSPICE-FOR-TI">https://www.ti.com/tool/PSPICE-FOR-TI</a>
<img src="https://velog.velcdn.com/images/young-0320/post/3e9a7924-6448-4cbc-a909-3ced3d752669/image.png" alt=""></p>
<h3 id="2-회원가입을-진행해줍니다">2. 회원가입을 진행해줍니다.</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/1627e9f4-ae0d-45d6-9f2b-1655b6d28333/image.png" alt=""></p>
<h3 id="3-로그인-후-해당-정보들을-입력해줍니다">3. 로그인 후 해당 정보들을 입력해줍니다.</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/c6a7243a-4d59-4ff6-9fff-37298d5fc19b/image.png" alt=""></p>
<h4 id="3-1-create-an-address">3-1. Create an address</h4>
<p>주소는 경희대학교 국제캠퍼스를 입력하였습니다. 
<strong>Company name</strong>: Kyung Hee University
<strong>Company URL</strong>: <a href="http://www.khu.ac.kr">www.khu.ac.kr</a>
<img src="https://velog.velcdn.com/images/young-0320/post/4d512fd0-2eab-4241-aa36-09902e8ea5d2/image.png" alt="">
상세 주소를 입력해줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/86126a79-8974-48e9-949c-be70a4cea9a1/image.png" alt=""></p>
<p><strong>Address Line 1</strong>: 1732, Deogyeong-daero, Giheung-gu</p>
<p><strong>City</strong>: Yongin-si</p>
<p><strong>State/Province</strong>: Gyeonggi-do</p>
<p><strong>Zip/Postal code</strong>: 17104</p>
<h4 id="3-2-create-a-checkout-profile">3-2. Create a checkout profile</h4>
<p>단계 별로 진행해 줍니다.
<img src="https://velog.velcdn.com/images/young-0320/post/78922cbd-7d40-49c3-8c13-4b8eb29c3db6/image.png" alt="">
<img src="https://velog.velcdn.com/images/young-0320/post/2ac6a346-e902-4cdf-92e5-b41e394b302b/image.png" alt="">
<img src="https://velog.velcdn.com/images/young-0320/post/e9659e2b-d2cd-4ca5-b5ff-eb5270f0276f/image.png" alt="">
<img src="https://velog.velcdn.com/images/young-0320/post/64181008-fcde-4c38-b740-fce86751a347/image.png" alt="">
<img src="https://velog.velcdn.com/images/young-0320/post/592a1a75-62d0-4c31-8ae6-d920a52b76ae/image.png" alt="">
<img src="https://velog.velcdn.com/images/young-0320/post/c3a3d487-c126-4119-818b-48672a2c689c/image.png" alt="">
마지막으로 Save를 눌러 저장을 합니다.</p>
<h3 id="4-request-창으로-돌아가서-submit-버튼을-눌러줍니다">4. Request 창으로 돌아가서 Submit 버튼을 눌러줍니다.</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/71af8c83-ab5b-4e9e-b979-815f63efb068/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/7e8c9c61-9ddb-4579-9443-c77ac0f000e8/image.png" alt="">
이제 약 5~10분 이내에 가입 한 이메일로 28자리의 Access Key가 발송될 것입니다.</p>
<h3 id="5-메일을-확인해-줍니다">5. 메일을 확인해 줍니다.</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/55eea86b-2a08-4b77-9eb4-2339118ae473/image.png" alt="">
메일에 포함된 링크를 클릭하여 페이지로 이동한 다음 로그인을 완료합니다.</p>
<h3 id="6-pspice를-설치해-줍니다">6. PSPice를 설치해 줍니다.</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/2e9eb1f9-d83f-4e3d-8fd2-9abb8bf8ee01/image.png" alt="">
Download 버튼을 클릭하여 설치를 진행합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/cf43e4c0-e512-4924-a3a2-9bca2ce99da2/image.png" alt="">
가입한 이메일과 메일로 전달받은 Access Key를 입력합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/0805cebe-efa4-47f6-8ef7-5002086c1fd8/image.png" alt="">
이후 설치가 자동으로 진행됩니다.
<img src="https://velog.velcdn.com/images/young-0320/post/d2f5ef5b-9064-4252-82cc-01e1465c7f1e/image.png" alt="">
설치가 완료되면 정상적으로 실행할 준비가 끝납니다.
<img src="https://velog.velcdn.com/images/young-0320/post/4a00e20e-00ec-4851-8a2e-2e3cd567cdc0/image.png" alt="">
성공!</p>
<h3 id="마무리하며">마무리하며</h3>
<p>이번 과정에서는 업계 표준 아날로그 회로 시뮬레이터 PSpice를 설치하고 설계를 진행할 수 있는 환경을 준비했습니다.</p>
<p>다음 포스팅에서는 VS Code에서 프로젝트를 설정하고, 보다 효율적으로 작업하기 위한 생산성 향상 방법들을 다뤄보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[현대적 C++ 프로젝트 관리의 표준 : CMake의 필요성과 사용 방법]]></title>
            <link>https://velog.io/@young-0320/CMake-%EB%AC%B8%EB%B2%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@young-0320/CMake-%EB%AC%B8%EB%B2%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 09 Feb 2026 09:34:09 GMT</pubDate>
            <description><![CDATA[<p>C/C++ 개발자가 IDE의 빌드 버튼 하나로 프로그램을 실행하는 것은 편리하지만, 프로젝트의 규모가 커지고 협업 환경이 다양해지면 곧 한계에 부딪힙니다. </p>
<p>내 컴퓨터(Windows/VS)에서는 잘 돌아가는 코드가 서버(Linux/GCC)에서는 빌드조차 되지 않는 상황, 이 &#39;환경의 파편화&#39;를 해결하는 것이 바로 CMake의 존재 이유입니다.</p>
<h2 id="1-cmake란-무엇인가">1. CMake란 무엇인가?</h2>
<p><img src="https://velog.velcdn.com/images/young-0320/post/e5ddf628-2f80-4a40-9a64-4b8f1255915f/image.png" alt=""></p>
<p><strong>CMake는 특정 IDE나 OS에 종속되지 않는 독립적인 빌드 환경을 구축하기 위한 &#39;메타 빌드 시스템&#39;입니다.</strong></p>
<p>C/C++ 프로젝트를 빌드할 때는 전통적으로 Makefile을 작성해야 합니다. 
하지만 개발자가 Makefile이나 Solution 파일을 직접 짜는 대신, 고수준의 설정 파일(CMakeLists.txt)만 작성하면 CMake가 타겟 환경에 맞는 빌드 파일을 알아서 빌드를 해줍니다.</p>
<p>또한 운영체제마다 다른 빌드 시스템(Linux의 Make, Windows의 Visual Studio 등)을 사용하기 때문에 OS별로 빌드 스크립트를 중복 관리할 필요가 없습니다.</p>
<p>소스 파일이 늘어나도 설정 파일의 변수나 와일드카드 기능을 통해 복잡한 의존 관계를 간단하게 업데이트할 수 있습니다.</p>
<h2 id="2-cmake의-필요성">2. CMake의 필요성</h2>
<p>왜 CMake를 사용해야 할까요? 단순히 Visual Studio를 쓰기 싫어서가 아니라, 
어떤 환경에서도 동작하는 지속 가능한 빌드 시스템이 필요하기 때문입니다.</p>
<p>전통적으로 Visual Studio 같은 IDE 없이 직접 코드를 빌드하기 위해서는 컴파일러 명령어를 터미널에 직접 입력해야 합니다. </p>
<pre><code># 1. 각 소스 파일(.c)을 컴파일하여 기계어인 목적 파일(.o)로 변환
gcc -c main.c   # main.c -&gt; main.o 생성
gcc -c A.c      # A.c -&gt; A.o 생성

# 2. 생성된 목적 파일들을 하나로 Link하여 최종 실행 파일(code.out) 생성
gcc -o code.out main.o A.o</code></pre><p>파일이 2~3개일 때는 문제가 없지만, 소스 파일이 수십, 수백 개로 늘어난다면 이런 명령어를 매번 입력하는 것은 불가능에 가깝습니다.</p>
<p>이 과정을 자동화하기 위해 make가 등장했습니다. 
Makefile에 빌드 규칙을 한 번만 적어두면, make가 파일의 변경 사항을 감지하여 필요한 부분만 효율적으로 컴파일해 줍니다. 즉 모든 명령어를 입력할 필요 없이 <code>make</code> 입력 한번으로 끝납니다.</p>
<pre><code># Makefile 예시
code.out: main.o A.o
    gcc -o code.out main.o A.o

main.o: main.c
    gcc -c main.c

A.o: A.c
    gcc -c A.c</code></pre><p>make는 훌륭한 도구이지만, 다음과 같은 한계가 있습니다.</p>
<p><strong>1. 플랫폼 종속성</strong>: 리눅스에서 작성한 Makefile은 윈도우의 nmake나 Visual Studio 환경에서 그대로 사용할 수 없습니다. 운영체제마다 빌드 도구가 다르기 때문입니다.</p>
<p><strong>2. 복잡한 문법</strong>: 프로젝트가 커질수록 Makefile 자체의 문법이 난해해져 유지보수가 어려워집니다.</p>
<p><strong>3. IDE와의 연동성 부족</strong>: 현대 엔지니어링에서는 VS Code, CLion, Visual Studio 등 다양한 에디터를 사용하는데, 각 에디터에 맞는 프로젝트 파일을 수동으로 생성하는 것은 매우 비효율적입니다.</p>
<p>결국 CMake는 운영체제마다 다른 Makefile이나 프로젝트 파일을 자동으로 생성해 주는 메타 빌드 시스템으로서, 현대 C/C++ 개발 환경의 사실상 표준이 되었습니다.</p>
<h2 id="3-cmake-문법">3. CMake 문법</h2>
<p>Cmake의 단계는 아래 이미지와 같습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/c315dba6-70bb-4386-b731-f40af2295193/image.png" alt=""></p>
<p>CMake 문법을 3단계로 나누어서 설명해 보겠습니다.</p>
<p>🟢 <strong>당장 프로젝트를 빌드하는 데 필요한 것(기초)</strong></p>
<p>🟡 <strong>협업·확장 가능한 구조를 만드는 데 필요한 것(모듈화)</strong></p>
<p>🔵 <strong>대형 프로젝트, 패키징, 자동화까지 다루는 영역(심화-생략)</strong></p>
<p>CMake 문법 자체가 워낙 방대하고 어려운 부분도 있어 해당 포스팅에서는 2단계까지만 설명하겠습니다.</p>
<h3 id="🟢-당장-프로젝트를-빌드하는-데-필요한-것기초">🟢 당장 프로젝트를 빌드하는 데 필요한 것(기초)</h3>
<p>최소한의 빌드 환경 구축에는 다음과 같은 과정이 필요합니다.</p>
<p>해당 단계의 목표는 수동으로 <code>gcc</code> 명령어를 치던 과정을 <code>CMakeLists.txt</code> 파일로 대체하는 것입니다. 이때 파일 이름은 대소문자를 구분하여 생성해야 합니다.</p>
<ol>
<li>프로젝트 루트 디렉토리에 <code>CMakeLists.txt</code> 파일을 생성합니다.</li>
<li>해당 파일 안에 다음과 같은 필수 코드들을 입력합니다.</li>
</ol>
<ul>
<li><p><code>cmake_minimum_required(VERSION [버전])</code> : CMake의 최소 버전을 지정합니다.</p>
</li>
<li><p><code>project([프로젝트 이름] VERSION [발전 단계] LANGUAGES [사용 언어])</code> :  프로젝트 정보들을 설정합니다.</p>
</li>
<li><p><code>add_executable([실행파일 이름] [필요한 소스 파일들])</code> : 실행파일을 빌드할 때 [필요한 소스 코드들]을 컴파일하고 링크하여 [실행파일 이름]이라는 실행파일을 생성합니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/young-0320/post/de32e401-057f-4685-b40f-c30ecb67ef71/image.png" alt=""></p>
<ul>
<li>최소 버전은 3.10으로 지정했습니다.</li>
<li>프로젝트의 이름은 CH11이고 버전은 0.1.0, 사용 언어는 C와 C++입니다.</li>
<li>mycomplex.cpp와 mycomplex_test.cpp 소스 파일을 컴파일하고 링크하여 LAB11이라는 실행파일을 빌드합니다.<pre><code># 폴더 구조
Project_Root/
├── CMakeLists.txt
├── mycomplex.cpp
└── mycomplex_test.cpp</code></pre><h3 id="🟡-협업·확장-가능한-구조를-만드는-데-필요한-것모듈화">🟡 협업·확장 가능한 구조를 만드는 데 필요한 것(모듈화)</h3>
프로젝트가 커지면 모든 소스를 한곳에 두는 것은 불가능합니다. 
CMake의 핵심은 물리적으로는 폴더(Directory)로 나누고, 
논리적으로는 타겟(Target)으로 묶어 의존성을 관리하는 것입니다.</li>
</ul>
<h4 id="1-물리적-계층-구조">1. 물리적 계층 구조</h4>
<p><code>add_subdirectory()</code>를 사용하면 프로젝트를 기능 단위로 물리적으로 분리할 수 있습니다. 
프로젝트의 파일 구조를 트리 형태로 조직화하여 관리의 편의성을 높이는 것입니다 </p>
<p>폴더별로 CMakeLists.txt를 두어 해당 폴더 내의 소스들만 관리하게 함으로써 관심사를 분리합니다.</p>
<p><strong>기본적으로 다음의 구조를 따릅니다.</strong></p>
<pre><code># 폴더 구조
FIR-FILTER/
  ├── CMakeLists.txt                  # [Root] 프로젝트 전체 관리 정의
  ├── lib
  │   ├── CMakeLists.txt              # `mycomplex` 정적 라이브러리 타깃 정의
  │   ├── mycomplex.h                 # `MyComplex` 클래스 선언(헤더)
  │   └── mycomplex.cpp               # `MyComplex` 클래스 구현(소스)
  └── fir_1d
      ├── model
      │   └── cpp
      │       ├── CMakeLists.txt      # fir_1d_ref 정적 라이브러리 타겟 정의
      │       ├── fir_1d_ref.h        # 1D FIR 모델 인터페이스(클래스 선언)
      │       └── fir_1d_ref.cpp      # 1D FIR 모델 구현
      └── sim
          └── tests
              └── CMakeLists.txt      # 최종 실행 파일 타겟 정의</code></pre><h4 id="2-단계별-구현">2. 단계별 구현</h4>
<p><strong>A. 최상위 <code>CMakeLists.txt</code>가 가장 먼저 호출되어 프로젝트를 정의하고 하위 작업을 지정합니다.</strong>
<img src="https://velog.velcdn.com/images/young-0320/post/4d8e7807-6843-4ca5-a35c-b848691ed9fc/image.png" alt=""></p>
<pre><code># Root CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(FIR-FILTER VERSION 1.0 LANGUAGES C CXX)

# 하위 디렉토리 추가 (컴파일 순서 제어)
add_subdirectory(lib)

add_subdirectory(fir_1d/model/cpp)
add_subdirectory(fir_1d/sim/tests)</code></pre><p>이때 하위 디렉토리는 명령어가 나열된 순서대로 탐색됩니다. 
따라서 fir_1d와 같은 하위 디렉토리에서 lib의 결과물을 참조하여 빌드하려면, 반드시 lib을 먼저 추가하여 라이브러리 타겟이 먼저 정의되도록 해야 합니다.</p>
<p><strong>B. 범용 라이브러리 모듈화</strong>
소스 코드를 재사용 가능한 이진 파일(Target)로 캡슐화하고, 컴파일에 필요한 헤더 경로를 외부에 전파합니다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/e9b3a4ac-a85f-4fb1-a145-1c41ac44ef08/image.png" alt="">
mycomplex라는 이름의 정적 라이브러리를 생성하고 이를 사용할 다른 타겟들에게 헤더 위치를 공개합니다.</p>
<pre><code># Lib CMakeLists.txt
# 라이브러리 생성
add_library(mycomplex 
    [STATIC|SHARED|MODULE] 
    mycomplex.cpp
)</code></pre><ul>
<li><p><strong>STATIC</strong> : 정적 라이브러리(.a, .lib). 컴파일 시 실행 파일에 포함됩니다. 용량이 증가하지만 배포가 간편하여 보통 기본값으로 사용됩니다.</p>
</li>
<li><p><strong>SHARED</strong> : 동적 라이브러리(.dll, .dylib). 실행 시 메모리에 로드됩니다. 실행 파일 용량은 줄어들지만, 실행 환경에 해당 라이브러리 파일이 함께 배포되어야 합니다.</p>
</li>
<li><p><strong>MODULE</strong> : 런타임에 동적으로 로드되는 플러그인 형식의 라이브러리입니다.</p>
<pre><code># mycomplex가 사용할 헤더 파일 경로 지정
target_include_directories(mycomplex 
  [PRIVATE|PUBLIC|INTERFACE] 
  ${CMAKE_CURRENT_SOURCE_DIR}
)</code></pre></li>
<li><p><strong>PRIVATE</strong> : 내부 구현용으로, 헤더 경로가 내(mycomplex) 빌드에만 필요함</p>
</li>
<li><p><strong>PUBLIC</strong> : 공개용으로, mycomplex 빌드에도 필요하고 나를 가져다 쓰는 상위 타겟(fir_1d_ref 등) 모두에게 필요함</p>
</li>
<li><p><strong>INTERFACE</strong> : 인터페이스 전용으로, 나를 빌드할 땐 필요 없지만 나를 쓰는 타겟은 반드시 알아야 할 때 사용합니다.</p>
</li>
</ul>
<p><strong>C. 소스 파일의 라이브러리화</strong></p>
<p><code>fir_1d_ref</code> 라이브러리를 생성하고, 자체 헤더 경로와 하위 의존성(mycomplex)을 모두 PUBLIC으로 설정하여 상위 타겟(test_fir_1d)으로 한 번에 전파하는 과정입니다.
<img src="https://velog.velcdn.com/images/young-0320/post/4a336cdc-eb15-4b7a-badc-8412a8f5df3d/image.png" alt=""></p>
<pre><code>add_library(fir_1d_ref STATIC fir_1d_ref.cpp)

# 상위 타겟에 라이브러리 전파
target_include_directories(fir_1d_ref
    PUBLIC 
        ${CMAKE_CURRENT_SOURCE_DIR}
)

# 범용 라이브러리(mycomplex)를 fir_1d_ref와 연결
target_link_libraries(fir_1d_ref PUBLIC mycomplex)
</code></pre><ol>
<li><p>타겟 생성
fir_1d_ref.cpp 소스 코드를 컴파일하여 정적 라이브러리(STATIC)로 만듭니다. 하나의 독립적인 모듈 블록을 생성하는 것입니다.</p>
</li>
<li><p>헤더 경로 전파 (PUBLIC)
<code>target_include_directories</code>를 PUBLIC으로 설정합니다. 이는 fir_1d_ref를 빌드할 때뿐만 아니라, 이 라이브러리를 가져다 쓰는 상위 실행 파일에게도 &quot;내 헤더 파일은 이 폴더에 있어&quot;라고 위치를 알려주는 역할을 합니다.</p>
</li>
<li><p>라이브러리 체이닝 (의존성 상속)
fir_1d_ref가 동작하려면 앞서 만든 mycomplex 라이브러리가 필요합니다. 이를 PUBLIC으로 링크하면, 나중에 최종 실행 파일이 fir_1d_ref 하나만 링크하더라도 하위의 mycomplex까지 자동으로 딸려와서 링크됩니다. 즉, 의존성이 체인처럼 연결됩니다.</p>
</li>
</ol>
<p>참고:
<strong>하위 타겟 (Lower-level Target):</strong> 다른 모듈에 의존하지 않고 독립적으로 존재할 수 있는 기본 부품</p>
<ul>
<li>예: mycomplex 라이브러리 </li>
</ul>
<p><strong>상위 타겟 (Upper-level Target):</strong> 하위 부품들을 조립하여 더 복잡한 기능을 만드는 주체</p>
<ul>
<li>예: fir_1d_ref 라이브러리나 test_fir_1d 실행 파일 </li>
</ul>
<p><strong>D. 실행파일 및 라이브러리 연결</strong>
Modern CMake의 가장 강력한 기능입니다. 
복잡한 헤더 경로를 일일이 추가할 필요 없이, 단순히 타겟을 link하는 것만으로 라이브러리의 모든 정보(헤더 경로, 컴파일 옵션 등)를 자동으로 상속받습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/3e7cf54b-6314-4a6e-8de7-cf3dfbeaa472/image.png" alt=""></p>
<pre><code># App CMakeLists.txt
# 실행 파일 생성
add_executable(test_fir_1d
    test_fir_1d_golden.cpp
  )

# fir_1d_ref가 PUBLIC으로 공개한 헤더 경로와 컴파일 옵션을 자동으로 상속받습니다.
target_link_libraries(test_fir_1d PRIVATE fir_1d_ref)</code></pre><ul>
<li><strong>PRIVATE</strong> : test_fir_1d는 최종 실행 파일이므로 다른 타겟이 이를 다시 링크하여 사용할 일이 없습니다. 따라서 상속받은 의존성을 더 이상 전파하지 않고 여기서 마무리하기 위해 PRIVATE을 사용합니다.</li>
<li><strong>PUBLIC</strong> : 실행 파일은 최상위 계층이며 다른 타겟이 이를 링크하지 않으므로, 의존성을 외부로 전파하는 PUBLIC을 사용할 필요가 없습니다.</li>
</ul>
<h4 id="마무리--cmake-프로젝트-설계-흐름-요약">마무리 : CMake 프로젝트 설계 흐름 요약</h4>
<p>지금까지 다룬 내용을 바탕으로, 확장 가능한 프로젝트가 구성되는 단계별 흐름을 정리하면 다음과 같습니다.</p>
<p><strong>1. 물리적 격리 (add_subdirectory):</strong> 소스 코드를 기능별 폴더로 분리하고 Root에서 순차적으로 호출하여, 빌드 시스템의 스코프(Scope)를 명확히 나눕니다.</p>
<p><strong>2. 논리적 캡슐화 (add_library):</strong> 각 기능 모듈(예: mycomplex, fir_1d_ref)을 독립적인 타겟으로 묶어 재사용 가능한 블록으로 만듭니다.</p>
<p><strong>3. 인터페이스 정의 (target_include_directories):</strong> 가시성(PRIVATE/PUBLIC/INTERFACE)을 설정하여 내부 구현은 숨기고, 외부 타겟과 결합할 때 필요한 헤더 경로만 선택적으로 전파합니다.</p>
<p><strong>4. 의존성 체이닝 (target_link_libraries):</strong> 상위 타겟은 복잡한 경로 설정 없이 하위 타겟을 링크하는 것만으로 컴파일에 필요한 모든 정보를 자동으로 상속받아 최종 실행 파일(add_executable)을 완성합니다.</p>
<h2 id="4-cmake의-사용">4. CMake의 사용</h2>
<h3 id="1-vs-code의-cmake-toolsgui-방식">1. VS Code의 CMake Tools(GUI 방식)</h3>
<p>마이크로소프트가 제공하는 CMake Tools 익스텐션을 사용하면, 복잡한 명령어를 버튼 클릭으로 대체할 수 있습니다.</p>
<ul>
<li><strong>1. 설치 :</strong> VS Code 마켓플레이스에서 <code>CMake Tools</code> 설치합니다.</li>
<li><strong>2. 설정 :</strong> Command Palette의  <code>CMake: Configure</code>에서 컴파일러(Kit)를 선택하고, 빌드 시스템(Makefile 등)을 생성합니다.</li>
<li><strong>3. 빌드 :</strong> 하단 상태 표시줄의 Build 버튼 클릭 혹은 <code>F7</code>으로 빌드합니다.</li>
<li><strong>4. 실행 :</strong> 이후 하단 상태 표시줄의 화살표 버튼을 클릭하여 터미널에서 실행합니다.</li>
</ul>
<h3 id="2-터미널--cli">2. 터미널 / CLI</h3>
<h4 id="a-out-of-source-빌드-폴더를-생성합니다">A. Out-of-source 빌드 폴더를 생성합니다.</h4>
<pre><code>mkdir build
cd build</code></pre><p>소스 코드와 빌드 결과물(.o, 실행 파일 등)이 섞이지 않도록 별도의 build 디렉토리를 생성하고 진입합니다.</p>
<h4 id="b-빌드-시스템을-구성configure합니다">B. 빌드 시스템을 구성(Configure)합니다.</h4>
<pre><code>cmake ..</code></pre><p>현재 위치(build)에서 상위 디렉토리(..)에 있는 CMakeLists.txt를 읽어들여, 현재 OS와 컴파일러 환경에 맞는 빌드 시스템(Makefile 등)을 생성합니다. (Root 디렉토리로 이동하는 것이 아닙니다.)</p>
<h4 id="c-실제-빌드build를-수행합니다">C. 실제 빌드(Build)를 수행합니다.</h4>
<pre><code>cmake --build .</code></pre><p>생성된 빌드 시스템을 이용해 소스 코드를 컴파일하고 타겟들을 링킹하여 최종 실행 파일을 만듭니다.</p>
<h4 id="d-프로그램을-실행합니다">D. 프로그램을 실행합니다.</h4>
<pre><code>./fir_1d/sim/tests/test_fir_1d</code></pre><p>최종 산출물은 Root의 build 폴더를 기준으로 CMakeLists.txt가 있던 계층 구조를 그대로 따라 생성됩니다.</p>
<h4 id="표준-명령어-흐름">표준 명령어 흐름</h4>
<pre><code>mkdir build &amp;&amp; cd build          # 1. 빌드 폴더 격리
cmake ..                         # 2. 구성 (Configure)
cmake --build .                  # 3. 컴파일 및 링킹 (Build)
./fir_1d/sim/tests/test_fir_1d  # 4. 최종 실행 (Run)</code></pre><h2 id="마무리하며">마무리하며</h2>
<p>단순히 언어의 문법을 넘어, 코드가 어떻게 시스템 위에서 실행 파일이 되는지 이해해야 합니다.</p>
<p>프로젝트의 규모가 커지고 리눅스 서버나 임베디드 보드 등 다양한 환경을 마주하게 되면, &quot;어디서든 빌드되는 프로젝트&quot;를 설계하는 능력은 코딩 실력만큼이나 중요한 핵심 역량이 됩니다.</p>
<p>오늘 다룬 CMake는 단순한 컴파일 도구가 아닙니다. 
복잡한 의존성을 통제하고 자동화하는 엔지니어링의 기초입니다.</p>
<p>이 글이 IDE의 편리한 &#39;Build 버튼&#39; 뒤에 숨겨진 원리를 이해하고, 더 견고하고 확장 가능한 프로젝트를 설계하는 첫걸음이 되기를 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #10 : Ubuntu에서 C++ 툴체인 구축하기]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-10-Ubuntu%EC%97%90%EC%84%9C-C-%ED%88%B4%EC%B2%B4%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-10-Ubuntu%EC%97%90%EC%84%9C-C-%ED%88%B4%EC%B2%B4%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 08 Feb 2026 08:49:46 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅은 지난 파이썬 환경 구축(#9)에 이어, C++ 컴파일러와 CMake 빌드 시스템을 설치하고 설정하는 방법에 대해 알아보겠습니다.</p>
<h3 id="1-c-툴체인의-핵심-구성-요소">1. C++ 툴체인의 핵심 구성 요소</h3>
<ul>
<li>컴파일러 (g++)</li>
<li>빌드 시스템(CMake, make)</li>
<li>디버거(GDB)</li>
</ul>
<h4 id="1-g">1. g++</h4>
<pre><code>g++ --version</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/043f0eca-9ad6-4ce3-9b66-d7685401dbd4/image.png" alt="">
<strong>g++ version : 13.3.0</strong></p>
<p>만약 설치가 안되어 있다면 아래의 명령어로 설치합니다.</p>
<pre><code>sudo apt update
sudo apt install g++</code></pre><h4 id="2-cmake">2. cmake</h4>
<pre><code>cmake --version</code></pre><p>설치가 되어 있지 않아 에러 코드 출력되었습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/f2b9eec9-474c-4884-a7ef-3e04b1b21c51/image.png" alt="">
설치 후 다시 버전 확인합니다.
<img src="https://velog.velcdn.com/images/young-0320/post/e0d2ab9e-a18f-47b6-92c2-48f29b941382/image.png" alt=""></p>
<p><strong>cmake version : 3.28.3</strong></p>
<h4 id="3-make">3. make</h4>
<pre><code>make -v</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/17bd52a2-2613-42f4-b92a-b543d1e1458c/image.png" alt=""></p>
<p><strong>make version : 4.3</strong></p>
<h4 id="4-gdb">4. GDB</h4>
<pre><code>gdb --version</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/73fa87f9-60bf-4e5a-9b27-8a2418748687/image.png" alt=""></p>
<p><strong>gdb version : 15.0.50</strong></p>
<h3 id="2-c-툴체인의-추가-구성-요소">2. C++ 툴체인의 추가 구성 요소</h3>
<ul>
<li>버전 관리 (Git)</li>
<li>가독성 향상 (clang-format)</li>
</ul>
<h4 id="1-git">1. Git</h4>
<p><img src="https://velog.velcdn.com/images/young-0320/post/6037c37c-83e6-4e44-97b7-2ba8ae128e0d/image.png" alt=""></p>
<p><strong>git version : 2.43.0</strong></p>
<h4 id="2-clang-format">2. clang-format</h4>
<pre><code>sudo apt install clang-format</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/0dee7469-923c-4fd6-b429-176009e30c31/image.png" alt=""></p>
<p><strong>clang-format version : 18.1.3</strong></p>
<h3 id="3-디지털-회로-설계-도구">3. 디지털 회로 설계 도구</h3>
<p><strong>시뮬레이션 및 검증:</strong></p>
<ul>
<li>Icarus Verilog (RTL 시뮬레이터)</li>
<li>Verilator (고성능 시뮬레이터)</li>
</ul>
<p><strong>분석 및 디버깅:</strong></p>
<ul>
<li>GTKWave (파형 분석)</li>
</ul>
<p><strong>텍스트 에디터:</strong></p>
<ul>
<li>Visual Studio Code (VS Code)</li>
<li>Emacs</li>
<li>Vi IMproved (Vim)</li>
</ul>
<h3 id="4-해당-툴-설치">4. 해당 툴 설치</h3>
<h4 id="1-icarus-verilog">1. Icarus Verilog</h4>
<pre><code># 버전 확인 
iverilog -v

# 설치되어 있지 않다면 
sudo apt update
sudo apt install iverilog</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/967ca7ba-8c4f-469b-ba37-8612473612ed/image.png" alt="">
<strong>iverilog version : 12.0</strong></p>
<h4 id="2-verilator">2. Verilator</h4>
<pre><code># 버전 확인 
verilator --version

# 설치되어 있지 않다면 
sudo apt install verilator</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/1dca268d-bae5-400a-9e0b-42da36f71066/image.png" alt=""></p>
<p><strong>verilator version : 5.020-1</strong></p>
<h4 id="3-gtkwave">3. GTKWave</h4>
<pre><code># 버전 확인 
gtkwave --version

# 설치되어 있지 않다면 
sudo apt install gtkwave</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/b4c38464-5b6b-4310-aab2-8af5af087831/image.png" alt=""></p>
<p><strong>gtkwave version : 3.3.116</strong></p>
<h4 id="4-visual-studio-code">4. Visual Studio Code</h4>
<pre><code># 버전 확인 
code --version

# 설치되어 있지 않다면 
sudo apt install code

# 실행
code .</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/9a457059-8a53-463d-92f9-cf184ae7655d/image.png" alt=""></p>
<p><strong>vscode version : 1.109.0</strong></p>
<h4 id="5-emacs">5. Emacs</h4>
<pre><code># 버전 확인 
emacs --version

# 설치되어 있지 않다면 
sudo apt install emacs</code></pre><p>Emacs를 이용하여 메일 서비스를 사용할지를 묻는 단계입니다. 메일 서비스를 굳이 텍스트 에디터에서 쓸 이유는 없으니 거절해 줍니다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/9b39134c-8a90-4fd6-aded-e8aa241ae487/image.png" alt="">
<code>&lt;Ok&gt;</code>  선택 후 ENTER
<img src="https://velog.velcdn.com/images/young-0320/post/7ad14092-5588-419d-875f-a8f049da76cd/image.png" alt="">
<code>No configuration</code> 선택 후 ENTER</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/b9d4eed1-7ed7-4f91-9225-a0ccd12c1219/image.png" alt="">
<strong>emacs version : 29.3</strong></p>
<h4 id="6-vim">6. vim</h4>
<pre><code># 버전 확인 
vim --version

# 설치되어 있지 않다면 
sudo apt install vim</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/d017b7fc-20ab-47b3-a764-127818312f09/image.png" alt=""></p>
<p><strong>vim version : 9.1</strong></p>
<h3 id="5-vs-code에서-emacs-vim-호출해보기">5. VS Code에서 emacs, vim 호출해보기</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/9907f31a-7710-4014-860c-158b9482bc9f/image.png" alt=""></p>
<p>vim와 emacs가 설치되어 있고 PATH에 정상적으로 등록됐음을 확인할 수 있습니다.</p>
<h3 id="6-개발-환경-구성">6. 개발 환경 구성</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/ce5f66db-582f-4332-9f5f-621e796130a9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/a9b2b0c0-3f3a-46e5-a737-24595ee78936/image.png" alt=""></p>
<h3 id="마무리하며">마무리하며</h3>
<p>VS Code를 메인 개발 환경으로 사용하되, 필요에 따라 Vim과 Emacs를 통합 터미널에서 활용하는 하이브리드 워크플로우를 구성했습니다. 특정 도구에 종속되기보다는 각 도구의 강점을 상황에 맞게 선택할 수 있는 유연한 개발 환경이 이번 툴체인 구축의 목표였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #9 : Ubuntu 22.04에서 pyenv로 Python 버전 관리하기]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-9-Ubuntu-22.04%EC%97%90%EC%84%9C-pyenv%EB%A1%9C-Python-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-9-Ubuntu-22.04%EC%97%90%EC%84%9C-pyenv%EB%A1%9C-Python-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 08 Feb 2026 06:27:55 GMT</pubDate>
            <description><![CDATA[<p>Ubuntu 22.04.3 LTS를 설치하니 기본 시스템 Python 버전이 3.12.3이었습니다. 하지만 프로젝트에 필요한 버전이 따로 있었고, 향후 여러 Python 버전을 유연하게 관리할 필요가 있어 pyenv를 도입하기로 결정했습니다.</p>
<p>이 포스팅에서는 pyenv 설치부터 원하는 Python 버전 설정까지 전 과정을 다룹니다.
<img src="https://velog.velcdn.com/images/young-0320/post/5b9548e3-fc21-45ca-9287-9e1889fa3351/image.png" alt=""></p>
<h3 id="1-github-페이지-방문">1. Github 페이지 방문</h3>
<p>pyenv의 깃허브 페이지(<a href="https://github.com/pyenv/pyenv)%EB%A1%9C">https://github.com/pyenv/pyenv)로</a> 이동하여 README를 살펴봅니다.
<img src="https://velog.velcdn.com/images/young-0320/post/037aeb12-f07e-44ec-8be8-d5cb43341e53/image.png" alt=""></p>
<h3 id="2-pyenv-설치-전-빌드-의존성-설정">2. pyenv 설치 전 빌드 의존성 설정</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/a07b9802-01f8-489e-ad47-ab0aa7707221/image.png" alt="">
아래의 명령어를 입력하여 빌드 의존성을 해결합니다.</p>
<pre><code>sudo apt update; sudo apt install make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev curl git \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/37f7ba57-03fd-4d5b-8bf7-a93abef5f552/image.png" alt=""></p>
<p>약간의 수정 버전. wget와 llvm을 추가로 설치해 확장성을 높였습니다.</p>
<pre><code>sudo apt update
sudo apt install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev</code></pre><h3 id="3-pyenv-설치">3. pyenv 설치</h3>
<p>해당 페이지의 README에 따라 진행해줍니다.</p>
<pre><code>curl -fsSL https://pyenv.run | bash</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/e94f883e-abc1-45f2-b97b-6a156bbe9991/image.png" alt=""></p>
<h3 id="4-환경-변수-설정">4. 환경 변수 설정</h3>
<p><img src="https://velog.velcdn.com/images/young-0320/post/2a7e5b8b-2fec-4b0f-98d3-24d23c8f42b2/image.png" alt="">
환경 변수가 설정되지 않아 발생하는 경고 문구입니다. 현재 상황에 따라 수정 파일을 선택하고, 설치 마지막에 출력되는 경로 코드를 복사하여 파일 맨 아래에 추가합니다.</p>
<pre><code>export PYENV_ROOT=&quot;$HOME/.pyenv&quot;
[[ -d $PYENV_ROOT/bin ]] &amp;&amp; export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;
eval &quot;$(pyenv init - bash)&quot;

# Restart your shell for the changes to take effect.
# Load pyenv-virtualenv automatically by adding
# the following to ~/.bashrc:

eval &quot;$(pyenv virtualenv-init -)&quot;</code></pre><p><strong>참고:</strong></p>
<ul>
<li><p><code>~/.bash_profile</code> : 로그인 시 1회 실행되는 전역 환경 변수를 설정하는 파일입니다. 해당 파일이 존재하면 <code>~/.profile</code>은 무시됩니다.</p>
</li>
<li><p><code>~/.profile</code> : 로그인 시 1회 실행되는 Bash 전용 로그인 설정 파일입니다.</p>
</li>
<li><p><code>~/.bashrc</code> : 터미널을 열 때마다(인터랙티브) 실행되는 작업 환경의 설정 파일입니다.</p>
</li>
</ul>
<h4 id="1-명령어로-파일의-존재-여부를-확인합니다">1. 명령어로 파일의 존재 여부를 확인합니다.</h4>
<p><img src="https://velog.velcdn.com/images/young-0320/post/eb3ec9e4-cc93-4c69-bd57-9bd93d5cdbf8/image.png" alt="">
<code>~/.profile</code> 파일은 존재하고 <code>~/.bash_profile</code> 파일은 존재하지 않습니다.</p>
<p><strong>로그인 셸에서 자동 로드하려면:</strong>
<code>~/.bash_profile</code> 파일이 존재하면 해당 파일에, 없으면<code>~/.profile</code> 파일에 경로를 추가해줍니다.</p>
<p><strong>인터랙티브 셸(비 로그인 셸)에서 자동 로드하려면:</strong>
<code>~/.bashrc</code>에 경로를 추가해줍니다.</p>
<h4 id="2-1-인터랙티브-셸비-로그인-셸에서-자동-로드하기">2-1. 인터랙티브 셸(비 로그인 셸)에서 자동 로드하기</h4>
<pre><code># 파일 열기
nano ~/.bashrc

# 파일의 맨 아래에 코드 추가
export PYENV_ROOT=&quot;$HOME/.pyenv&quot;
[[ -d $PYENV_ROOT/bin ]] &amp;&amp; export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;
eval &quot;$(pyenv init - bash)&quot;
eval &quot;$(pyenv virtualenv-init -)&quot;</code></pre><p>저는 절대 경로를 사용하여 경로 지정하였습니다.
<img src="https://velog.velcdn.com/images/young-0320/post/fbe6c441-ff6a-4b0b-868d-b864097efe17/image.png" alt=""></p>
<p>CTRL + O, ENTER 그리고 CTRL + X를 눌러 파일을 저장하고 종료합니다.</p>
<h4 id="2-2-로그인-셸에서-자동-로드하기">2-2. 로그인 셸에서 자동 로드하기</h4>
<pre><code># 파일 열기
nano ~/.profile

# 파일의 맨 아래에 코드 추가
export PYENV_ROOT=&quot;$HOME/.pyenv&quot;
[[ -d $PYENV_ROOT/bin ]] &amp;&amp; export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;
eval &quot;$(pyenv init - bash)&quot;

# 파일 열기
nano ~/.bashrc

# 파일의 맨 아래에 코드 추가
export PYENV_ROOT=&quot;$HOME/.pyenv&quot;
[[ -d $PYENV_ROOT/bin ]] &amp;&amp; export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;
eval &quot;$(pyenv init - bash)&quot;
eval &quot;$(pyenv virtualenv-init -)&quot;</code></pre><p>CTRL + O, ENTER 그리고 CTRL + X를 눌러 파일을 저장하고 종료합니다.</p>
<h4 id="3-수정한-설정을-적용합니다">3. 수정한 설정을 적용합니다.</h4>
<pre><code># ~/.bashrc 파일을 수정했다면
source ~/.bashrc 

# ~/.bash_profile 파일을 수정했다면
source ~/.bash_profile

# ~/.profile 파일을 수정했다면
source ~/.profile</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/d3d37938-a8b9-4b5b-9111-62742151641e/image.png" alt=""></p>
<h3 id="5-pyenv-설치-확인">5. pyenv 설치 확인</h3>
<p>모든 터미널을 완전히 껐다 킨 뒤 다음 명령어를 입력하여 <code>pyenv</code>가 정상적으로 설치되었는지 확인합니다.</p>
<pre><code>pyenv --version</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/2b910c4a-2336-4fa6-ba4f-88316d5ad388/image.png" alt=""></p>
<p>성공!</p>
<h3 id="6-python-설치">6. Python 설치</h3>
<h4 id="1-설치-가능-python-목록을-확인합니다">1. 설치 가능 Python 목록을 확인합니다.</h4>
<pre><code>pyenv install --list | grep &quot; 3.1&quot;</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/32b7d138-0ef9-4c90-8450-bf68d3e70e45/image.png" alt=""></p>
<p>3.1.0부터 3.15t-dev까지 설치가 가능합니다.</p>
<h4 id="2-python-버전-선택">2. Python 버전 선택</h4>
<p><strong>3.13.12 버전을 선택하였습니다.</strong></p>
<p>이유는 다음과 같습니다:</p>
<p>*<em>안정성 *</em></p>
<ul>
<li>3.13.12는 이미 전 세계 엔지니어들이 검증을 끝낸 버전입니다. </li>
<li>주요 라이브러리들과의 안정적인 호환성이 확보되어 있습니다.</li>
</ul>
<p><strong>최신 버전의 위험성</strong></p>
<ul>
<li>3.14 혹은 3.15와 같은 최신 버전은 아직 충분한 실전 검증을 거치지 못했습니다.</li>
<li>NumPy, SciPy 같은 수치 연산 라이브러리와의 예기치 않은 호환성 문제가 발생할 수 있습니다.</li>
<li>만약 미세한 부동소수점 연산 버그라도 있다면, &#39;알고리즘 문제인지, 하드웨어 구현 문제인지, Python 환경 문제인지&#39; 구분하기 어려운 디버깅 지옥에 빠질 수 있습니다.</li>
</ul>
<h4 id="3-python-31212-설치">3. Python 3.12.12 설치</h4>
<pre><code># 3.13.12 버전 설치
pyenv install 3.13.12

# 전역 버전으로 변경
pyenv global 3.13.12</code></pre><p><code>pyenv global 3.13.12</code> 명령어는 시스템 Python을 직접 수정하지 않습니다. 
대신 Shim(가로채기) 방식으로 동작합니다</p>
<ul>
<li>시스템 Python(3.12.3)은 절대 경로에 그대로 유지됩니다.</li>
<li>pyenv가 PATH 앞쪽에 위치하여 python 명령을 가로챕니다.</li>
<li>OS의 핵심 도구들은 절대 경로로 시스템 Python을 실행하므로 영향을 받지 않습니다.</li>
</ul>
<p>이는 시스템 Python을 직접 업데이트하는 것보다 훨씬 안전한 방식입니다.</p>
<h3 id="7-python-설치-확인">7. Python 설치 확인</h3>
<pre><code>python --version
which python</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/c3e8eac0-9770-4541-b543-9cd271adc05b/image.png" alt=""></p>
<p>&#39;3.13.12&#39;와 &#39;/home/young/.pyenv/shims/python&#39; 출력을 통해 버전과 경로가 제대로 설정됐음을 확인할 수 있습니다.</p>
<h3 id="마무리하며">마무리하며</h3>
<p>pyenv를 통해 안정적인 Python 개발 환경 구축을 완료했습니다. 
시스템 Python을 건드리지 않으면서도 원하는 버전을 자유롭게 관리할 수 있게 되었습니다.</p>
<p>다음 포스팅에서는 VSCode에서 C++ 개발 환경을 구축하는 방법을 다뤄보겠습니다. 컴파일러 설정부터 디버깅 환경까지, 효율적인 C++ 개발을 위한 설정을 다루겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경 설정 #8 : 5개 디스크를 위한 Ubuntu 고정 마운트 설정하기]]></title>
            <link>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-8-5%EA%B0%9C-%EB%94%94%EC%8A%A4%ED%81%AC%EB%A5%BC-%EC%9C%84%ED%95%9CUbuntu-%EA%B3%A0%EC%A0%95-%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young-0320/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-8-5%EA%B0%9C-%EB%94%94%EC%8A%A4%ED%81%AC%EB%A5%BC-%EC%9C%84%ED%95%9CUbuntu-%EA%B3%A0%EC%A0%95-%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 07 Feb 2026 08:55:07 GMT</pubDate>
            <description><![CDATA[<p>우분투를 처음 설치하면 우분투 디스크 외에 다른 디스크들은 자동 마운트 상태로 인식합니다. 이번 포스팅에서는 자동 마운트를 고정 마운트(Static Mount)로 수정하고, 심볼릭 링크(Symbolic Link)를 통해 분산된 하드웨어 자원을 최적화된 단일 경로로 통합하는 과정을 다뤄보겠습니다. </p>
<h3 id="1-고정-마운트-설정">1. 고정 마운트 설정</h3>
<p><strong>자동 마운트의 문제점</strong>:</p>
<ul>
<li>파일 탐색기에서 디스크를 클릭해야만 마운트되어 디스크에 접근할 수 있음</li>
<li>마운트 경로가 고정되어 있지 않아 매번 달라질 수 있음 (예: /media/young/xxxxx)</li>
<li>터미널이나 스크립트에서 일관된 경로로 접근하기 어려움</li>
</ul>
<p><strong>현재 디스크 인식 상태 확인</strong></p>
<pre><code>lsblk</code></pre><p>명령어로 Block Device 인식 상태를 확인합니다.</p>
<p><img src="https://velog.velcdn.com/images/young-0320/post/666929fa-20e6-4f1c-9a9d-54f13374688b/image.png" alt=""></p>
<p>현재 물리적으로는 5개의 디스크(sda, sdb, sdc, nvme0n1, nvme1n1)가 인식되지만, 실제로 읽고 쓸 수 있는 상태로 마운트된 것은 일부뿐입니다.</p>
<ul>
<li>nvme0n1: 우분투가 설치된 시스템 디스크 </li>
<li>nvme1n1: 윈도우가 설치된 시스템 디스크(부분 마운트 중)</li>
<li>sdb, sdc: 임시로 마운트된 디스크</li>
</ul>
<p>나머지 디스크들은 인식은 되지만 아직 마운트되지 않아 접근할 수 없는 상태입니다.
이러한 문제점을 해결하기 위해 고정 마운트 설정을 진행하겠습니다. 고정 마운트를 설정하면 부팅 시 자동으로 지정된 경로에 디스크가 마운트되어, 언제든 일관된 경로로 접근할 수 있습니다.</p>
<h4 id="1-각-디스크의-고유-식별자uuid-확보">1. 각 디스크의 고유 식별자(UUID) 확보</h4>
<pre><code>sudo blkid</code></pre><p>명령어로 각 디스크의 UUID를 확보합니다.</p>
<p>/dev/sda1: LABEL=&quot;Archive&quot; UUID=<code>982E35F32E3xxxx</code> 
/dev/sdb2: LABEL=&quot;Workspace&quot; UUID=<code>0ECE-xxxx</code>
/dev/sdc2: LABEL=&quot;ToolBox&quot; UUID=<code>960EBAE30EBAxxxx</code>
/dev/nvme1n1p3: LABEL=&quot;Windows&quot; UUID=<code>04DC4011DC40xxxx</code> 
dev/nvme0n1p2: UUID=<code>07b2f762-a6a8-4fcf-bc0a-8adca4abxxxx</code> </p>
<h4 id="2-마운트-포인트-생성">2. 마운트 포인트 생성</h4>
<p>각 디스크가 연결될 빈 폴더를 생성합니다.</p>
<pre><code>sudo mkdir -p /mnt/workspace
sudo mkdir -p /mnt/toolbox
sudo mkdir -p /mnt/archive
sudo mkdir -p /mnt/windows</code></pre><p><img src="https://velog.velcdn.com/images/young-0320/post/9587d91a-d2ad-444b-a0d9-8ea850ec394c/image.png" alt=""></p>
<p><strong>/mnt를 사용하는 이유</strong> :
/mnt는 리눅스에서 외부 디스크를 마운트할 때 관례적으로 사용하는 디렉토리입니다. /media는 자동 마운트용, /mnt는 수동/고정 마운트용으로 구분합니다.</p>
<h4 id="3-fstab-백업-파일-생성">3. fstab 백업 파일 생성</h4>
<p>fstab 파일은 부팅 시 디스크 마운트를 설정하는 핵심 파일입니다. 잘못 수정하면 시스템이 부팅되지 않을 수 있으므로, 반드시 백업 후 신중하게 작업하세요.</p>
<pre><code>sudo cp /etc/fstab /etc/fstab.bak
sudo nano /etc/fstab</code></pre><p><strong>uid/gid=1000</strong> : 일반 사용자가 sudo 없이 파일을 읽고 쓸 수 있게 권한을 부여합니다.
<strong>nofail</strong> : 디스크가 연결되지 않아도 우분투 부팅이 멈추지 않게 합니다.
<strong>ro (Read-only)</strong>: 윈도우 시스템 파일을 보호합니다.</p>
<p><strong>파일을 수정하기 전 터미널에 아래의 명령어를 입력하여 해당 정보들을 다시 한번 더 확인합니다.</strong></p>
<p><code>blkid /dev/sdXX</code> : UUID 다시 확인
<code>id</code> : uid와 gid 다시 확인
<code>ls /mnt</code> : 마운트 포인트가 있는지 다시 확인</p>
<p>*<em>fstab 파일의 맨 아랫줄에 다음 내용을 추가합니다. *</em></p>
<p><strong>Archive (ntfs)</strong>: <code>UUID=982E35F32E35xxxx /mnt/archive ntfs defaults,uid=1000,gid=1000,nofail 0 0</code></p>
<p><strong>Workspace (exfat)</strong>: <code>UUID=0ECE-xxxx /mnt/workspace exfat defaults,uid=1000,gid=1000,nofail 0 0</code></p>
<p><strong>ToolBox (ntfs)</strong>: <code>UUID=960EBAE30EBAxxxx /mnt/toolbox ntfs defaults,uid=1000,gid=1000,nofail 0 0</code></p>
<p><strong>Windows (ntfs)</strong>: <code>UUID=04DC4011DC40xxxx /mnt/windows ntfs defaults,uid=1000,gid=1000,ro,nofail 0 0</code> (안전을 위해 읽기 전용 ro 추천)</p>
<p>저장(CTRL + O, ENTER) 이후 편집기를 종료(CTRL + X)합니다.</p>
<h4 id="4-설정-적용-및-검증">4. 설정 적용 및 검증</h4>
<pre><code>sudo mount -a
lsblk -f</code></pre><p>/mnt/toolbox와 같이 고정 마운트가 된 것을 터미널 창에서 확인합니다.</p>
<p>이제 모든 디스크가 외장 하드가 아닌 시스템 구성 요소로 인식되어 독바에서 사라집니다.</p>
<h4 id="예외-상황">예외 상황</h4>
<p><strong>1. 마운트 경로 불일치</strong>
<code>sudo mount -a</code>를 입력했을 때 아래와 같은 문구가 출력될 수 있습니다. 해당 내용은 임시 자동 마운트와 새로운 고정 마운트 경로가 불일치하는 경우 출력됩니다.
<img src="https://velog.velcdn.com/images/young-0320/post/e880f4c8-7309-4b13-a7c6-6b04b17085fb/image.png" alt=""></p>
<pre><code>mount: (hint) your fstab has been modified, but systemd still uses
       the old version; use &#39;systemctl daemon-reload&#39; to reload.
Mount is denied because the NTFS volume is already exclusively opened.
The volume may be already mounted, or another software may use it which
could be identified for example by the help of the &#39;fuser&#39; command.</code></pre><p>아래의 명령어로 fstab 파일을 동기화 한 뒤 임시 마운트 경로를 해제 후 다시 마운트합니다.</p>
<pre><code># 1. 내부 설정을 다시 읽어 동기화합니다.
sudo systemctl daemon-reload 

# 2. 기존의 임시 마운트를 해제합니다.
sudo umount /dev/sdb2

# 3. 수정한 fstab 설정대로 다시 마운트를 시도합니다.
sudo mount -a

# 4. 결과를 확인합니다.
lsblk -f</code></pre><p><strong>2. 부팅 불가</strong>
fstab 구문 오류 혹은 UUID 오타 발생 시 부팅이 불가능(검은 화면)할 수도 있습니다.
Emergency Mode에서 fstab을 백업본으로 복구 하여 해당 오류를 해결합니다.</p>
<pre><code># 복구 모드로 진입
sudo cp /etc/fstab.bak /etc/fstab
sudo reboot</code></pre><h3 id="2-심볼릭-링크-생성">2. 심볼릭 링크 생성</h3>
<p>윈도우의 바로가기(Shortcut)와 유사하지만, 운영체제 커널 수준에서 경로를 치환해주는 더 강력한 기능입니다.</p>
<pre><code>ln -s [원본 디렉토리의 절대 경로] [내가 만들 바로가기 이름]</code></pre><p><strong>예시</strong></p>
<pre><code># workspace 연결
ln -s /mnt/workspace ~/ws

# toolbox 연결
ln -s /mnt/toolbox ~/tool

# archive 연결
ln -s /mnt/archive ~/archive</code></pre><p><strong>연결 확인</strong></p>
<pre><code>ls -l ~</code></pre><p>해당 명령어를 입력했을 때 아래의 이미지와 같이 폴더 이름 옆에 화살표(-&gt;)와 같이 절대 경로가 출력되면 성공입니다.
<img src="https://velog.velcdn.com/images/young-0320/post/27a35cda-b0ff-4185-9ca3-3c2431af7ace/image.png" alt=""></p>
<h3 id="마무리하며">마무리하며</h3>
<p>5개의 물리 디스크를 우분투 시스템의 일부로 연결시켰습니다. /etc/fstab을 통한 고정 마운트와 심볼릭 링크를 활용한 경로 추상화는 개발 환경의 재현성과 안정성을 확보하는 핵심적인 작업입니다.
이제 재부팅을 하거나 장치를 재연결해도 빌드 스크립트와 시뮬레이션 경로는 절대 깨지지 않을 것입니다.</p>
<p>다음 포스팅에서는 Python 환경과 신호처리 및 시스템 프로그래밍을 위한 C++, 그리고 반도체 설계를 위한 Verilog 도구들의 설치에 대해 다루겠습니다.</p>
]]></description>
        </item>
    </channel>
</rss>