Study Guide + Summary

Seungyun Lee·2026년 7월 30일

AXI4_UVM_FULL

목록 보기
2/16

이 문서 하나로 공부할 수 있게 방법 + 내용을 모두 담았다.

  • Part 0~1 — 어떻게, 어떤 순서로 공부할지
  • Part 2 — 실제 학습 내용 (핵심)
  • Part 3~6 — 면접 대비 정리

⚠️ 코드 스니펫은 이해를 돕기 위한 것이고, 정답은 항상 실제 파일이다.
파일이 갱신되면 이 문서보다 파일을 믿을 것.


Part 0. 공부 3원칙

① 위에서 아래로 (Top-down).
코드부터 열지 말 것. 그림(아키텍처) → 실행(파형) → 코드 순서.

② 파일 순서가 아니라 "트랜잭션의 일생"을 따라가라.
알파벳순으로 읽으면 죽는다. write 버스트 하나가
태어나서(seq) → 핀을 흔들고(driver) → 관측되고(monitor) → 검사받는(scoreboard)
경로를 따라가면 자연스럽게 연결된다.

③ 수동 읽기 금지, 능동 학습.
"읽고 이해했다"는 착각이 제일 위험하다. 코드를 덮고 다시 써보기,
파형에서 신호↔코드 연결하기, 소리 내어 설명하기.
"인식(recognize)"과 "설명(explain)"의 차이. 면접은 후자를 본다.


Part 1. 학습 경로 (3개 레이어)

🟦 Layer A — 큰 그림 + DUT

순서무엇집중
1docs/ARCHITECTURE.md다이어그램 손으로 따라 그리기. 백지 재현되면 통과
2파형 보기 (run_axi.bat1번)코드 전에 살아있는 걸 먼저. → Part 2.2 체크리스트
3rtl/axi_ram.v + docs/axi_ram_dut_analysis.pdfwrite FSM 3상태. awready가 언제 1이 되는지

🟩 Layer B — 트랜잭션의 일생 ⭐ (면접 질문의 80%)

순서파일집중
4tb/uvm/axi4_seq_item.svhrand 필드 + constraint. beat_addr()가 왜 필요한지
5tb/axi4_if.svclocking block이 왜 있나
6tb/uvm/axi4_driver.svh ⭐⭐가장 중요. 6개 스레드 + 큐 구조
7tb/uvm/axi4_monitor.svh핀만 보고 트랜잭션 복원. 채널 병렬 + FIFO
8axi4_ref_model.svh + axi4_scoreboard.svhpredict/check 패턴

🟨 Layer C — UVM 배선 + 부가 검증

순서파일집중
9tb/tb_top.svvif를 config_db에 넣고 run_test()
10agent_cfgagentenvbase_testbuild/connect phase 흐름
11axi4_sequences.svhstart_item/finish_item
12axi4_coverage.svhcovergroup, cross, ignore_bins
13tb/axi4_sva.svbind, VALID/READY 규칙
14rtl/Axi_ram_v2.v큐 기반 outstanding

Part 2. 학습 내용 정리 ⭐

2.1 AXI4 프로토콜 기초

5개 독립 채널

채널방향역할
AW (Write Address)마스터→슬레이브쓸 주소/제어
W (Write Data)마스터→슬레이브쓸 데이터 beat들
B (Write Response)슬레이브→마스터쓰기 완료 응답
AR (Read Address)마스터→슬레이브읽을 주소/제어
R (Read Data)슬레이브→마스터읽은 데이터 beat들

핵심: 5개가 독립적이다. read와 write는 서로 순서 보장이 없다.
(그래서 axi4_outstanding_test에서 write 다 끝내고(drain) read를 시작한다.)

핸드셰이크 — AXI의 유일한 규칙

VALIDREADY가 둘 다 1인 클럭 상승엣지에 전송이 성립한다.

파생 규칙:

  • VALID는 READY를 기다릴 수 있지만, READY 전에 내리면 안 된다 (한 번 올리면 유지)
  • READY는 VALID와 무관하게 아무 때나 올려도 된다
  • 전송 대기 중에는 payload(주소/데이터)를 바꾸면 안 된다

→ 이 두 규칙이 tb/axi4_sva.svAXI_VALID_HELD / AXI_STABLE 어서션이다.

burst 종류

burst주소 변화용도
FIXED (00)안 변함FIFO 같은 고정 주소
INCR (01)+ (1 << size)일반 메모리 접근 (제일 흔함)
WRAP (10)윈도우 안에서 되돌아감캐시 라인 fill

WRAP 규칙: beat 수가 반드시 2/4/8/16 중 하나.
→ 윈도우 크기가 2의 거듭제곱이어야 비트마스크로 랩할 수 있기 때문.

주요 필드

필드의미
lenbeat 수 - 1 (len=3이면 4 beats)
sizebeat당 바이트 = 2^size (size=2면 4바이트)
strbwrite 시 바이트 레인 마스크 (비트당 1바이트)
last마지막 beat 표시 (wlast/rlast)
resp00=OKAY, 10=SLVERR, 11=DECERR

반드시 지켜야 할 제약 (우리 constraint에 그대로 들어있음)

  1. 4KB 경계를 넘지 못한다c_4k
  2. 시작 주소는 size에 정렬되어야 한다 → c_align
  3. WRAP은 2/4/8/16 beats만 → c_wrap_len
  4. size가 버스 폭을 초과 못 함 → c_size

2.2 파형 읽는 법 (Layer A 2번의 핵심)

run_axi.bat1번(write path waveform)으로 열고 아래와 대조한다.

4-beat write burst의 정상 파형 (사이클별)

bready는 드라이버(b_thread)가 항상 1로 유지한다.

확인 체크리스트 ✅

  1. 리셋 직후 awready가 1 — DUT가 IDLE에서 대기
  2. awvalidawready동시에 1인 사이클이 있다 → 주소 수락
  3. 주소 수락 다음 사이클에 wready가 1 — 드라이버가 AW/W를 분리한 이유
  4. wvalid가 먼저 올라가 기다린다 — valid는 ready 전에 안 내려감
  5. beat 수 = awlen + 1 — 직접 세어볼 것
  6. 마지막 beat에서만 wlast=1
  7. wlast 다음에 bvalid, bresp=00
  8. write_state_reg: 0(IDLE) → 1(BURST) → 0

2.3 Write 트랜잭션의 일생

Step 0 — 태어남 (sequence)

req = axi4_seq_item::type_id::create("req");    // ① factory
start_item(req);                                 // ② 시퀀서에 요청
if (!req.randomize() with { dir == AXI_WRITE; }) // ③ 제약 만족 랜덤
finish_item(req);                                // ④ 드라이버가 가져갈 때까지 블로킹
  • new 대신 type_id::create → factory 경유 (테스트에서 타입 교체 가능)
  • finish_item은 드라이버가 item_done()을 부를 때까지 블로킹
    시퀀스와 드라이버의 속도를 맞추는 흐름 제어(back-pressure)

Step 1~2 — 시퀀서 → 드라이버 (TLM)

seq_item_port.get_next_item(req);   // 드라이버가 당겨옴 (pull 모델)

seq_item_port(드라이버) ↔ seq_item_export(시퀀서)는 agent의 connect_phase에서 연결.

Step 3 — 큐에 분배 (item_thread)

while (n_inflight >= cfg.max_outstanding) @(vif.master_cb);  // 깊이 제한
aw_q.push_back(req);  w_q.push_back(req);                    // 같은 순서로!
n_inflight++;
seq_item_port.item_done();          // ★ 버스 완료 전에 즉시 반환

item_thread는 버스 신호를 하나도 안 건드린다. 큐에 넣고 카운터만 관리.

Step 4 — 핀 구동 (aw_thread / w_thread)

// aw_thread
vif.master_cb.awaddr  <= tr.addr;
vif.master_cb.awvalid <= 1'b1;
@(vif.master_cb);
while (!vif.master_cb.awready) @(vif.master_cb);   // ready까지 유지
vif.master_cb.awvalid <= 1'b0;

master_cb(clocking block)로 구동 → 클럭 엣지 레이스 방지.

Step 5 — 모니터가 관측

드라이버와 완전히 별개로 핀을 감시한다. mon_aw가 AW 핸드셰이크를 잡아
트랜잭션을 만들고, mon_w가 데이터를 채우고, mon_b가 완성해서 방송한다.

Step 6 — Broadcast (analysis port, 1→N)

ap.write(tr);                              // 모니터
// env에서:
agent.mon.ap.connect(sb.ap_imp);           // → scoreboard
agent.mon.ap.connect(cov.analysis_export); // → coverage

2.4 드라이버 구조 (파이프라인)

run_phase6개 영구 스레드를 띄운다. 버스트마다 만드는 게 아니라
시뮬 내내 돈다. 서로 큐와 카운터로만 소통.

fork
    item_thread();                      // 시퀀서 → 큐 (버스 안 건드림)
    aw_thread();  w_thread();  b_thread();   // write 채널들
    ar_thread();  r_thread();                // read 채널들
join

비유: item_thread는 주문 접수 창구, aw/w/b_thread는 주방.
창구는 티켓(큐)만 꽂고, 주방이 요리한다. 그래서 창구가 안 막힌다.

outstanding이 생기는 원리

outstanding: out + standing = 밖에 나가 서 있는, 주소는 보냈는데 아직 응답 못 받은 상태

item_done()버스트 완료 전에 호출된다
→ 시퀀스가 곧바로 다음 걸 만들어 큐에 쌓음
여러 버스트가 버스 위에 동시 존재

깊이 제한은 while (n_inflight >= cfg.max_outstanding) @(cb) 한 줄.

  • max_outstanding = 1 → 직렬 (기존과 동일)
  • max_outstanding = 4 → 4개까지 겹침

카운터 2종

카운터의미
n_inflight시퀀서에서 받았지만 완료 안 된 것 (큐 대기 포함) → 깊이 제한용
wr_outstanding / wr_peak버스에 실제로 떠 있는 것 (주소 수락~응답 전) → 계측/증명용

wr_peakv1=2, v2=4로 나오는 게 outstanding 지원 차이의 실측 증거.


2.5 모니터 (outstanding-aware)

채널마다 스레드를 두고 FIFO로 단계를 상관(correlate) 시킨다.

AW 핸드셰이크 → aw_q에 push
W beats       → aw_q[0]에 채움, wlast에서 w_done_q로 이동
B 핸드셰이크  → w_done_q에서 pop, resp 붙여서 ap.write()

왜 순차 모니터면 안 되나 (실제로 겪은 버그)

처음엔 AW 대기 → W beat 수집 → B 대기 순차 루프였다.
버스트 N의 W beat를 수집하는 사이에 DUT가 N+1의 주소를 받으면
그 핸드셰이크를 영영 못 본다. → 트랜잭션 유실, payload 오배치.

교훈: 모니터는 DUT만큼 동시성을 모델링해야 한다.

전제: in-order 응답 (두 DUT 모두 보장). ID별 out-of-order라면 per-ID FIFO 필요.


2.6 Reference Model (골든 모델)

정의

DUT가 올바르게 동작했다면 메모리가 어떤 상태여야 하는지 예측하는 모델.

predict / check 패턴:

write 관측 → ref_model.apply_write()      (예측)
read  관측 → ref_model.expected_beat() → 실제와 비교 (검사)

설계 결정 5가지 (전부 면접 소재)

① DUT와 독립적으로 만든다 (가장 중요)
beat_addr()정확한 FIXED/INCR/WRAP 주소를 계산한다.
DUT 코드(addr + (1<<size))를 베꼈다면 같은 WRAP 버그를 복사해 아무것도 못 잡는다.

② 바이트 단위 저장

protected bit [7:0] mem [bit [AXI_ADDR_WIDTH-1:0]];

wstrb로 일부 바이트만 써지는 걸 정확히 반영. 워드 단위면 부분 쓰기에서 가짜 미스매치.

③ Sparse (연관 배열) — 안 쓴 곳은 0
DUT가 시각 0에 메모리를 0으로 초기화하므로 모델도 안 쓴 바이트는 0으로 예측.

④ word_base 정렬 — DUT의 인덱싱을 흉내

word_base = a - (a % AXI_STRB_WIDTH);   // 4바이트 워드 시작

DUT는 메모리를 워드 단위(addr >> 2) 로 인덱싱한다. 그래서 narrow transfer
(size < 버스폭)일 때 여러 beat가 같은 워드에 들어간다. 모델도 맞춰야 한다.

⑤ read 예측은 "정렬된 워드 전체" 반환
DUT가 32비트 워드를 통째로 반환하므로 모델도 그렇게. 그다음
스코어보드가 addressed lane만 골라서 비교 (나머지는 don't-care).

apply_write 흐름

foreach (tr.data[i]) begin
    a = tr.beat_addr(i);              // 이 beat의 올바른 주소
    word_base = a - (a % 4);
    for (lane = 0; lane < 4; lane++)
        if (tr.strb[i][lane])                             // strobe 켜진 레인만
            mem[word_base + lane] = tr.data[i][8*lane +: 8];
end

WRAP 버그를 잡는 원리

WRAP write @0x0A08 (윈도우 0x0A00~0x0A0F):
  ref_model이 예측한 주소 : 0x0A08, 0x0A0C, 0x0A00, 0x0A04   ← 랩
  DUT가 실제로 쓴 주소    : 0x0A08, 0x0A0C, 0x0A10, 0x0A14   ← 선형

모델은 0x0A00/0x0A04에 데이터가 있다고 예측 → DUT는 안 씀 → 미스매치!

beat_addr()가 자극과 모델 양쪽의 "올바른 주소" 단일 출처라는 게 핵심 설계.


2.7 Scoreboard

uvm_analysis_imp #(axi4_seq_item, axi4_scoreboard) ap_imp;

function void write(axi4_seq_item tr);       // 모니터가 방송할 때마다 호출
    if (tr.dir == AXI_WRITE) ref_model.apply_write(tr);   // 예측
    else                     check_read(tr);              // 검사

비교 시 lane 마스킹

mask = tr.lane_mask(i);                       // 이 beat가 실제 주소지정한 레인
for (lane = 0; lane < 4; lane++) begin
    if (!mask[lane]) continue;                // 주소지정 안 된 레인은 don't-care
    if (tr.data[i][8*lane +: 8] !== exp[8*lane +: 8]) `uvm_error(...)
end

narrow transfer에서 나머지 레인은 AXI상 don't-care라 다 비교하면 가짜 실패가 난다.


2.8 Functional Coverage

스코어보드는 "맞았나?", 커버리지는 "시험이나 해봤나?" — 둘 다 필요.

coverpoint무엇
cp_dirread / write
cp_burstFIXED / INCR / WRAP
cp_size1 / 2 / 4 바이트
cp_len1 / 2-4 / 5-8 / 9-16 beats
cp_respOKAY (나머지는 도달 불가)
cp_region주소 상위 4비트 (4KB 영역 16개)
cp_strbFULL / PARTIAL / NONE

cross: burst × size, burst × len, dir × burst

ignore_bins — 도달 불가를 근거와 함께 제외

// DUT가 resp를 OKAY로 하드코딩 → 에러 응답 자체가 불가능
ignore_bins unreachable = {AXI_EXOKAY, AXI_SLVERR, AXI_DECERR};

// 1-beat WRAP은 AXI 규격상 불법 (WRAP은 2/4/8/16만)
ignore_bins wrap_single = binsof(cp_burst.wrap) && binsof(cp_len.single);

도달 불가 빈은 방치하지 말고 이유를 명시해야 한다. 안 그러면 "커버리지 98%"가
진짜 구멍인지 원래 불가능한 건지 아무도 모른다.

마감 과정 (실제로 겪은 흐름)

단계커버리지한 일
랜덤만98.33%제약 랜덤의 한계
리포트로 홀 특정 → x_burst_len 12개 중 10개
ignore_bins90.91%불가능 빈 제외 (분모 12→11)
directed sweep100%모든 (burst × 길이) 조합 명시적으로

교훈: 제약 랜덤은 90%까지, 마지막 몇 %는 거의 항상 directed가 필요하다.


2.9 SVA (프로토콜 어서션)

bindRTL·TB를 안 건드리고 체커를 주입한다 (실무 표준 패턴).

주요 검사:

  • VALID는 READY 전에 안 내려감 (5개 채널)
  • 대기 중 payload 안정성
  • 예약 인코딩 2'b11 금지, size가 버스 폭 초과 금지
  • 유효 신호에 X/Z 금지
  • 리셋 중 응답 금지
  • WLAST/RLAST가 AWLEN/ARLEN이 지정한 beat에 위치 (정적 원형버퍼로 추적)

SVA vs Scoreboard 역할 구분 ⭐

질문WRAP 버그를 잡나?
SVA"버스가 규칙대로 동작하나?"❌ (핸드셰이크는 정상이었음)
Scoreboard"데이터가 맞나?"

WRAP 버그는 데이터가 잘못된 위치에 저장되는 기능 버그이지 프로토콜 위반이 아니다.
둘은 보완재이지 대체재가 아니다.


2.10 DUT v1 vs v2

v1 (axi_ram.v)

WRITE_STATE_IDLE: s_axi_awready_next = 1'b1;   // IDLE일 때만 주소 받음

주소를 받는 순간 엔진이 그 트랜잭션에 묶인다 → outstanding 불가.
그리고 burst != FIXED면 무조건 addr + (1<<size)WRAP 미구현 (버그).

v2 (Axi_ram_v2.v)

① 큐로 인터페이스와 엔진을 분리

assign s_axi_awready = !aw_full;   // ★ 엔진 상태와 무관! 큐 자리만 있으면 받음

이 한 줄이 outstanding 지원의 핵심. 큐 3개(aw_q, ar_q, b_q).
b_q 덕분에 v1의 RESP 상태가 사라짐 (3-state → 2-state).

② WRAP 제대로 구현

total_bytes    = (len + 1) << size;      // 윈도우 크기
wrap_mask      = total_bytes - 1;        // 예: 16-1 = 0x000F
wrap_base      = addr & ~wrap_mask;      // 윈도우 시작
next_addr_calc = (inc & wrap_mask) | wrap_base;   // ★ 오프셋만 남기고 복귀

WRAP 길이가 2/4/8/16으로 제한되는 이유가 여기 있다 —
윈도우가 2의 거듭제곱이라 마스크 한 번으로 끝난다.

③ credit-based flow control

wire w_can_start = !aw_empty & !b_full;   // 출구 자리를 확보한 뒤 입구를 연다

버스트를 시작해놓고 응답 큐가 꽉 차서 멈추는 상황을 원천 차단 (데드락 회피 기본 패턴).

④ FIFO에 별도 카운터를 쓰는 이유
포인터만 쓰면 wr_ptr == rd_ptr일 때 full/empty 구분이 안 된다.
해법은 ① 포인터에 1비트 추가 ② 별도 카운터 — v2는 ②.
(CNT_W = $clog2(DEPTH+1): 0~4를 표현해야 하므로 포인터보다 1비트 더)

v2가 여전히 못 하는 것 (다음 업그레이드 대상)

한계내용
응답 코드 고정bresp/rresp OKAY 하드코딩 → 에러 보고 불가
in-order만실제 AXI는 ID별 out-of-order 허용, v2는 엄격한 FIFO
LOCK 무시배타적 접근 미구현
WRAP 길이 미검증불법 길이가 오면 마스크가 깨짐 (입력이 합법이라 가정)
read/write 해저드같은 주소에 대한 두 채널 간 순서 보장 없음

실측 비교

v1v2
axi4_wrap_test24 미스매치 🐛통과
peak outstanding24

v1의 2는 우연 — FSM이 BVALID를 올리는 같은 사이클에 AWREADY도 올려서
응답 대기 중 주소 하나를 더 받는다.


Part 3. 인터뷰 필수 개념 (우선순위)

#개념파일왜 중요
1트랜잭션 흐름 전체Layer B 전부모든 질문의 뿌리
2config_db + factory + phasebuild_phase들UVM 3대 기둥
3TLM (seq_item_port / analysis_port)driver, monitor, sb컴포넌트 간 통신
4driver 프로토콜 로직axi4_driver.svh유일한 "진짜 로직"
5scoreboard predict/checkscoreboard + ref_model"정답을 어떻게 아나"
6objectionbase_test run_phase"시뮬이 왜 안 끝나요"
7coverage 마감 + ignore_binscoverage"100%가 무슨 뜻"
8SVA vs scoreboardsva + scoreboard"둘 다 왜 필요"

Part 4. 반드시 외울 3개의 버그 스토리 ⭐

면접관은 문법보다 검증적 사고를 본다. 각각 30초 안에 설명할 수 있어야 한다.

1. DUT의 WRAP 버그 + 대칭 검사의 함정

WRAP write를 WRAP read로 되읽으면 통과한다 — DUT가 양쪽에서 똑같이 틀리니
자기가 잘못 쓴 그 자리에서 그대로 읽어온다. 참조 모델도 양쪽 다 올바르니 일관됨.
WRAP으로 쓰고 INCR로 읽어야 데이터가 물리적으로 어디 있는지 드러난다.

교훈: 쓴 방식 그대로 되읽는 검사는 주소 버그를 못 잡는다.

2. 내 모니터의 동시성 버그

파이프라이닝을 켜니 스코어보드가 48건 미스매치 → DUT를 지목. 범인은 순차 모니터.
8개 중 3개만 관측하고 있었다.

교훈: 스코어보드 실패가 DUT가 범인이란 뜻은 아니다. 검증 환경도 검증 대상이다.

3. SVA가 조용히 죽어 있던 문제

XSim이 untyped property 인자를 경고만 내고 무시 → 어서션 전체가 vacuous.
실제 어서션을 뒤집어 5번 발화(관측된 AWVALID 5사이클과 일치)를 확인해 증명.

교훈: 어서션이 통과하는 것과 어서션이 존재하는 것은 다르다.


Part 5. 능동 학습 연습

  1. 백지 아키텍처 — 7개 컴포넌트 + 데이터 흐름 화살표 그리기
  2. 드라이버 재현aw_thread를 코드 덮고 써보기. 막히는 곳이 약점
  3. 파형 ↔ 코드 연결awvalid가 뜬 순간이 코드 어느 줄인지 짚기
  4. 버그 3개 말로 설명 — 각각 30초
  5. 커밋 로그 읽기git log. 왜 그렇게 했는지, 무슨 버그를 잡았는지 다 있다.
    코드만 봐선 안 보이는 "의사결정"이라 면접에서 금값
  6. v1 vs v2 비교 실행run_axi.bat 8번 vs 9번. 같은 테스트, 다른 결과

Part 6. 추천 일정 (하루 1~2시간)

일차목표
1일Layer A + Part 2.1~2.2. 파형 체크리스트 8개 확인
2~3일Layer B + Part 2.3~2.4. 드라이버 백지 재현
4일Part 2.5~2.7 (monitor, ref model, scoreboard)
5일Layer C + Part 2.8~2.9 (coverage, SVA)
6일Part 2.10 (v1 vs v2) + 버그 3개 스토리
7일전체 복습 + 예상 질문 소리 내어 답하기

Part 7. 예상 면접 질문

UVM 구조

  • 컴포넌트가 왜 phase로 나뉘나? build/connect/run의 순서와 방향은?
  • config_db는 왜 필요한가? 없으면 어떻게 되나?
  • factory를 왜 쓰나? newtype_id::create의 차이는?
  • driver와 sequencer는 어떻게 통신하나?
  • monitor는 왜 driver와 분리되어 있나?
  • analysis port가 1:N인 이유는?
  • objection을 안 걸면 어떻게 되나?

검증 방법론

  • scoreboard는 정답을 어떻게 아는가?
  • reference model을 DUT와 독립으로 만드는 이유는?
  • functional coverage 100%가 "검증 끝"을 의미하나? (아니오 — 자극했다는 뜻일 뿐)
  • ignore_bins는 언제 쓰나? 남용하면 어떤 위험이 있나?
  • SVA와 scoreboard의 역할 차이는? WRAP 버그를 SVA가 못 잡은 이유는?
  • 커버리지가 100%인데 버그가 남아있을 수 있나? (있다 — 커버리지 모델 자체의 한계)

AXI 프로토콜

  • 핸드셰이크 규칙은? VALID를 내릴 수 있는 시점은?
  • 4KB 경계 규칙이 왜 있나?
  • WRAP 길이가 2/4/8/16으로 제한되는 이유는?
  • narrow transfer에서 wstrb는 어떻게 정해지나?
  • outstanding transaction을 슬레이브가 어떻게 지원하나?

이 프로젝트 특화

  • 드라이버에서 AW와 W를 왜 분리했나?
  • 파이프라인 드라이버에서 read가 write를 추월하지 않게 하려면?
  • 모니터가 outstanding을 못 따라가면 어떤 증상이 나타나나?
  • 어서션이 실제로 동작하는지 어떻게 확인했나?

가장 중요한 조언

이 프로젝트의 진짜 가치는 "동작하는 UVM 코드"가 아니라
발견한 3개의 버그와 그 과정이다. 코드 100줄 외우는 것보다
Part 4의 세 문장을 자기 말로 설명하는 게 훨씬 강력하다.

profile
Design Verification engineer

0개의 댓글