HDL (Hardware Description Language)
하드웨어 기술 언어(HDL)의 심화 내용을 다룹니다. Verilog/SystemVerilog의 모듈 구조부터 FSM 패턴, 파라미터화, interface, 어서션(SVA), constrained random 검증, 기능 커버리지까지, 그리고 VHDL의 Entity/Architecture, Process, 패키지, Generic/Generate, FSM 패턴, VHDL-2008 개선사항, HDL 3종 비교, 차세대 HDL(Chisel, SpinalHDL, Amaranth), RTL 코딩 스타일을 종합적으로 정리합니다.
핵심 요약
- Verilog/SystemVerilog — C 계열 문법의 HDL로, 미국·아시아 산업계에서 주류입니다. SystemVerilog(IEEE 1800)는 검증 기능과 객체 지향 프로그래밍을 추가한 확장입니다
- VHDL — Ada 계열 문법의 HDL로, 유럽·방산·항공우주 분야에서 주류입니다. 강한 타입 시스템으로 컴파일 시점 오류 검출이 우수합니다
- RTL (Register Transfer Level) — 합성 가능한 수준의 하드웨어 기술 방법으로, 레지스터 간 데이터 흐름과 조합 논리를 기술합니다
- 합성 (Synthesis) — HDL 코드를 실제 게이트 레벨 넷리스트로 변환하는 과정입니다. 시뮬레이션 전용 구문은 합성되지 않습니다
단계별 이해
- 1단계 — 기본 문법: 모듈(module/entity) 구조, 포트 선언, 와이어/레지스터 타입을 이해합니다
- 2단계 — 조합/순차 논리: always_comb/always_ff (Verilog) 또는 process (VHDL)로 조합 논리와 플립플롭을 기술합니다
- 3단계 — FSM 패턴: 상태 머신 코딩 스타일(3-프로세스, 2-프로세스)을 익힙니다
- 4단계 — 파라미터화와 재사용: parameter/generic, generate로 범용 모듈을 설계합니다
- 5단계 — 검증 기법: 테스트벤치, SVA 어서션, constrained random, 기능 커버리지를 활용합니다
- 6단계 — RTL 코딩 규칙: 래치 방지, 리셋 전략, CDC 규칙 등 합성 품질을 높이는 패턴을 따릅니다
처음 읽는 RTL의 해석 순서
HDL 입문에서 가장 중요한 습관은 코드를 위에서 아래로 "실행"한다고 생각하지 않는 것입니다. RTL(Register Transfer Level)을 읽을 때는 문장 순서가 아니라 회로 역할 순서로 읽어야 합니다. 가장 안전한 읽기 순서는 다음과 같습니다.
- 모듈 경계 — 입력과 출력 포트를 보고, 이 블록이 시스템에서 무엇을 받아 무엇을 내보내는지 먼저 파악합니다
- 클록·리셋 — 어떤 신호가 상태를 갱신하는지 확인합니다.
always_ff또는process(clk)가 보이면 순차 논리가 있는 뜻입니다 - 조합 논리 —
assign,always_comb,process(all)블록이 현재 입력으로 무엇을 계산하는지 봅니다 - 상태 변수 —
count,state,busy처럼 다음 사이클로 넘어가는 값이 무엇인지 확인합니다 - 인터페이스 규칙 — 유효(valid), 준비(ready), 칩 선택(chip select), 리셋 극성처럼 블록 외부와의 약속을 마지막에 정리합니다
입문 단계에서는 이 흐름만 정확히 지켜도 코드를 훨씬 덜 두려워하게 됩니다. 특히 조합 논리와 순차 논리를 분리해서 읽는 습관은 래치 생성, 블로킹/논블로킹 할당 혼동, 리셋 누락 같은 초보자 실수를 줄이는 데 매우 효과적입니다.
가장 작은 RTL 예제를 줄 단위로 읽기
다음 예제는 "입력이 들어오면 카운트를 증가시키고, 마지막 값에 도달하면 완료 플래그를 올리는" 단순한 RTL입니다. 규모는 작지만, 포트 선언, 조합 논리, 상태 갱신, 출력 생성이 모두 들어 있어 기초 학습에 적합합니다.
module packet_counter (
input logic clk,
input logic rst_n,
input logic sample_valid,
output logic done,
output logic [3:0] count
);
logic [3:0] next_count;
always_comb begin
next_count = count;
if (sample_valid && count != 4'd9)
next_count = count + 1'b1;
else if (sample_valid && count == 4'd9)
next_count = 4'd0;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else
count <= next_count;
end
assign done = sample_valid && (count == 4'd9);
endmodule
| 코드 조각 | 읽는 질문 | 핵심 의미 |
|---|---|---|
input/output 포트 | 이 블록이 외부와 무엇을 주고받습니까? | 클록, 리셋, 입력 사건, 완료 신호, 카운트 값의 다섯 개 약속을 정의합니다 |
always_comb | 현재 입력과 현재 상태로 다음 값을 어떻게 계산합니까? | sample_valid가 오면 next_count를 갱신합니다. 아직 저장은 하지 않습니다 |
always_ff | 언제 상태가 실제로 바뀝니까? | 클록 상승 에지에서만 count가 갱신됩니다. 이것이 순차 논리의 핵심입니다 |
assign done | 출력은 현재 상태를 어떻게 외부에 보여 줍니까? | 현재 값이 9이고 입력이 동시에 들어온 순간에 완료를 알립니다 |
이 예제를 읽는 핵심은 next_count와 count를 분리해서 보는 것입니다. next_count는 "이번 사이클에 계산한 후보값"이고, count는 "실제로 저장되어 다음 사이클까지 유지되는 상태"입니다. 입문자가 가장 빨리 성장하는 지점이 바로 이 차이를 몸에 익히는 순간입니다.
입문 단계에서 자주 하는 실수
| 실수 | 왜 발생합니까? | 교정 방법 |
|---|---|---|
| 조합 논리에서 일부 분기에만 값을 할당합니다 | 소프트웨어의 if 문처럼 생각하기 때문입니다 | 기본값을 먼저 두고, 모든 분기에서 값이 정해지도록 작성합니다 |
=와 <=를 섞어 씁니다 | 하드웨어와 시뮬레이션 스케줄 차이를 체감하지 못했기 때문입니다 | 조합 논리는 블로킹, 순차 논리는 논블로킹이라는 기본 규칙을 먼저 고정합니다 |
| 리셋이 필요한 상태와 불필요한 상태를 구분하지 못합니다 | 모든 레지스터를 무조건 리셋하려는 습관 때문입니다 | 관측 가능 상태, 프로토콜 초기값, 디버그 필요성을 기준으로 리셋 대상을 정합니다 |
| 테스트벤치 없이 코드를 합성합니다 | CPU 프로그램처럼 "일단 돌려 보면 됩니다"라고 생각하기 때문입니다 | 작은 모듈이라도 최소한의 자체 검증 테스트벤치를 먼저 붙입니다 |
하드웨어와 소프트웨어의 차이
HDL을 배우는 소프트웨어 개발자가 가장 먼저 넘어야 할 벽은 사고 방식의 전환입니다. 프로그래밍 언어는 CPU가 명령을 하나씩 실행하는 절차를 기술하지만, HDL은 동시에 존재하는 회로 구조를 기술합니다.
순차 실행 vs 병렬 실행
소프트웨어에서 for 문으로 1,000번 반복하면 CPU는 1,000 사이클(또는 그 이상)을 소모합니다. 반면 하드웨어에서 1,000개의 AND 게이트를 배치하면, 모든 게이트가 동시에 1 사이클 만에 결과를 출력합니다.
- 소프트웨어 모델: 프로그램 카운터(PC)가 한 줄씩 전진하며 명령을 실행합니다. 분기, 루프, 함수 호출이 모두 순차적입니다
- 하드웨어 모델: 모든
assign문과always블록이 동시에 동작합니다. 코드의 위치(위/아래)는 실행 순서와 무관합니다
// 소프트웨어 관점: 순차 실행 (의사 코드)
// result[0] = a[0] & b[0]; → 1사이클
// result[1] = a[1] & b[1]; → 2사이클
// ... 1000번 반복 → 1000사이클
// 하드웨어 관점: 병렬 실행 (모두 동시)
assign result[0] = a[0] & b[0]; // ─┐
assign result[1] = a[1] & b[1]; // │ 모두 동시에
// ... // │ 1 게이트 지연
assign result[999] = a[999] & b[999]; // ─┘
이 차이 때문에 HDL에서는 "실행 순서"가 아닌 "연결 구조"를 생각해야 합니다. 아래 두 코드는 작성 순서가 다르지만 완전히 동일한 하드웨어를 생성합니다.
// 순서 A
assign x = a & b;
assign y = x | c;
// 순서 B — 위와 완전히 동일한 회로
assign y = x | c;
assign x = a & b;
클록과 동기식 설계
디지털 회로에서 클록(Clock)은 모든 동작의 기준 박자입니다. 클록 신호는 0과 1을 주기적으로 반복하며, 이 주기를 기준으로 레지스터가 데이터를 저장합니다.
- 클록 주기(Period)와 주파수(Frequency): 100 MHz 클록은 주기가 10 ns(나노초)입니다.
T = 1/f의 관계입니다 - 상승 에지(posedge): 클록이 0에서 1로 전환되는 순간입니다. 대부분의 설계에서 데이터 캡처 시점입니다
- 하강 에지(negedge): 클록이 1에서 0으로 전환되는 순간입니다. DDR(Double Data Rate) 등 특수한 경우에 사용됩니다
- 동기식 설계(Synchronous Design): 모든 상태 변화를 클록 에지에 동기화합니다. 이 원칙을 지키면 타이밍 분석이 예측 가능해지고, 도구가 자동으로 타이밍을 검증할 수 있습니다
- Fmax: 설계가 안정적으로 동작할 수 있는 최대 클록 주파수입니다. 조합 논리의 지연이 길수록 Fmax가 낮아집니다
셋업 타임(Setup Time)과 홀드 타임(Hold Time)을 위반하면 플립플롭이 메타스테이블(Metastable) 상태에 빠져 출력이 불확정(0도 1도 아닌 중간 상태)이 됩니다. 합성 도구는 자동으로 셋업/홀드 타이밍을 검증하지만, 클록 도메인 교차(CDC) 경로는 설계자가 직접 관리해야 합니다.
// 클록 에지에 동기화된 순차 논리 — 동기식 설계의 기본
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= '0; // 비동기 리셋
else
q <= d; // 상승 에지에서 d 값을 캡처
end
첫 번째 프로그램: 4비트 카운터
소프트웨어의 "Hello, World!"에 해당하는 것이 HDL에서는 카운터입니다. 동일한 4비트 카운터를 Verilog와 VHDL 두 언어로 작성하여 비교합니다.
// Verilog: 4비트 카운터
module counter4 (
input logic clk,
input logic rst_n,
output logic [3:0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0000;
else
count <= count + 1;
end
endmodule
-- VHDL: 4비트 카운터
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity counter4 is
port (
clk : in std_logic;
rst_n : in std_logic;
count : out std_logic_vector(3 downto 0)
);
end entity counter4;
architecture rtl of counter4 is
signal cnt : unsigned(3 downto 0) := (others => '0');
begin
process(clk, rst_n)
begin
if rst_n = '0' then
cnt <= (others => '0');
elsif rising_edge(clk) then
cnt <= cnt + 1;
end if;
end process;
count <= std_logic_vector(cnt);
end architecture rtl;
오픈 소스 시뮬레이터 Icarus Verilog로 Verilog 카운터를 시뮬레이션할 수 있습니다.
# 컴파일 및 시뮬레이션
iverilog -o sim counter4.v tb_counter4.v
vvp sim
# 파형 확인 (GTKWave)
gtkwave dump.vcd
iverilog는 Verilog 소스를 컴파일하고, vvp는 시뮬레이션을 실행합니다. 테스트벤치(tb_counter4.v)에서 $dumpfile/$dumpvars를 호출하면 파형 파일(.vcd)이 생성되며, GTKWave로 시각적으로 확인할 수 있습니다.
아래 파형은 위 4비트 카운터의 시뮬레이션 결과입니다. 파형(Waveform)은 HDL 디버깅의 기본 도구로, 각 신호의 시간에 따른 변화를 시각적으로 확인할 수 있습니다.
HDL 설계 시 주의사항
HDL 초급자가 반복적으로 겪는 4가지 실수와 해결 방법을 정리합니다. 이 실수들을 미리 이해하면 디버깅 시간을 크게 줄일 수 있습니다.
감도 리스트 누락
Verilog의 always 블록은 감도 리스트(Sensitivity List)에 지정된 신호가 변할 때만 실행됩니다. 감도 리스트에서 신호를 빠뜨리면 시뮬레이션과 합성 결과가 달라지는 심각한 문제가 발생합니다. 시뮬레이션에서는 감도 리스트를 그대로 따르지만, 합성 도구는 감도 리스트를 무시하고 블록 내부에서 읽히는 모든 신호를 입력으로 연결합니다. 예를 들어 always @(a)에서 b를 빠뜨리면, 시뮬레이션에서는 b가 변해도 출력이 갱신되지 않지만, 합성된 하드웨어에서는 b 변화가 즉시 출력에 반영됩니다.
// 문제: 감도 리스트에 b가 빠짐
always @(a) begin
y = a & b; // 시뮬: a 변할 때만 실행 / 합성: a, b 모두 반영
end
// 해결 1: Verilog-2001 와일드카드
always @(*) begin
y = a & b; // 모든 입력 신호 자동 포함
end
// 해결 2: SystemVerilog always_comb (권장)
always_comb begin
y = a & b; // 감도 리스트 자동 관리 + 래치 검사
end
always_comb를 사용하면 감도 리스트를 자동으로 관리합니다. 또한 의도하지 않은 래치가 생성될 경우 컴파일러가 경고를 발생시킵니다.
always_ff에서 블로킹 할당
순차 논리(always_ff)에서 블로킹 할당(=)을 사용하면 시프트 레지스터와 같은 파이프라인 구조가 의도대로 동작하지 않습니다.
// 잘못된 코드: = (블로킹) → 한 사이클에 전부 같은 값
always_ff @(posedge clk) begin
stage1 = din; // stage1 즉시 갱신
stage2 = stage1; // 이미 갱신된 stage1(=din) 사용
stage3 = stage2; // 이미 갱신된 stage2(=din) 사용
// 결과: stage1 = stage2 = stage3 = din (시프트 안 됨)
end
// 올바른 코드: <= (논블로킹) → 정상 시프트
always_ff @(posedge clk) begin
stage1 <= din; // 모든 RHS 먼저 평가
stage2 <= stage1; // 이전 stage1 값 사용
stage3 <= stage2; // 이전 stage2 값 사용
// 결과: din → stage1 → stage2 → stage3 (정상 시프트)
end
규칙: always_ff 내부에서는 반드시 논블로킹 할당(<=)을 사용합니다.
다중 드라이버
두 개 이상의 always 블록이 같은 신호를 할당하면 합성 도구가 에러를 발생시킵니다. 시뮬레이션에서는 경합 조건(Race Condition)이 발생하여 예측 불가능한 결과가 나타납니다.
// 잘못된 코드: 두 always 블록이 같은 신호 할당
always_ff @(posedge clk)
if (write_en) data_out <= write_data;
always_ff @(posedge clk)
if (clear) data_out <= '0;
// 합성 에러: "ERROR: Signal 'data_out' is driven by multiple sources"
// 해결: 하나의 always 블록에서 모든 조건 처리
always_ff @(posedge clk) begin
if (clear)
data_out <= '0;
else if (write_en)
data_out <= write_data;
end
하나의 신호는 반드시 하나의 always 블록에서만 할당해야 합니다. 여러 소스에서 선택해야 하는 경우에는 MUX(멀티플렉서) 구조로 우선순위를 명시합니다. 시뮬레이션에서 다중 드라이버가 충돌하면 해당 신호는 X(불확정) 상태가 되어, 이 신호에 의존하는 모든 후속 로직도 X로 전파됩니다.
의도하지 않은 래치
조합 논리에서 모든 조건 경로에 출력을 할당하지 않으면, 합성 도구가 이전 값을 유지하기 위해 래치(Latch)를 추론합니다. 래치는 타이밍 분석을 어렵게 만들고, 대부분의 FPGA/ASIC 설계에서 의도하지 않은 버그의 원인이 됩니다.
// 문제: else 누락 → 래치 추론
always_comb begin
if (sel)
y = a;
// sel=0일 때 y에 대한 할당 없음 → 이전 값 유지 → 래치!
end
// 해결 1: else 분기 추가
always_comb begin
if (sel)
y = a;
else
y = 1'b0;
end
// 해결 2: 블록 시작에 기본값 할당 (권장)
always_comb begin
y = 1'b0; // 기본값: 모든 경로 보장
if (sel)
y = a;
end
case 문에서 default를 생략해도 동일한 래치 문제가 발생합니다. 합성 도구가 "WARNING: [Synth 8-327] inferring latch for variable 'y'" 경고를 출력하면, 해당 변수의 모든 분기에서 할당이 이루어지는지 확인해야 합니다.
Verilog & SystemVerilog
Verilog는 IEEE 1364 표준으로 정의된 하드웨어 기술 언어이며, 1984년 Gateway Design Automation에서 처음 개발되었습니다. 이후 IEEE 1800 표준인 SystemVerilog로 확장되어 검증(verification) 기능과 객체 지향 프로그래밍 개념이 추가되었습니다. 현재 대부분의 FPGA/ASIC 설계는 SystemVerilog를 기본 언어로 사용합니다.
모듈 구조
모듈은 레고 블록과 같습니다. 각 블록에는 연결 지점(포트)이 있고, 내부에 특정 기능의 회로를 담고 있습니다. 작은 모듈들을 조합하여 복잡한 시스템을 만들 수 있습니다. 소프트웨어의 함수(function)와 비슷하지만, 각 모듈은 물리적인 회로 블록으로 합성되어 동시에 동작합니다.
Verilog의 기본 설계 단위는 모듈(module)입니다. 모듈은 입출력 포트를 가지며, 내부에 회로 동작을 기술합니다.
module/endmodule: 설계 단위의 시작과 끝을 정의합니다input/output/inout: 포트 방향을 지정합니다wire: 조합 논리의 연결선으로, 연속 할당(assign)의 대상입니다reg: 값을 저장할 수 있는 변수로,always블록 내부에서 할당합니다. 이름과 달리 반드시 레지스터로 합성되는 것은 아닙니다logic(SystemVerilog):wire와reg의 구분을 통합한 타입으로, 컨텍스트에 따라 자동으로 결정됩니다
wire는 전선(직접 연결)과 같아서 입력이 바뀌면 출력이 즉시 반영됩니다. reg는 메모장(값을 기억)과 같아서 클록 에지에서만 새 값을 기록합니다. SystemVerilog의 logic은 만능 타입으로, 컴파일러가 사용 맥락에 따라 자동으로 wire 또는 reg로 결정합니다.
| 사용 위치 | wire | reg | logic (SystemVerilog) |
|---|---|---|---|
assign 문의 대상 | ✓ | ✗ | ✓ |
always 블록의 대상 | ✗ | ✓ | ✓ |
| 모듈 입력 포트 | ✓ (기본) | ✗ | ✓ |
| 모듈 출력 포트 | 조합 논리 시 | 순차 논리 시 | ✓ |
logic만 사용하면 됩니다. wire/reg 구분은 Verilog-2001 호환이 필요한 경우에만 고려하세요.
모듈 인스턴스화 — 구조적 설계
레고 블록을 조립하듯, 작은 모듈을 큰 모듈 안에 인스턴스화(Instantiation)하여 복잡한 시스템을 구성합니다. 이를 구조적 설계(Structural Design)라 합니다. 지금까지의 예제는 모두 행위(Behavioral) 기술이었지만, 실제 프로젝트에서는 두 방식을 혼합하여 사용합니다.
// 하위 모듈: half_adder
module half_adder (
input logic a, b,
output logic sum, carry
);
assign sum = a ^ b;
assign carry = a & b;
endmodule
// 상위 모듈: full_adder (half_adder 2개를 인스턴스화)
module full_adder (
input logic a, b, cin,
output logic sum, cout
);
logic w_s0, w_c0, w_c1; // 내부 연결 와이어
// 인스턴스화: .포트이름(연결할신호)
half_adder u0 (.a(a), .b(b), .sum(w_s0), .carry(w_c0));
half_adder u1 (.a(w_s0), .b(cin), .sum(sum), .carry(w_c1));
assign cout = w_c0 | w_c1;
endmodule
코드 설명
- 이름 기반 연결
.port(wire): 포트 이름을 명시하여 연결합니다. 순서가 바뀌어도 안전하므로 항상 이 방식을 권장합니다 - 순서 기반 연결:
half_adder u0 (a, b, w_s0, w_c0);처럼 선언 순서로 연결할 수도 있지만, 포트 순서가 바뀌면 버그가 발생하므로 권장하지 않습니다 - 내부 와이어:
w_s0,w_c0,w_c1은 하위 모듈 간 연결을 위한 내부 신호입니다. 모듈 외부에는 노출되지 않습니다 - 합성 결과: 각
half_adder인스턴스는 독립된 하드웨어 블록으로 합성되어 동시에 동작합니다
조합 논리와 순차 논리
디지털 회로는 크게 조합 논리(Combinational Logic)와 순차 논리(Sequential Logic)로 나뉩니다. 이 구분은 HDL 코딩에서 가장 중요한 개념입니다.
조합 논리 vs 순차 논리 — 타이밍 동작 차이
위 다이어그램은 회로 구조를 보여주지만, 실제 신호가 어떻게 전파되는지는 타이밍 파형으로 이해하는 것이 직관적입니다. 조합 논리는 입력 변화에 즉시 반응하고, 순차 논리는 다음 클록 에지에서 출력이 갱신됩니다.
- 조합 논리: 입력이 바뀌면 즉시 출력이 결정됩니다. 클록에 의존하지 않습니다.
- Verilog:
always @(*)또는assign - SystemVerilog:
always_comb(래치 추론 방지 기능 내장)
- Verilog:
- 순차 논리: 클록 에지(edge)에서만 출력이 갱신됩니다. 플립플롭(Flip-Flop)으로 합성됩니다.
- Verilog:
always @(posedge clk) - SystemVerilog:
always_ff @(posedge clk)
- Verilog:
always_comb는 SystemVerilog에서 always @(*)를 대체하는 권장 구문입니다. 차이점은 (1) 감도 리스트를 자동으로 구성하여 누락 실수를 방지하고, (2) 래치가 추론되면 합성 도구가 오류를 발생시킵니다(always @(*)는 경고만 발생). 새 설계에서는 반드시 always_comb를 사용하는 것을 권장합니다.
블로킹과 논블로킹 할당
Verilog에서 가장 흔한 실수 중 하나는 할당 연산자의 잘못된 사용입니다. 두 종류의 할당 연산자는 시뮬레이션 동작과 합성 결과에 직접적인 영향을 미칩니다.
- 블로킹 할당(Blocking Assignment,
=): 순차적으로 실행됩니다. 조합 논리(always_comb)에서 사용해야 합니다 - 논블로킹 할당(Non-blocking Assignment,
<=): 병렬로 실행됩니다. 순차 논리(always_ff)에서 사용해야 합니다
이 규칙을 지키지 않으면 시뮬레이션과 합성 결과가 달라지는 심각한 버그가 발생할 수 있습니다. 순차 논리 블록에서 블로킹 할당을 사용하면, 레지스터 간 데이터 전달 순서가 시뮬레이터의 이벤트 스케줄링에 의존하게 되어 비결정적 동작이 나타납니다.
이 차이를 가장 명확히 보여주는 예제가 시프트 레지스터입니다. 블로킹 할당(=)을 사용하면 모든 레지스터가 같은 값으로 덮어쓰여지고, 논블로킹 할당(<=)을 사용해야 정상적으로 데이터가 시프트됩니다.
// 잘못된 예: 블로킹 할당으로 시프트 레지스터 (동작 오류)
always_ff @(posedge clk) begin
stage1 = din; // stage1 즉시 갱신
stage2 = stage1; // stage2 = 새 stage1 = din (시프트 안 됨!)
stage3 = stage2; // stage3 = 새 stage2 = din
// 결과: stage1 = stage2 = stage3 = din (모두 같은 값)
end
// 올바른 예: 논블로킹 할당으로 시프트 레지스터
always_ff @(posedge clk) begin
stage1 <= din; // 이전 din 값을 stage1에 저장
stage2 <= stage1; // 이전 stage1 값을 stage2에 저장
stage3 <= stage2; // 이전 stage2 값을 stage3에 저장
// 결과: din → stage1 → stage2 → stage3 (정상 시프트)
end
논블로킹 할당에서는 모든 우변(RHS)이 먼저 평가된 후, 좌변(LHS)에 동시에 반영됩니다. 따라서 할당문의 순서에 관계없이 올바른 시프트 동작이 보장됩니다.
시프트 레지스터 동작 비교 — 블로킹 vs 논블로킹
always_comb)에서는 블로킹 할당(=)을, 순차 논리(always_ff)에서는 논블로킹 할당(<=)을 사용합니다. 하나의 always 블록에서 두 종류의 할당을 절대 섞지 않습니다.
파라미터화
재사용 가능한 IP를 만들기 위해서는 모듈을 파라미터화(Parameterization)해야 합니다.
parameter: 인스턴스화 시 외부에서 재정의할 수 있는 상수입니다localparam: 모듈 내부에서만 사용되며, 외부에서 재정의할 수 없는 상수입니다.parameter로부터 파생된 값을 정의할 때 사용합니다generate: 조건부 또는 반복적 하드웨어 구조를 생성합니다.genvar와 함께 사용하여 배열 형태의 인스턴스를 생성할 수 있습니다
parameter와 localparam의 핵심 차이는 외부 오버라이드 가능 여부입니다. parameter는 인스턴스화 시 #(...) 구문으로 값을 변경할 수 있지만, localparam은 모듈 내부에서 계산되어 외부에서 변경할 수 없습니다.
$clog2(N)은 N을 표현하는 데 필요한 비트 수(log₂의 올림)를 컴파일 시점에 계산하는 SystemVerilog 시스템 함수입니다. 예를 들어 $clog2(256) = 8, $clog2(100) = 7입니다. 메모리 깊이(DEPTH)로부터 주소 비트 수(ADDR_W)를 자동으로 도출할 때 필수적으로 사용됩니다.
module fifo #(
parameter integer WIDTH = 8,
parameter integer DEPTH = 256
) (
/* ... 포트 ... */
);
// localparam: DEPTH에서 자동 계산 (외부 변경 불가)
localparam integer ADDR_W = $clog2(DEPTH); // 256 → 8비트
localparam integer PTR_W = ADDR_W + 1; // Full/Empty 판별용
logic [PTR_W-1:0] wr_ptr, rd_ptr;
logic [WIDTH-1:0] mem [0:DEPTH-1];
endmodule
// 인스턴스화: #(.파라미터명(값)) 구문
fifo #(.WIDTH(16), .DEPTH(1024)) u_fifo (
.clk(sys_clk),
/* ... 나머지 포트 ... */
);
2:1 멀티플렉서
가장 간단한 조합 논리 예제입니다. 선택 신호(sel)에 따라 두 입력 중 하나를 출력으로 연결합니다.
// 방법 1: assign (단순한 표현에 적합)
module mux2to1_assign (
input logic a, b, sel,
output logic y
);
assign y = sel ? a : b;
endmodule
// 방법 2: always_comb (복잡한 조합 논리에 적합)
module mux2to1_comb (
input logic a, b, sel,
output logic y
);
always_comb begin
if (sel)
y = a;
else
y = b;
end
endmodule
두 코드는 동일한 하드웨어(MUX)로 합성됩니다. assign은 단순한 표현에, always_comb는 복잡한 조합 논리에 적합합니다. 4:1 MUX는 2비트 sel과 case문으로 확장할 수 있으며, FPGA의 LUT(Look-Up Table)가 기본적으로 소규모 MUX로 동작합니다.
3-to-8 디코더
디코더는 입력 코드를 해석하여 해당하는 출력 하나만 활성화합니다. case 문을 사용하여 직관적으로 구현할 수 있습니다.
module decoder3to8 (
input logic [2:0] in,
output logic [7:0] out
);
always_comb begin
out = 8'b0000_0000; // 기본값: 래치 방지 핵심
case (in)
3'd0: out = 8'b0000_0001;
3'd1: out = 8'b0000_0010;
3'd2: out = 8'b0000_0100;
3'd3: out = 8'b0000_1000;
3'd4: out = 8'b0001_0000;
3'd5: out = 8'b0010_0000;
3'd6: out = 8'b0100_0000;
3'd7: out = 8'b1000_0000;
default: out = 8'b0000_0000;
endcase
end
endmodule
case 문에서 default 분기를 생략하거나, 블록 시작 부분에 기본값을 할당하지 않으면 래치(Latch)가 추론됩니다. 반드시 모든 경로에서 출력이 할당되도록 보장하세요. 자세한 내용은 의도하지 않은 래치 섹션을 참고하세요.
위 코드에서 out = 8'b0000_0000; 기본값 할당이 핵심입니다. 이것이 없으면 default가 있어도, case 문 밖에서의 값이 정의되지 않아 래치가 추론될 수 있습니다. 또한 동일한 디코더를 assign out = 1 << in; 한 줄로도 구현할 수 있습니다 — 비트 시프트 연산이 디코딩과 수학적으로 동일하기 때문입니다.
우선순위 인코더
디코더의 역연산입니다. 여러 입력 중 가장 높은 우선순위(MSB 쪽)가 활성화된 위치를 이진 코드로 출력합니다. 인터럽트 컨트롤러에서 여러 인터럽트 요청 중 가장 높은 우선순위를 선택하거나, 아비터에서 여러 요청자 중 하나를 선택할 때 핵심적으로 사용됩니다.
// 파라미터화 우선순위 인코더
module priority_encoder #(
parameter int WIDTH = 8
) (
input logic [WIDTH-1:0] req,
output logic [$clog2(WIDTH)-1:0] idx,
output logic valid
);
always_comb begin
valid = |req; // OR 축약: 하나라도 1이면 valid
idx = '0;
// MSB부터 스캔 — 가장 높은 우선순위 선택
for (int i = WIDTH-1; i >= 0; i--)
if (req[i]) begin
idx = i[$clog2(WIDTH)-1:0];
break; // 첫 번째 매칭에서 중단
end
end
endmodule
코드 설명
|req(OR 축약 연산자): 모든 비트를 OR하여 하나라도 1이면valid=1을 출력합니다.&req(AND 축약),^req(XOR 축약)도 동일한 패턴입니다for+break: SystemVerilog의break는 합성 가능한 구문으로, MSB부터 스캔하여 첫 번째 활성 비트에서 루프를 종료합니다. 합성 도구는 이를 우선순위 MUX 체인으로 변환합니다- 합성 결과:
casez기반 구현과 동일한 우선순위 MUX로 합성됩니다.for루프는 하드웨어 복제(unroll)되어 병렬 회로가 됩니다 - 응용: Linux 커널의
ffs()(Find First Set) 함수가 이 회로의 소프트웨어 등가물이며, 인터럽트 컨트롤러(GIC), 아비터, 리소스 할당기에서 핵심적으로 사용됩니다
인에이블 카운터
첫 프로그램의 4비트 카운터를 확장합니다. enable 입력을 추가하고, 최대값에 도달하면 자동으로 0으로 되돌아갑니다.
module counter_enable #(
parameter integer WIDTH = 4,
parameter integer MAX_VAL = 15
) (
input logic clk,
input logic rst_n,
input logic enable,
output logic [WIDTH-1:0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= '0;
else if (enable) begin
if (count == MAX_VAL[WIDTH-1:0])
count <= '0;
else
count <= count + 1;
end
end
endmodule
여러 조건을 우선순위에 따라 처리하는 패턴입니다: 리셋 → 인에이블 확인 → 최대값 확인 → 증가. 이 우선순위 체인은 순차 논리 설계에서 가장 자주 사용되는 구조입니다.
시프트 레지스터
8비트 Serial-In/Parallel-Out(SIPO) 시프트 레지스터입니다. 직렬 데이터를 병렬로 변환하는 기본 패턴으로, SPI, UART 등 직렬 통신 프로토콜에서 핵심적으로 사용됩니다.
module shift_reg_sipo #(
parameter integer WIDTH = 8
) (
input logic clk,
input logic rst_n,
input logic din, // 직렬 입력
output logic [WIDTH-1:0] dout // 병렬 출력
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= '0;
else
dout <= {dout[WIDTH-2:0], din}; // 왼쪽 시프트 + 새 비트 삽입
end
endmodule
연결(concatenation) 연산자 {}는 HDL에서 가장 자주 사용되는 비트 조작 도구입니다. {dout[6:0], din}은 기존 비트를 왼쪽으로 한 칸 밀고, 가장 오른쪽에 새 비트(din)를 삽입합니다.
에지 검출기
입력 신호의 상승 에지(0→1) 또는 하강 에지(1→0) 순간을 감지하여 1클록 폭의 펄스를 출력하는 가장 기본적인 순차 회로입니다. 인터럽트 에지 감지, 버튼 입력 처리, CDC 펄스 동기화 등 거의 모든 디지털 설계에서 활용됩니다.
// 에지 검출기 — 상승/하강 에지 펄스 생성
module edge_detector (
input logic clk, rst_n,
input logic signal,
output logic pos_edge, // 상승 에지 펄스 (1클록)
output logic neg_edge // 하강 에지 펄스 (1클록)
);
logic signal_d; // 1클록 지연된 입력
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) signal_d <= 1'b0;
else signal_d <= signal;
assign pos_edge = signal & ~signal_d; // 현재=1, 이전=0 → 상승
assign neg_edge = ~signal & signal_d; // 현재=0, 이전=1 → 하강
endmodule
주파수 분주기
입력 클록을 N분의 1로 분주하는 회로입니다. UART 보드레이트 생성, LED 점멸 타이머, 타이머 프리스케일러 등 클록 주파수 변환이 필요한 곳에 사용됩니다.
// 짝수 분주기 — 50% 듀티 사이클 보장
module clk_divider #(
parameter int DIV = 4 // 분주비 (짝수만 가능)
) (
input logic clk_in, rst_n,
output logic clk_out
);
localparam int HALF = DIV / 2 - 1;
logic [$clog2(DIV)-1:0] counter;
always_ff @(posedge clk_in or negedge rst_n)
if (!rst_n) begin
counter <= '0;
clk_out <= 1'b0;
end else if (counter == HALF) begin
counter <= '0;
clk_out <= ~clk_out; // 토글 → 50% 듀티
end else
counter <= counter + 1;
endmodule
clk_out)을 다른 모듈의 클록 입력으로 사용하면 CDC 문제가 발생할 수 있습니다. 가능하면 원본 클록을 유지하고, 클록 인에이블(CE) 방식으로 구현하는 것이 FPGA에서 권장됩니다.
PWM 생성기
카운터와 비교기를 조합하여 듀티 사이클(Duty Cycle)을 제어하는 펄스 폭 변조(PWM) 회로입니다. 카운터 값이 비교값(duty)보다 작을 때 출력이 HIGH, 크거나 같으면 LOW가 됩니다.
// N비트 PWM 생성기
module pwm_generator #(
parameter int WIDTH = 8 // 해상도: 2^WIDTH 단계
) (
input logic clk, rst_n,
input logic [WIDTH-1:0] duty, // 듀티 사이클 (0~2^WIDTH-1)
output logic pwm_out
);
logic [WIDTH-1:0] counter;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) counter <= '0;
else counter <= counter + 1; // 자동 롤오버
assign pwm_out = (counter < duty); // duty=0 → 항상 LOW, duty=max → 항상 HIGH
endmodule
8비트 PWM의 경우 듀티 사이클을 256단계로 제어할 수 있습니다. duty = 8'd192이면 75%(192/256) 듀티 사이클이 됩니다. LED 밝기 제어, 모터 속도 제어, DC-DC 컨버터 등에 활용됩니다.
LFSR (선형 피드백 시프트 레지스터)
시프트 레지스터의 특정 탭(Tap) 위치를 XOR로 피드백하여 의사 랜덤(Pseudo-Random) 시퀀스를 생성합니다. 최대 길이 다항식을 사용하면 2ⁿ-1 주기의 비반복 시퀀스가 생성됩니다. BIST(Built-In Self-Test) 패턴 생성, 데이터 스크램블링, CRC 계산에 활용됩니다.
// 갈루아(Galois) LFSR — 최대 길이 시퀀스
module lfsr #(
parameter int WIDTH = 8,
parameter logic [WIDTH-1:0] TAPS = 8'b10111000 // x⁸+x⁶+x⁵+x⁴+1
) (
input logic clk, rst_n,
input logic enable,
output logic [WIDTH-1:0] lfsr_out
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n)
lfsr_out <= '1; // 시드값 (0은 사용 불가 — 영원히 0)
else if (enable) begin
if (lfsr_out[0])
lfsr_out <= (lfsr_out >> 1) ^ TAPS;
else
lfsr_out <= lfsr_out >> 1;
end
endmodule
| 비트 수 | 최대 길이 다항식 (탭) | 주기 |
|---|---|---|
| 4 | x⁴ + x³ + 1 | 15 |
| 8 | x⁸ + x⁶ + x⁵ + x⁴ + 1 | 255 |
| 16 | x¹⁶ + x¹⁵ + x¹³ + x⁴ + 1 | 65,535 |
| 32 | x³² + x²² + x² + x + 1 | 4,294,967,295 |
0이면 영원히 0을 출력합니다. 리셋 시 반드시 0이 아닌 값으로 초기화해야 합니다. 갈루아 구조는 XOR 게이트가 병렬로 배치되어 피보나치 구조보다 Fmax가 높습니다.
게이트 레벨 모델링과 패리티 생성기
지금까지의 예제는 모두 행위(Behavioral) 수준 — always_comb, assign 연산자를 사용한 기술이었습니다. Verilog는 게이트 프리미티브(and, or, xor, nand, nor, not, buf)를 직접 인스턴스화하는 게이트 레벨(Gate-Level) 모델링도 지원합니다. 합성 도구가 자동으로 게이트를 생성하므로 RTL에서 직접 사용할 일은 드물지만, 합성 후 넷리스트 분석이나 교육 목적에서 이해가 필요합니다.
// 게이트 프리미티브로 구현한 전가산기 (Full Adder)
module full_adder_gate (
input logic a, b, cin,
output logic sum, cout
);
logic w1, w2, w3;
// 게이트 인스턴스: 프리미티브명 인스턴스명 (출력, 입력...)
xor g1 (w1, a, b); // w1 = a ⊕ b
xor g2 (sum, w1, cin); // sum = a ⊕ b ⊕ cin
and g3 (w2, w1, cin); // w2 = (a ⊕ b) · cin
and g4 (w3, a, b); // w3 = a · b
or g5 (cout, w2, w3); // cout = (a ⊕ b)·cin + a·b
endmodule
패리티 생성기/검사기
패리티(Parity)는 데이터 전송 오류를 감지하는 가장 기본적인 방법입니다. XOR의 축약 연산(^data)으로 모든 비트의 짝수/홀수 패리티를 한 줄로 구현할 수 있습니다. 합성 도구는 이를 XOR 트리(Tree)로 최적화합니다.
// 패리티 생성기/검사기
module parity #(
parameter int WIDTH = 8,
parameter bit ODD = 1 // 1=홀수 패리티, 0=짝수 패리티
) (
input logic [WIDTH-1:0] data,
output logic parity_bit // 생성: 데이터에 붙일 패리티
);
// ^data = 모든 비트의 XOR (짝수 패리티)
assign parity_bit = (^data) ^ ODD;
// 검사: {data, parity_bit} 전체를 XOR → 0이면 오류 없음
endmodule
합성 도구는 ^data를 2입력 XOR 게이트의 균형 트리(Balanced Tree)로 변환합니다. 8비트 데이터의 경우 3단계(log₂8) XOR 트리가 생성되며, 이는 체인 구조보다 전파 지연이 짧습니다. ECC(Error Correcting Code), SECDED(Single Error Correct, Double Error Detect)의 기초가 됩니다.
라운드 로빈 아비터
우선순위 인코더는 항상 MSB 요청을 선택하므로, 낮은 우선순위 요청이 영원히 서비스되지 않는 기아(Starvation) 문제가 있습니다. 라운드 로빈 아비터(Round-Robin Arbiter)는 마지막으로 서비스한 요청 다음부터 순환하여 공정한(Fair) 자원 할당을 보장합니다.
// 라운드 로빈 아비터 — 순환 우선순위
module round_robin_arbiter #(
parameter int N = 4 // 요청자 수
) (
input logic clk, rst_n,
input logic [N-1:0] req, // 요청 벡터
output logic [N-1:0] grant // 허가 (원-핫)
);
logic [N-1:0] mask; // 마스크: 이전에 서비스된 위치 이하를 차단
logic [N-1:0] masked_req, grant_masked, grant_unmasked;
// 마스크된 요청에서 가장 낮은 비트 선택 (LSB 우선)
assign masked_req = req & mask;
assign grant_masked = masked_req & (~masked_req + 1); // isolate lowest set bit
assign grant_unmasked = req & (~req + 1); // 마스크 없이 fallback
// 마스크된 요청이 있으면 그것을, 없으면 전체에서 선택
assign grant = (|masked_req) ? grant_masked : grant_unmasked;
// 허가 후 마스크 갱신: 허가된 위치 위쪽만 통과
always_ff @(posedge clk or negedge rst_n)
if (!rst_n)
mask <= {N{1'b1}};
else if (|grant)
mask <= ~(grant | (grant - 1)); // 허가 위치 이하 비트 마스크
endmodule
코드 설명
x & (~x + 1): 2의 보수 트릭으로 가장 낮은 설정 비트(Lowest Set Bit)를 분리합니다. 예:4'b1010 → 4'b0010. 하드웨어에서 1-LUT 체인으로 합성됩니다- 마스크 전략: 마지막 허가 위치 이하를
0으로 마스크하여, 다음 라운드에서 그 위치 위의 요청이 우선됩니다. 마스크된 요청이 없으면(모두 서비스 완료) 마스크를 무시하고 전체에서 선택합니다 - 원-핫(One-Hot) 출력:
grant는 항상 최대 1비트만 활성화되는 원-핫 벡터입니다. 디코더 없이 직접 자원 선택 MUX에 연결할 수 있습니다 - 응용: 버스 아비터(AXI Interconnect), 메모리 컨트롤러 포트 스케줄러, 네트워크 패킷 스케줄러에서 사용됩니다
그레이 코드 카운터
클록 도메인 교차(CDC)에서 멀티비트 포인터를 안전하게 전달하려면 그레이 코드가 필수입니다(CDC 코딩 규칙 참조). 다음은 바이너리 카운터 + 변환기를 조합한 완전한 그레이 코드 카운터입니다.
// 그레이 코드 카운터 — CDC FIFO 포인터에 사용
module gray_counter #(
parameter int WIDTH = 4
) (
input logic clk, rst_n, enable,
output logic [WIDTH-1:0] gray, // 그레이 코드 출력 (CDC 전달용)
output logic [WIDTH-1:0] binary // 바이너리 출력 (로컬 연산용)
);
logic [WIDTH-1:0] binary_next;
assign binary_next = binary + 1;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
binary <= '0;
gray <= '0;
end else if (enable) begin
binary <= binary_next;
gray <= binary_next ^ (binary_next >> 1); // bin→gray 변환
end
endmodule
그레이 코드의 핵심 속성은 인접한 값 사이에서 단 1비트만 변합니다는 것입니다. 바이너리 011→100은 3비트가 동시에 변하여 CDC에서 잘못된 중간값이 샘플링될 수 있지만, 그레이 코드 010→110은 1비트만 변하므로 2-FF 동기화기로 안전하게 전달됩니다.
트라이스테이트 버퍼와 양방향 I/O
FPGA의 I/O 패드는 입력과 출력을 하나의 핀으로 공유하는 양방향(Bidirectional) 포트를 지원합니다. Verilog의 inout 포트와 트라이스테이트('Z) 할당으로 구현합니다. I2C, 양방향 데이터 버스 등에 사용됩니다.
// 양방향 I/O — 트라이스테이트 버퍼 패턴
module bidir_io (
input logic clk, rst_n,
input logic oe, // 출력 인에이블
input logic [7:0] data_out, // 내부 → 외부 데이터
output logic [7:0] data_in, // 외부 → 내부 데이터
inout wire [7:0] data_pad // 물리 패드 (양방향)
);
// 출력: oe=1이면 구동, oe=0이면 하이임피던스(Z)
assign data_pad = oe ? data_out : 8'bZZZZ_ZZZZ;
// 입력: 패드에서 항상 읽기 (oe와 무관)
assign data_in = data_pad;
endmodule
inout 포트는 반드시 wire 타입이어야 합니다(logic 불가). 트라이스테이트는 FPGA 내부에서는 사용할 수 없으며, I/O 패드에서만 합성됩니다. FPGA 내부의 양방향 통신은 MUX 기반 방향 선택으로 구현해야 합니다.
레지스터 파일
레지스터 파일(Register File)은 CPU, DSP, GPU의 핵심 저장소입니다. 다수의 레지스터에 동시 읽기(multi-port read)와 쓰기(single/dual-port write)를 지원합니다. FPGA에서는 분산 RAM(LUT RAM) 또는 BRAM으로 합성되며, 읽기 포트가 비동기이면 분산 RAM, 동기이면 BRAM으로 추론됩니다.
// 2-읽기/1-쓰기 레지스터 파일
module register_file #(
parameter int DEPTH = 32, // 레지스터 수 (RISC-V: 32)
parameter int WIDTH = 32 // 데이터 폭
) (
input logic clk,
// 쓰기 포트
input logic we,
input logic [$clog2(DEPTH)-1:0] waddr,
input logic [WIDTH-1:0] wdata,
// 읽기 포트 A (비동기 — 분산 RAM 추론)
input logic [$clog2(DEPTH)-1:0] raddr_a,
output logic [WIDTH-1:0] rdata_a,
// 읽기 포트 B (비동기)
input logic [$clog2(DEPTH)-1:0] raddr_b,
output logic [WIDTH-1:0] rdata_b
);
logic [WIDTH-1:0] regs [0:DEPTH-1];
// 동기 쓰기
always_ff @(posedge clk)
if (we) regs[waddr] <= wdata;
// 비동기 읽기 (조합 논리 — 분산 RAM)
assign rdata_a = regs[raddr_a];
assign rdata_b = regs[raddr_b];
// RISC-V x0 하드와이어: 항상 0 반환 (필요 시 추가)
// assign rdata_a = (raddr_a == '0) ? '0 : regs[raddr_a];
endmodule
비동기 읽기(assign rdata = regs[addr])는 분산 RAM(LUT RAM)으로 합성됩니다. 동기 읽기(always_ff에서 읽기)로 변경하면 BRAM으로 합성되어 더 큰 깊이를 지원합니다. RISC-V의 x0 레지스터처럼 항상 0을 반환하는 하드와이어 레지스터는 읽기 경로에 MUX를 추가하여 구현합니다.
버튼 디바운서 FSM
가장 단순한 형태의 유한 상태 머신(FSM)입니다. 기계식 버튼을 누를 때 발생하는 바운스(떨림)를 걸러내는 디바운서를 2-상태 FSM으로 구현합니다.
module debouncer #(
parameter integer CNT_MAX = 20'd999_999 // 50MHz → ~20ms
) (
input logic clk,
input logic rst_n,
input logic btn_in,
output logic btn_out
);
typedef enum logic {IDLE, DEBOUNCE} state_t;
state_t state, next_state;
logic [19:0] cnt;
logic btn_sync;
// 상태 레지스터
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
cnt <= '0;
btn_out <= 1'b0;
btn_sync <= 1'b0;
end else begin
btn_sync <= btn_in;
state <= next_state;
case (next_state)
IDLE: cnt <= '0;
DEBOUNCE: cnt <= cnt + 1;
endcase
if (state == DEBOUNCE && cnt == CNT_MAX)
btn_out <= btn_sync;
end
end
// 다음 상태 논리
always_comb begin
next_state = state;
case (state)
IDLE: if (btn_sync != btn_out) next_state = DEBOUNCE;
DEBOUNCE: if (cnt == CNT_MAX) next_state = IDLE;
endcase
end
endmodule
이것이 FSM의 가장 단순한 형태입니다. 상태 레지스터(always_ff)와 다음 상태 논리(always_comb)를 분리하는 2-프로세스 패턴은 모든 FSM에 동일하게 적용됩니다. 이 패턴을 이해하면 UART, SPI, AXI 등 복잡한 프로토콜 FSM도 같은 구조로 설계할 수 있습니다. 더 상세한 FSM 코딩 패턴(3-프로세스 패턴, UART 수신기)은 아래 Verilog 심화 섹션에서 다룹니다.
첫 번째 테스트벤치
위의 인에이블 카운터에 대한 기본 테스트벤치입니다. 이것이 모든 테스트벤치의 뼈대이며, 여기에 자동 검증 로직을 추가하면 self-checking 테스트벤치가 됩니다.
module tb_counter_enable;
logic clk, rst_n, enable;
logic [3:0] count;
// DUT (Device Under Test) 인스턴스
counter_enable #(.WIDTH(4), .MAX_VAL(15)) u_dut (
.clk(clk), .rst_n(rst_n),
.enable(enable), .count(count)
);
// 클록 생성: 주기 10 단위 (5 + 5)
always #5 clk = ~clk;
initial begin
// 파형 덤프 설정
$dumpfile("tb_counter_enable.vcd");
$dumpvars(0, tb_counter_enable);
// 초기화
clk = 0; rst_n = 0; enable = 0;
// 리셋 해제 (2 클록 후)
#20 rst_n = 1;
// 인에이블 활성화
#10 enable = 1;
// 20 클록 동안 관찰
repeat (20) @(posedge clk)
$display("time=%0t count=%d enable=%b", $time, count, enable);
// 인에이블 비활성화 테스트
enable = 0;
repeat (5) @(posedge clk)
$display("time=%0t count=%d enable=%b (disabled)", $time, count, enable);
#20 $finish;
end
endmodule
자체 검증 패턴 추가
위 테스트벤치는 $display로 수동 확인하는 수준입니다. 실무에서는 기대값과 실제 출력을 자동으로 비교하는 자체 검증(Self-Checking) 패턴을 사용합니다. 다음 코드를 위 테스트벤치의 initial 블록에 추가하면 됩니다.
// 자체 검증 — 위 테스트벤치에 추가
int errors = 0;
task automatic check(logic [3:0] expected, string msg);
if (count !== expected) begin
$error("[%0t] %s: expected=%0d, got=%0d", $time, msg, expected, count);
errors++;
end
endtask
initial begin
// ... (클록, 파형 설정 생략) ...
clk = 0; rst_n = 0; enable = 0;
// 1. 리셋 검증: 리셋 중 카운터가 0인지 확인
#20 check(4'd0, "리셋 중 카운터");
rst_n = 1;
// 2. 인에이블 비활성 검증: 값이 유지되는지 확인
repeat (3) @(posedge clk);
check(4'd0, "인에이블 OFF 시 유지");
// 3. 인에이블 활성 검증: 정상 증가하는지 확인
enable = 1;
repeat (5) @(posedge clk);
check(4'd5, "5 사이클 후 카운터");
// 4. 롤오버 검증: MAX_VAL(15)에서 0으로 돌아오는지
repeat (11) @(posedge clk);
check(4'd0, "롤오버 후 카운터");
// 결과 요약
if (errors == 0) $display("=== ALL TESTS PASSED ===");
else $fatal(1, "=== %0d ERRORS ===", errors);
$finish;
end
SystemVerilog 확장
SystemVerilog(IEEE 1800)는 Verilog를 기반으로 설계와 검증 양쪽 모두를 강화한 언어입니다. 주요 확장 기능은 다음과 같습니다.
interface: 신호 그룹을 하나의 번들로 묶어 포트 연결을 단순화합니다. AXI 등의 복잡한 버스 인터페이스를 깔끔하게 표현할 수 있습니다logic:wire와reg를 대체하는 통합 타입입니다enum: 열거형 타입으로, FSM 상태 머신 코딩을 명확하게 합니다struct/union: C 언어와 유사한 구조체로, 관련 신호를 그룹화합니다package: 타입, 상수, 함수를 모듈 간에 공유하기 위한 네임스페이스입니다- 어서션(Assertion):
assert property로 설계 의도를 검증할 수 있으며, 시뮬레이션과 형식 검증에서 활용됩니다
logic 타입은 Verilog의 wire와 reg 구분을 없앱니다. 기존에는 연속 할당(assign)에 wire를, 절차적 할당(always)에 reg를 사용해야 했지만, SystemVerilog에서는 logic 하나로 양쪽 모두 가능합니다. 다만 양방향 포트(inout)에는 여전히 wire를 사용해야 합니다.
typedef로 사용자 정의 타입을 만들 수 있으며, struct, union, enum과 결합하면 코드 가독성이 크게 향상됩니다.
// typedef + struct: 패킷 헤더 정의
typedef struct packed {
logic [3:0] version;
logic [3:0] ihl;
logic [7:0] dscp_ecn;
logic [15:0] total_len;
logic [15:0] identification;
logic [15:0] flags_frag;
logic [7:0] ttl;
logic [7:0] protocol;
logic [15:0] checksum;
logic [31:0] src_ip;
logic [31:0] dst_ip;
} ipv4_header_t;
// packed struct는 비트 벡터로 직접 접근 가능
ipv4_header_t hdr;
assign hdr = rx_data[159:0]; // 160비트 = 20바이트
assign is_udp = (hdr.protocol == 8'd17);
packed와 unpacked 배열의 차이도 중요합니다. packed 배열(logic [7:0][3:0] data)은 연속된 비트 벡터로 합성되어 비트 슬라이싱이 가능합니다. unpacked 배열(logic [7:0] mem [0:15])은 독립적인 요소의 배열로, 메모리(BRAM)로 추론됩니다.
시뮬레이션 전용 타입으로는 string(문자열), 큐(int q[$]), 연관 배열(int aa[string])이 있습니다. 이들은 합성되지 않으며 테스트벤치에서만 사용합니다.
unique case와 priority case는 합성 도구에게 추가 정보를 제공합니다. unique case는 모든 조건이 상호 배타적임을 선언하여 병렬 멀티플렉서를 생성하고, priority case는 우선순위가 있는 인코더를 생성합니다. 일반 case와 달리 시뮬레이션에서 조건 누락이나 중복 시 경고를 발생시킵니다.
AXI-Lite 레지스터 인터페이스 예제
다음은 4개의 읽기/쓰기 레지스터를 제공하는 간단한 AXI-Lite 슬레이브(Slave) 모듈입니다. FPGA 기반 커스텀 IP에서 리눅스 드라이버가 ioread32()/iowrite32()로 접근하는 레지스터 블록의 전형적인 구현 패턴입니다.
module axi_lite_regs #(
parameter integer ADDR_WIDTH = 4,
parameter integer DATA_WIDTH = 32
) (
input logic aclk,
input logic aresetn,
/* AXI-Lite Write Channel */
input logic [ADDR_WIDTH-1:0] s_awaddr,
input logic s_awvalid,
output logic s_awready,
input logic [DATA_WIDTH-1:0] s_wdata,
input logic [DATA_WIDTH/8-1:0] s_wstrb,
input logic s_wvalid,
output logic s_wready,
output logic [1:0] s_bresp,
output logic s_bvalid,
input logic s_bready,
/* AXI-Lite Read Channel */
input logic [ADDR_WIDTH-1:0] s_araddr,
input logic s_arvalid,
output logic s_arready,
output logic [DATA_WIDTH-1:0] s_rdata,
output logic [1:0] s_rresp,
output logic s_rvalid,
input logic s_rready
);
/* 4개 레지스터 (32비트 × 4 = 16바이트 주소 공간) */
logic [DATA_WIDTH-1:0] slv_reg [0:3];
logic [ADDR_WIDTH-1:0] aw_addr_latched;
logic [ADDR_WIDTH-1:0] ar_addr_latched;
/* 쓰기 주소 핸드셰이크 */
assign s_awready = s_awvalid && s_wvalid;
assign s_wready = s_awvalid && s_wvalid;
assign s_bresp = 2'b00; /* OKAY */
/* 쓰기 데이터 처리 (순차 논리) */
always_ff @(posedge aclk or negedge aresetn) begin
if (!aresetn) begin
slv_reg[0] <= '0;
slv_reg[1] <= '0;
slv_reg[2] <= '0;
slv_reg[3] <= '0;
s_bvalid <= 1'b0;
end else begin
if (s_awvalid && s_wvalid) begin
case (s_awaddr[3:2])
2'b00: slv_reg[0] <= s_wdata;
2'b01: slv_reg[1] <= s_wdata;
2'b10: slv_reg[2] <= s_wdata;
2'b11: slv_reg[3] <= s_wdata;
endcase
s_bvalid <= 1'b1;
end else if (s_bvalid && s_bready)
s_bvalid <= 1'b0;
end
end
/* 읽기 주소 핸드셰이크 */
assign s_arready = s_arvalid && !s_rvalid;
assign s_rresp = 2'b00; /* OKAY */
/* 읽기 데이터 처리 (순차 논리) */
always_ff @(posedge aclk or negedge aresetn) begin
if (!aresetn) begin
s_rdata <= '0;
s_rvalid <= 1'b0;
end else begin
if (s_arvalid && !s_rvalid) begin
case (s_araddr[3:2])
2'b00: s_rdata <= slv_reg[0];
2'b01: s_rdata <= slv_reg[1];
2'b10: s_rdata <= slv_reg[2];
2'b11: s_rdata <= slv_reg[3];
endcase
s_rvalid <= 1'b1;
end else if (s_rvalid && s_rready)
s_rvalid <= 1'b0;
end
end
endmodule
코드 설명
parameter: 주소 폭과 데이터 폭을 파라미터화하여 재사용성을 높였습니다. 기본값은 4비트 주소(16바이트 공간)와 32비트 데이터입니다slv_reg[0:3]: 4개의 32비트 레지스터 배열입니다. 리눅스 드라이버에서iowrite32(val, base + offset)으로 접근합니다- 쓰기 로직:
s_awvalid && s_wvalid조건이 만족되면 주소 채널의s_awaddr[3:2]로 레지스터를 선택하고 데이터를 기록합니다. 상위 2비트를 사용하는 이유는 32비트(4바이트) 단위 접근이므로 하위 2비트는 바이트 오프셋이기 때문입니다 - 읽기 로직:
s_arvalid가 활성화되면 해당 주소의 레지스터 값을s_rdata로 출력합니다.s_rvalid와s_rready의 핸드셰이크가 완료되면 트랜잭션이 종료됩니다 - 비동기 리셋:
negedge aresetn으로 AXI 프로토콜의 액티브 로우(Active Low) 리셋을 처리합니다. 리셋 시 모든 레지스터와 제어 신호를 초기화합니다
FSM (Finite State Machine) 코딩 패턴
유한 상태 머신(FSM)은 FPGA 설계에서 가장 중요한 순차 논리 패턴입니다. 제어 로직, 프로토콜 핸들러, 시퀀서 등 거의 모든 디지털 시스템의 핵심 구성요소입니다. FSM은 출력이 현재 상태에만 의존하는 무어(Moore) 머신과 현재 상태와 입력 모두에 의존하는 밀리(Mealy) 머신으로 분류됩니다.
합성 품질과 유지보수성을 위해 3-프로세스 코딩 패턴이 권장됩니다: (1) 상태 레지스터, (2) 다음 상태 로직, (3) 출력 로직을 각각 별도의 always 블록으로 분리합니다.
// UART 수신기 FSM (8N1, 3-프로세스 패턴)
module uart_rx #(
parameter int CLK_PER_BIT = 868 // 100MHz / 115200 baud
)(
input logic clk,
input logic rst_n,
input logic rx_serial,
output logic [7:0] rx_data,
output logic rx_valid
);
// 상태 열거형 — one-hot 인코딩 지정
typedef enum logic [3:0] {
IDLE = 4'b0001,
START = 4'b0010,
DATA = 4'b0100,
STOP = 4'b1000
} state_t;
state_t state, next_state;
logic [15:0] clk_cnt;
logic [2:0] bit_idx;
logic [7:0] rx_shift;
logic rx_d1, rx_d2; // 메타스테이빌리티 방지 2-FF
// 입력 동기화 (CDC 2-FF synchronizer)
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) {rx_d1, rx_d2} <= 2'b11;
else {rx_d1, rx_d2} <= {rx_serial, rx_d1};
// (1) 상태 레지스터
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= IDLE;
else state <= next_state;
// (2) 다음 상태 로직 (조합 논리)
always_comb begin
next_state = state;
unique case (state)
IDLE: if (!rx_d2)
next_state = START;
START: if (clk_cnt == CLK_PER_BIT/2 - 1)
next_state = (!rx_d2) ? DATA : IDLE;
DATA: if (clk_cnt == CLK_PER_BIT - 1 && bit_idx == 3'd7)
next_state = STOP;
STOP: if (clk_cnt == CLK_PER_BIT - 1)
next_state = IDLE;
endcase
end
// (3) 데이터패스 로직
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
clk_cnt <= '0;
bit_idx <= '0;
rx_shift <= '0;
rx_data <= '0;
rx_valid <= 1'b0;
end else begin
rx_valid <= 1'b0;
case (state)
IDLE: begin
clk_cnt <= '0;
bit_idx <= '0;
end
START: begin
if (clk_cnt == CLK_PER_BIT/2 - 1) clk_cnt <= '0;
else clk_cnt <= clk_cnt + 1;
end
DATA: begin
if (clk_cnt == CLK_PER_BIT - 1) begin
clk_cnt <= '0;
rx_shift[bit_idx] <= rx_d2;
bit_idx <= bit_idx + 1;
end else
clk_cnt <= clk_cnt + 1;
end
STOP: begin
if (clk_cnt == CLK_PER_BIT - 1) begin
rx_data <= rx_shift;
rx_valid <= 1'b1;
end else
clk_cnt <= clk_cnt + 1;
end
endcase
end
endmodule
코드 설명
typedef enum logic [3:0]: 4비트 one-hot 인코딩으로 상태를 정의합니다. one-hot은 FPGA에서 상태 디코딩 로직이 단순해져 타이밍에 유리합니다- 2-FF 동기화기: 외부 비동기
rx_serial신호를 내부 클록에 동기화합니다. 메타스테이빌리티 위험을 줄이기 위한 필수 패턴입니다 - 3-프로세스 구조: 상태 레지스터(
always_ff), 다음 상태 로직(always_comb), 데이터패스를 분리하여 가독성과 합성 품질을 높입니다 unique case: 모든 상태가 다루어졌음을 합성 도구에 알려, 불필요한 우선순위 인코더 생성을 방지합니다- 비트 샘플링: START 상태에서 반 비트 주기를 기다려 비트 중앙에서 샘플링을 시작합니다. 이후 DATA 상태에서 정확히 1비트 주기마다 샘플링합니다
파라미터화 모듈 — 동기 FIFO
FIFO(First-In First-Out)는 FPGA 설계에서 가장 자주 사용되는 버퍼 구조입니다. 파라미터화를 통해 데이터 폭과 깊이를 재사용 가능하게 설계합니다.
// 파라미터화 동기 FIFO (BRAM 추론)
module sync_fifo #(
parameter int WIDTH = 8,
parameter int DEPTH = 256,
localparam int ADDR_W = $clog2(DEPTH)
)(
input logic clk,
input logic rst_n,
// Write port
input logic wr_en,
input logic [WIDTH-1:0] wr_data,
output logic full,
// Read port
input logic rd_en,
output logic [WIDTH-1:0] rd_data,
output logic empty,
output logic [ADDR_W:0] count
);
logic [WIDTH-1:0] mem [DEPTH]; // BRAM 추론 대상
logic [ADDR_W-1:0] wr_ptr, rd_ptr;
logic [ADDR_W:0] cnt;
assign full = (cnt == DEPTH);
assign empty = (cnt == 0);
assign count = cnt;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
wr_ptr <= '0;
rd_ptr <= '0;
cnt <= '0;
end else begin
case ({wr_en && !full, rd_en && !empty})
2'b10: begin wr_ptr <= wr_ptr + 1; cnt <= cnt + 1; end
2'b01: begin rd_ptr <= rd_ptr + 1; cnt <= cnt - 1; end
2'b11: begin wr_ptr <= wr_ptr + 1; rd_ptr <= rd_ptr + 1; end
default: ;
endcase
end
// 쓰기 포트 (BRAM inference)
always_ff @(posedge clk)
if (wr_en && !full)
mem[wr_ptr] <= wr_data;
// 읽기 포트 (BRAM read-first mode)
always_ff @(posedge clk)
if (rd_en && !empty)
rd_data <= mem[rd_ptr];
endmodule
generate 구문과 반복 구조
generate 구문은 반복적인 하드웨어 구조를 간결하게 기술합니다. 파라미터화된 모듈과 결합하면 강력한 재사용성을 제공합니다.
// generate for — N비트 리플 캐리 가산기
module ripple_carry_adder #(
parameter int N = 8
)(
input logic [N-1:0] a, b,
input logic cin,
output logic [N-1:0] sum,
output logic cout
);
logic [N:0] carry;
assign carry[0] = cin;
assign cout = carry[N];
genvar i;
generate
for (i = 0; i < N; i++) begin : gen_fa
full_adder fa (
.a(a[i]), .b(b[i]), .cin(carry[i]),
.sum(sum[i]), .cout(carry[i+1])
);
end
endgenerate
endmodule
// 조건부 generate — 데이터 폭에 따른 메모리 타입 선택
module adaptive_buffer #(
parameter int DEPTH = 16
)(/* ports */);
generate
if (DEPTH <= 32) begin : gen_dist
// 소규모: 분산 RAM (LUT 기반)
(* ram_style = "distributed" *)
logic [7:0] mem [DEPTH];
end else begin : gen_bram
// 대규모: Block RAM 추론
(* ram_style = "block" *)
logic [7:0] mem [DEPTH];
end
endgenerate
endmodule
SystemVerilog interface 상세
SystemVerilog의 interface는 관련 신호들을 하나의 번들로 묶어 모듈 간 연결을 단순화합니다. modport로 각 모듈이 볼 수 있는 신호의 방향을 제한하여 설계 안전성을 높입니다.
// AXI-Stream interface 정의
interface axis_if #(
parameter int DATA_WIDTH = 64,
parameter int USER_WIDTH = 1
)(
input logic aclk,
input logic aresetn
);
logic [DATA_WIDTH-1:0] tdata;
logic [DATA_WIDTH/8-1:0] tkeep;
logic tvalid;
logic tready;
logic tlast;
logic [USER_WIDTH-1:0] tuser;
// Master: tvalid/tdata/tkeep/tlast/tuser를 구동
modport master (
input aclk, aresetn, tready,
output tdata, tkeep, tvalid, tlast, tuser
);
// Slave: tready를 구동하고 나머지를 수신
modport slave (
input aclk, aresetn, tdata, tkeep, tvalid, tlast, tuser,
output tready
);
// Monitor: 모든 신호를 관찰만 (검증용)
modport monitor (
input aclk, aresetn, tdata, tkeep, tvalid, tready, tlast, tuser
);
endinterface
// interface를 사용한 모듈 연결
module packet_processor (
axis_if.slave s_axis, // 입력 스트림
axis_if.master m_axis // 출력 스트림
);
assign m_axis.tdata = s_axis.tdata ^ 8'hFF; // 예: 비트 반전
assign m_axis.tvalid = s_axis.tvalid;
assign m_axis.tkeep = s_axis.tkeep;
assign m_axis.tlast = s_axis.tlast;
assign s_axis.tready = m_axis.tready;
endmodule
SystemVerilog 어서션 (SVA) 심화
SVA(SystemVerilog Assertions)는 설계 속성을 선언적으로 기술하여, 시뮬레이션과 형식 검증 모두에서 자동으로 검사할 수 있게 합니다. 즉시 어서션(Immediate Assertion)과 동시 어서션(Concurrent Assertion)으로 나뉩니다.
- 즉시 어서션: 절차적 코드 내에서 조건을 즉시 검사합니다.
assert (condition) else $error("msg"); - 동시 어서션: 시간적 관계를 기술합니다. 클록 에지마다 속성(property)을 평가합니다
주요 시퀀스 연산자
##N— N 클록 사이클 지연.a ##2 b는 a 후 2 사이클 뒤 b##[M:N]— M~N 사이클 범위 내 지연|->— 겹침 함의(overlapped implication). 선행 조건 성립 시 같은 사이클부터 후행 시퀀스 시작|=>— 비겹침 함의(non-overlapped). 선행 조건 성립 다음 사이클부터 후행 시퀀스 시작[*N]— N회 반복.a[*3]은 a가 3 사이클 연속throughout— 조건이 시퀀스 전체에 걸쳐 유지
입문 예제 — 카운터 어서션
어서션을 처음 접한다면, 복잡한 프로토콜보다 간단한 카운터 속성부터 시작하는 것이 효과적입니다. 다음 예제는 이 페이지의 인에이블 카운터에 대해 기본적인 안전성 속성을 검증합니다.
// 카운터 기본 어서션 — SVA 입문
module counter_assertions (
input logic clk, rst_n, en,
input logic [7:0] count
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// 즉시 어서션: 카운터 값이 범위를 초과하지 않는지 검사
always_ff @(posedge clk)
assert (count <= 8'd255) else $error("카운터 오버플로우");
// 동시 어서션: 리셋 후 카운터가 0이어야 합니다
reset_clears: assert property (
!rst_n |=> count == '0
);
// 동시 어서션: 인에이블이 꺼지면 값이 유지되어야 합니다
hold_when_disabled: assert property (
!en |=> count == $past(count)
);
// ##N 예제: 인에이블 후 2 사이클 뒤 카운터가 증가했는지 확인
increments: assert property (
en && (count < 8'd255) |=> count == $past(count) + 1
);
endmodule
코드 설명
default clocking: 모든 동시 어서션이posedge clk에서 평가됩니다. 각 어서션에 클록을 반복 지정할 필요가 없습니다default disable iff: 리셋 중(!rst_n)에는 어서션 평가를 비활성화합니다. 리셋 과도 상태에서의 거짓 실패를 방지합니다|=>(비겹침 함의): 선행 조건이 성립한 다음 사이클에서 후행 조건을 검사합니다.!rst_n |=> count == '0은 "리셋이 활성화되면 다음 사이클에 카운터가 0"을 의미합니다$past(signal): 이전 사이클의 신호 값을 참조합니다.count == $past(count)는 "값이 변하지 않았습니다"를 표현합니다
AXI 프로토콜 검증 어서션
기본 어서션에 익숙해졌다면, 실제 버스 프로토콜의 규칙을 어서션으로 기술하는 실전 예제를 살펴봅니다. 다음은 AXI4 핸드셰이크 규칙(VALID가 어서트되면 READY까지 유지)과 채널 간 순서 규칙을 검증합니다.
// AXI 프로토콜 검증 어서션 모음
module axi_protocol_checker (
input logic aclk, aresetn,
input logic awvalid, awready,
input logic wvalid, wready, wlast,
input logic bvalid, bready,
input logic arvalid, arready,
input logic rvalid, rready, rlast
);
// [AXI Rule] valid는 한번 assert되면 ready 전까지 유지해야 합니다
property valid_until_ready(logic valid, logic ready);
@(posedge aclk) disable iff (!aresetn)
valid && !ready |=> valid;
endproperty
aw_stable: assert property (valid_until_ready(awvalid, awready))
else $error("AWVALID de-asserted before AWREADY");
w_stable: assert property (valid_until_ready(wvalid, wready))
else $error("WVALID de-asserted before WREADY");
ar_stable: assert property (valid_until_ready(arvalid, arready))
else $error("ARVALID de-asserted before ARREADY");
// [AXI Rule] 리셋 중에는 모든 valid 신호가 LOW이어야 합니다
property no_valid_during_reset;
@(posedge aclk)
!aresetn |-> !awvalid && !wvalid && !arvalid;
endproperty
reset_valid: assert property (no_valid_during_reset)
else $error("Valid signal asserted during reset");
// [AXI Rule] 쓰기 응답(B)은 마지막 쓰기(WLAST) 이후에 발생해야 합니다
property bresp_after_wlast;
@(posedge aclk) disable iff (!aresetn)
(wvalid && wready && wlast) |-> ##[1:16] (bvalid && bready);
endproperty
b_after_w: assert property (bresp_after_wlast);
endmodule
constrained random 검증
SystemVerilog의 constrained random 검증은 시뮬레이션 커버리지를 극대화하는 핵심 기법입니다. 트랜잭션을 클래스로 추상화하고, 제약 조건(constraint)으로 유효한 랜덤 자극을 자동 생성합니다.
// AXI 트랜잭션 클래스 (constrained random)
class axi_transaction;
rand logic [31:0] addr;
rand logic [7:0] len; // burst length - 1
rand logic [2:0] size; // 2^size bytes per beat
rand logic [1:0] burst; // FIXED, INCR, WRAP
randc logic [3:0] id; // randc: 반복 없이 순환
rand logic [31:0] data []; // 동적 배열
// AXI 4K 바운더리 제약: 버스트가 4KB 경계를 넘지 않아야 합니다
constraint c_4k_boundary {
(addr % 4096) + ((len + 1) * (1 << size)) <= 4096;
}
// 주소 정렬 제약
constraint c_aligned {
addr % (1 << size) == 0;
}
// 버스트 타입 분포 (가중 랜덤)
constraint c_burst_dist {
burst dist { 0 := 10, // FIXED: 10%
1 := 80, // INCR: 80%
2 := 10 }; // WRAP: 10%
}
// 데이터 배열 크기 = burst length + 1
constraint c_data_size {
data.size() == len + 1;
}
function void print();
$display("AXI Txn: addr=%h len=%0d size=%0d burst=%0d id=%0d",
addr, len, size, burst, id);
endfunction
endclass
// 사용 예
initial begin
axi_transaction txn = new();
repeat (100) begin
assert(txn.randomize()) else $fatal("Randomization failed");
txn.print();
// 인라인 제약으로 특정 시나리오 강제
assert(txn.randomize() with { addr inside {['h1000:'h1FFF]}; })
else $fatal("Constrained randomization failed");
end
end
기능 커버리지
기능 커버리지(Functional Coverage)는 검증 완료 기준을 정량적으로 측정합니다. 코드 커버리지(라인/분기/토글)와 달리, 기능 커버리지는 설계 의도와 사양이 충분히 검증되었는지를 측정합니다.
// FSM 상태 전이 기능 커버리지
covergroup fsm_coverage @(posedge clk);
// 현재 상태 커버리지 — 모든 상태가 방문되었는가?
state_cp: coverpoint dut.state {
bins idle = {IDLE};
bins start = {START};
bins data = {DATA};
bins stop = {STOP};
illegal_bins invalid = default;
}
// 상태 전이 커버리지 — 모든 합법적 전이가 발생했는가?
transition_cp: coverpoint dut.state {
bins idle_to_start = (IDLE => START);
bins start_to_data = (START => DATA);
bins start_to_idle = (START => IDLE); // 거짓 시작 감지
bins data_to_stop = (DATA => STOP);
bins stop_to_idle = (STOP => IDLE);
}
// 교차 커버리지 — 수신 데이터 값과 상태의 조합
data_x_state: cross state_cp, dut.rx_data[7:6] {
ignore_bins non_data = binsof(state_cp) intersect {IDLE, START};
}
endgroup
fsm_coverage cov = new();
VHDL
VHDL(VHSIC Hardware Description Language)은 IEEE 1076 표준으로 정의된 하드웨어 기술 언어입니다. 미국 국방부(DoD)의 VHSIC(Very High Speed Integrated Circuit) 프로그램에서 시작되었으며, Ada 프로그래밍 언어의 문법을 기반으로 합니다. Verilog와 비교하여 강한 타입(Strong Typing) 시스템이 특징입니다.
Entity/Architecture 구조
Entity는 전자부품의 데이터시트(핀 배치표)에 해당하고, Architecture는 내부 회로도에 해당합니다. 같은 핀 배치(Entity)로 시뮬레이션용 동작 모델과 합성용 RTL 모델을 따로 만들 수 있어서, 설계의 추상화 수준을 분리할 수 있습니다.
VHDL의 기본 설계 단위는 엔티티(Entity)와 아키텍처(Architecture)의 쌍으로 구성됩니다.
- Entity: 모듈의 외부 인터페이스(포트)를 선언합니다. Verilog의
module포트 선언부에 해당합니다 - Architecture: 내부 동작을 기술합니다. 하나의 Entity에 여러 Architecture를 정의할 수 있습니다 (동작 수준, 구조 수준 등)
Entity와 Architecture를 분리하는 이유는 인터페이스와 구현의 분리입니다. 하나의 Entity(인터페이스)에 대해 여러 Architecture(구현)를 작성할 수 있습니다. 예를 들어 동일한 ALU Entity에 동작 수준(behavioral) Architecture와 구조 수준(structural) Architecture를 각각 정의하여, 시뮬레이션 속도와 합성 최적화를 분리할 수 있습니다.
-- 하나의 Entity, 두 개의 Architecture
entity adder is
port (a, b : in unsigned(7 downto 0);
sum : out unsigned(8 downto 0));
end entity;
-- 동작 수준: 시뮬레이션 빠름
architecture behavioral of adder is
begin
sum <= ('0' & a) + ('0' & b);
end architecture;
-- 구조 수준: 게이트 레벨 구현
architecture structural of adder is
-- full adder 인스턴스를 generate로 연결...
begin
-- 생략
end architecture;
모듈 인스턴스화 방법도 두 가지가 있습니다. 전통적인 Component 선언 방식은 Architecture 선언부에 component를 미리 선언해야 하지만, VHDL-93부터 지원되는 직접 인스턴스화(Direct Instantiation)는 entity work.모듈명(아키텍처명)으로 바로 사용할 수 있어 코드가 간결합니다.
-- Component 방식 (장황함)
component adder is
port (a, b : in unsigned(7 downto 0); sum : out unsigned(8 downto 0));
end component;
-- 직접 인스턴스화 (권장, VHDL-93+)
u_add : entity work.adder(behavioral)
port map (a => op_a, b => op_b, sum => result);
Signal과 Variable
- Signal: 하드웨어 와이어를 모델링합니다. 할당(
<=) 후 즉시 반영되지 않고 델타 사이클(Delta Cycle) 후에 갱신됩니다. Verilog의 논블로킹 할당과 유사합니다 - Variable:
process내부에서만 사용되며, 할당(:=) 즉시 반영됩니다. 시뮬레이션 시 순차적 동작이 필요할 때 사용합니다
Signal이 즉시 반영되지 않는 이유는 VHDL의 델타 사이클(Delta Cycle) 메커니즘 때문입니다. Signal 할당은 현재 프로세스가 일시 정지(suspend)될 때까지 대기한 후, 델타 사이클에서 모든 Signal이 동시에 갱신됩니다. 이 방식은 실제 하드웨어의 병렬 동작을 정확히 모델링합니다.
반면 Variable은 소프트웨어 변수처럼 할당 즉시 새 값이 반영됩니다. 이 때문에 동일한 로직이라도 Signal과 Variable을 사용하면 시뮬레이션 결과가 달라질 수 있습니다.
-- 흔한 실수: Signal의 지연 갱신을 모르고 사용
process(clk)
begin
if rising_edge(clk) then
sig_a <= sig_b + 1; -- sig_a는 아직 이전 값
sig_c <= sig_a; -- sig_c = sig_a의 이전 값 (갱신 전!)
-- 의도: sig_c = sig_b + 1 이었다면 Variable을 사용해야 합니다
end if;
end process;
-- 올바른 구현: Variable로 중간 계산
process(clk)
variable v_temp : unsigned(7 downto 0);
begin
if rising_edge(clk) then
v_temp := sig_b + 1; -- 즉시 반영
sig_c <= v_temp; -- sig_c = sig_b + 1 (의도대로)
end if;
end process;
델타 사이클 시각화
다음 다이어그램은 동일한 코드에서 Signal과 Variable이 어떻게 다르게 동작하는지를 시뮬레이션 시간축 위에 보여줍니다. Signal은 프로세스가 일시 정지(suspend)된 후 다음 델타(Δ)에서 일괄 갱신되지만, Variable은 할당 즉시 새 값이 반영됩니다.
VHDL의 강한 타입 시스템
C 언어에서 int를 float에 대입하면 묵시적으로 변환되지만, VHDL에서는 컴파일 에러가 발생합니다. 번거롭지만, 하드웨어에서 8비트 신호를 16비트 포트에 연결하는 실수를 설계 시점에 잡아줍니다. 실리콘에서 버그를 발견하는 비용은 RTL에서 발견하는 비용의 1000배 이상이므로, 이 엄격함은 큰 가치가 있습니다.
VHDL은 Verilog에 비해 훨씬 엄격한 타입 시스템을 가지고 있습니다. 이는 타입 불일치 오류를 컴파일 시점에 잡아내어 설계 안전성을 높이지만, 코드가 다소 장황해지는 단점이 있습니다.
std_logic: 단일 비트 신호로,'0','1','Z'(고임피던스),'X'(알 수 없음) 등 9가지 상태를 가집니다std_logic_vector: 비트 벡터로, 산술 연산 시에는unsigned또는signed타입으로 변환해야 합니다unsigned/signed:ieee.numeric_std라이브러리에서 제공하며, 산술 연산을 지원합니다integer: 범위를 지정할 수 있는 정수 타입(integer range 0 to 255)입니다. 합성 시 필요한 비트 수가 자동으로 결정됩니다
타입 간 변환이 필요할 때는 ieee.numeric_std의 변환 함수를 사용합니다. Verilog에서는 암묵적으로 처리되는 것이 VHDL에서는 반드시 명시적 변환을 거쳐야 합니다.
-- VHDL 타입 변환 체인 예시
signal slv : std_logic_vector(7 downto 0);
signal u : unsigned(7 downto 0);
signal i : integer range 0 to 255;
-- std_logic_vector → unsigned → integer
u <= unsigned(slv); -- 비트 패턴 재해석
i <= to_integer(unsigned(slv)); -- 정수로 변환
-- integer → unsigned → std_logic_vector
u <= to_unsigned(i, 8); -- 8비트 unsigned로 변환
slv <= std_logic_vector(to_unsigned(i, 8));
-- 흔한 컴파일 오류: 직접 산술 불가
-- slv <= slv + 1; -- 오류! std_logic_vector에 + 연산 없음
-- 올바른 방법:
slv <= std_logic_vector(unsigned(slv) + 1);
LED 점멸기(Blinker) 예제
다음은 VHDL로 작성한 간단한 LED 점멸기입니다. 클록을 분주하여 일정 주기로 LED를 토글합니다.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity led_blinker is
generic (
CLK_FREQ : integer := 100_000_000; -- 100 MHz
BLINK_HZ : integer := 1 -- 1 Hz
);
port (
clk : in std_logic;
rst_n : in std_logic;
led : out std_logic
);
end entity led_blinker;
architecture rtl of led_blinker is
constant MAX_COUNT : integer := CLK_FREQ / (2 * BLINK_HZ) - 1;
signal counter : integer range 0 to MAX_COUNT := 0;
signal led_state : std_logic := '0';
begin
blink_proc : process(clk, rst_n)
begin
if rst_n = '0' then
counter <= 0;
led_state <= '0';
elsif rising_edge(clk) then
if counter = MAX_COUNT then
counter <= 0;
led_state <= not led_state;
else
counter <= counter + 1;
end if;
end if;
end process;
led <= led_state;
end architecture rtl;
코드 설명
generic: VHDL의 파라미터화 방법입니다. Verilog의parameter에 해당하며, 클록 주파수와 점멸 주파수를 외부에서 설정할 수 있습니다constant MAX_COUNT: 클록을 분주하여 원하는 주파수의 토글을 만들기 위한 카운트 최댓값입니다. 100 MHz 클록에서 1 Hz 점멸이면 5천만-1 = 49,999,999 입니다integer range 0 to MAX_COUNT: VHDL의 강한 타입 시스템을 보여주는 예입니다. 범위를 명시하면 합성 도구가 필요한 비트 수(여기서는 26비트)를 자동으로 결정합니다rising_edge(clk): 클록 상승 에지 감지 함수로, Verilog의posedge clk에 해당합니다
Process 문 상세
VHDL의 process는 HDL 설계의 핵심 구성요소입니다. 감도 리스트(sensitivity list)에 따라 조합 논리 또는 순차 논리를 기술합니다.
- 조합 프로세스: 감도 리스트에 모든 입력 신호를 포함합니다. VHDL-2008에서는
process(all)로 자동화할 수 있습니다. 모든 경로에서 출력을 할당하지 않으면 래치가 추론됩니다 - 순차 프로세스:
process(clk)또는process(clk, rst_n)형태입니다.rising_edge(clk)로 클록 에지를 검출합니다 - Variable vs Signal 타이밍 차이:
variable은 할당(:=) 즉시 반영되어 같은 프로세스 내에서 이후 문장에 영향을 미칩니다.signal은 할당(<=) 후 프로세스가 끝날 때(델타 사이클 후) 갱신됩니다. 이 차이를 이해하지 못하면 시뮬레이션과 합성 불일치가 발생합니다
감도 리스트에서 신호를 누락하면, 합성 결과와 시뮬레이션 동작이 달라집니다. 합성 도구는 감도 리스트와 무관하게 로직을 추론하지만, 시뮬레이터는 감도 리스트에 있는 신호가 변할 때만 프로세스를 실행합니다.
-- 감도 리스트 누락 문제
-- 잘못: sel 누락 → 시뮬레이션에서 sel 변경 시 출력 미갱신
process(a, b) -- sel이 빠져 있음!
begin
if sel = '1' then y <= a;
else y <= b;
end if;
end process;
-- 올바른: VHDL-2008 process(all) 사용 (권장)
process(all)
begin
if sel = '1' then y <= a;
else y <= b;
end if;
end process;
VHDL에서 process 외부에 작성하는 문장은 동시 문(concurrent statement)이며, process 내부의 문장은 순차 문(sequential statement)입니다. 동시 문은 모두 병렬로 동작하고, 순차 문은 프로세스 안에서 위에서 아래로 실행됩니다. wait 문은 감도 리스트 대신 사용할 수 있는 대안으로, wait until rising_edge(clk) 형태로 테스트벤치에서 주로 사용합니다.
-- wait 문을 사용한 테스트벤치 프로세스
stim_proc : process
begin
rst_n <= '0';
wait for 100 ns;
rst_n <= '1';
wait until rising_edge(clk);
din <= x"AA";
wait until rising_edge(clk);
din <= x"55";
wait; -- 영원히 대기 (프로세스 종료)
end process;
-- Variable vs Signal 차이 예시
var_sig_demo : process(clk)
variable v_cnt : unsigned(7 downto 0) := (others => '0');
begin
if rising_edge(clk) then
v_cnt := v_cnt + 1; -- variable: 즉시 반영
sig_a <= v_cnt; -- sig_a = 새 값 (v_cnt+1)
sig_b <= std_logic_vector(v_cnt); -- sig_b도 새 값
-- signal은 프로세스 끝까지 이전 값 유지
sig_c <= sig_d; -- sig_c = sig_d의 이전 값
sig_d <= sig_c; -- sig_d = sig_c의 이전 값 (swap 동작)
end if;
end process;
패키지와 레코드 타입
VHDL의 package는 타입, 상수, 함수를 묶어 재사용할 수 있는 라이브러리 단위입니다. record 타입은 관련 신호들을 구조체로 묶어 버스 인터페이스를 깔끔하게 정의합니다.
-- AXI-Lite 버스 레코드 타입 패키지
library ieee;
use ieee.std_logic_1164.all;
package axi_lite_pkg is
-- Master → Slave 방향 신호 묶음
type axi_lite_m2s_t is record
awaddr : std_logic_vector(31 downto 0);
awvalid : std_logic;
wdata : std_logic_vector(31 downto 0);
wstrb : std_logic_vector(3 downto 0);
wvalid : std_logic;
bready : std_logic;
araddr : std_logic_vector(31 downto 0);
arvalid : std_logic;
rready : std_logic;
end record;
-- Slave → Master 방향 신호 묶음
type axi_lite_s2m_t is record
awready : std_logic;
wready : std_logic;
bresp : std_logic_vector(1 downto 0);
bvalid : std_logic;
arready : std_logic;
rdata : std_logic_vector(31 downto 0);
rresp : std_logic_vector(1 downto 0);
rvalid : std_logic;
end record;
-- 초기화 상수
constant AXI_LITE_M2S_INIT : axi_lite_m2s_t := (
awaddr => (others => '0'), awvalid => '0',
wdata => (others => '0'), wstrb => (others => '0'), wvalid => '0',
bready => '0',
araddr => (others => '0'), arvalid => '0', rready => '0'
);
end package axi_lite_pkg;
-- 패키지를 사용하는 Entity
library ieee;
use ieee.std_logic_1164.all;
use work.axi_lite_pkg.all;
entity my_peripheral is
port (
clk : in std_logic;
rst_n : in std_logic;
s_axi_i : in axi_lite_m2s_t; -- 레코드 하나로 깔끔
s_axi_o : out axi_lite_s2m_t
);
end entity;
Generic과 Generate
VHDL의 generic은 Verilog의 parameter에 해당하며, generate는 반복적 또는 조건부 하드웨어 구조를 기술합니다.
-- for generate 예시: N비트 시프트 레지스터
entity shift_reg is
generic (N : positive := 8);
port (
clk : in std_logic;
din : in std_logic;
dout : out std_logic
);
end entity;
architecture rtl of shift_reg is
signal chain : std_logic_vector(N-1 downto 0);
begin
chain(0) <= din;
dout <= chain(N-1);
gen_stages : for i in 1 to N-1 generate
ff : process(clk)
begin
if rising_edge(clk) then
chain(i) <= chain(i-1);
end if;
end process;
end generate;
end architecture;
-- if generate 예시: 조건부 디버그 로직
gen_debug : if ENABLE_DEBUG generate
-- ENABLE_DEBUG generic이 true일 때만 합성되는 로직
debug_proc : process(clk)
begin
if rising_edge(clk) then
debug_counter <= debug_counter + 1;
end if;
end process;
end generate;
VHDL FSM 예제 — SPI 마스터
다음은 VHDL로 작성한 SPI(Serial Peripheral Interface) 마스터 컨트롤러 FSM입니다. 2-프로세스 스타일(상태 레지스터 + 조합 로직)을 사용합니다.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_master is
generic (
CLK_DIV : positive := 4; -- SCLK = clk / (2 * CLK_DIV)
DATA_LEN : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
start : in std_logic;
tx_data : in std_logic_vector(DATA_LEN-1 downto 0);
rx_data : out std_logic_vector(DATA_LEN-1 downto 0);
done : out std_logic;
-- SPI 물리 인터페이스
sclk : out std_logic;
mosi : out std_logic;
miso : in std_logic;
cs_n : out std_logic
);
end entity;
architecture rtl of spi_master is
type state_t is (S_IDLE, S_LOAD, S_SHIFT, S_DONE);
signal state : state_t := S_IDLE;
signal shift_tx : std_logic_vector(DATA_LEN-1 downto 0);
signal shift_rx : std_logic_vector(DATA_LEN-1 downto 0);
signal bit_cnt : integer range 0 to DATA_LEN-1 := 0;
signal clk_cnt : integer range 0 to CLK_DIV-1 := 0;
signal sclk_reg : std_logic := '0';
signal sclk_edge : std_logic;
begin
mosi <= shift_tx(DATA_LEN-1); -- MSB first 전송
sclk <= sclk_reg;
sclk_edge <= '1' when clk_cnt = CLK_DIV-1 else '0';
fsm_proc : process(clk, rst_n)
begin
if rst_n = '0' then
state <= S_IDLE;
cs_n <= '1';
done <= '0';
sclk_reg <= '0';
elsif rising_edge(clk) then
done <= '0';
case state is
when S_IDLE =>
cs_n <= '1';
sclk_reg <= '0';
if start = '1' then
state <= S_LOAD;
end if;
when S_LOAD =>
shift_tx <= tx_data;
bit_cnt <= 0;
clk_cnt <= 0;
cs_n <= '0'; -- 칩 셀렉트 활성화
state <= S_SHIFT;
when S_SHIFT =>
if sclk_edge = '1' then
clk_cnt <= 0;
sclk_reg <= not sclk_reg;
if sclk_reg = '0' then -- SCLK 상승 에지: MISO 샘플링
shift_rx <= shift_rx(DATA_LEN-2 downto 0) & miso;
else -- SCLK 하강 에지: 다음 비트
shift_tx <= shift_tx(DATA_LEN-2 downto 0) & '0';
if bit_cnt = DATA_LEN-1 then
state <= S_DONE;
else
bit_cnt <= bit_cnt + 1;
end if;
end if;
else
clk_cnt <= clk_cnt + 1;
end if;
when S_DONE =>
rx_data <= shift_rx;
done <= '1';
cs_n <= '1';
sclk_reg <= '0';
state <= S_IDLE;
end case;
end if;
end process;
end architecture;
VHDL-2008 주요 개선
VHDL-2008(IEEE 1076-2008)은 생산성과 합성 호환성을 크게 개선하는 기능을 도입했습니다. 최신 합성 도구(Vivado 2019.1+, Quartus 19.1+, GHDL)에서 지원됩니다.
process(all): 감도 리스트에all을 지정하면, 프로세스 내에서 읽히는 모든 신호가 자동으로 포함됩니다. 래치 추론의 주요 원인인 감도 리스트 누락을 방지합니다- 간소화된 포트 매핑:
port map(a => a)대신port map(a)축약이 가능합니다 (이름이 같은 경우) ieee.numeric_std_unsigned:std_logic_vector를 직접 부호 없는 산술에 사용할 수 있습니다. 별도의unsigned변환이 불필요합니다- 향상된 Generic: 타입 generic(
generic type), 패키지 generic으로 더 유연한 파라미터화가 가능합니다 - 외부 이름(External Name):
<< signal .uut.internal_sig : std_logic >>으로 계층 구조를 관통하여 내부 신호에 접근할 수 있습니다. 테스트벤치에서 DUT 내부를 관찰할 때 유용합니다 - 조건부 할당 확장:
process없이when/else와with/select구문이 시그널 할당에서 사용 가능합니다
시뮬레이션과 테스트벤치 방법론
HDL 설계에서 시뮬레이션은 합성 전에 설계의 기능적 정확성을 검증하는 핵심 단계입니다. 효과적인 테스트벤치(Testbench) 설계와 적절한 시뮬레이션 도구 선택은 설계 품질을 결정짓는 중요한 요소입니다.
테스트벤치 구조
테스트벤치는 DUT(Design Under Test)를 감싸는 시뮬레이션 전용 환경입니다. 수동으로 파형을 확인하는 방식은 대규모 설계에서 비효율적이므로, 자체 검증(Self-Checking) 테스트벤치 패턴을 권장합니다.
자체 검증 테스트벤치의 기본 구조는 다음과 같습니다.
- Stimulus Generator: DUT에 입력 자극을 생성합니다. 고정 패턴, constrained random, 또는 파일에서 읽어오는 방식이 있습니다
- DUT (Design Under Test): 검증 대상 모듈을 인스턴스화합니다
- Reference Model: 기대 출력을 계산하는 소프트웨어 모델입니다. C/C++ DPI 또는 SystemVerilog 함수로 구현합니다
- Checker / Scoreboard: DUT 출력과 기대 출력을 비교하여 Pass/Fail을 판정합니다
클록 생성 패턴
시뮬레이션에서 클록은 합성 불가능한 #delay 구문으로 생성합니다. 주기(Period)를 파라미터화하면 다양한 주파수에서 테스트할 수 있습니다.
// SystemVerilog 클록 생성
parameter int CLK_PERIOD = 10; // 10ns → 100MHz
logic clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
-- VHDL 클록 생성
constant CLK_PERIOD : time := 10 ns;
clk_proc : process
begin
clk <= '0';
wait for CLK_PERIOD/2;
clk <= '1';
wait for CLK_PERIOD/2;
end process;
자체 검증 테스트벤치 예제
다음은 간단한 ALU(산술 논리 장치)에 대한 완전한 자체 검증 테스트벤치입니다. DUT 출력을 참조 모델과 자동으로 비교합니다.
// 자체 검증 테스트벤치 — ALU 검증
module tb_alu;
parameter int WIDTH = 8;
parameter int NUM_TESTS = 1000;
logic clk = 0;
logic [WIDTH-1:0] a, b, result;
logic [1:0] op;
logic carry_out;
// 클록 생성 (100MHz)
always #5 clk = ~clk;
// DUT 인스턴스화
alu #(.WIDTH(WIDTH)) dut (
.clk(clk), .a(a), .b(b), .op(op),
.result(result), .carry_out(carry_out)
);
// 참조 모델 (소프트웨어 계산)
function automatic logic [WIDTH:0] ref_model(
logic [WIDTH-1:0] a, b, logic [1:0] op
);
case (op)
2'b00: return {1'b0, a + b};
2'b01: return {1'b0, a - b};
2'b10: return {1'b0, a & b};
2'b11: return {1'b0, a | b};
endcase
endfunction
// Scoreboard
int pass_cnt = 0, fail_cnt = 0;
initial begin
$display("=== ALU Self-Checking Testbench ===");
// 리셋 시퀀스
a = '0; b = '0; op = '0;
repeat(5) @(posedge clk);
// constrained random 테스트
for (int i = 0; i < NUM_TESTS; i++) begin
a = $urandom_range(0, (1 << WIDTH) - 1);
b = $urandom_range(0, (1 << WIDTH) - 1);
op = $urandom_range(0, 3);
@(posedge clk); // DUT에 입력 전달
@(posedge clk); // 결과 대기 (파이프라인 1단계)
// 검증
begin
automatic logic [WIDTH:0] expected = ref_model(a, b, op);
if ({carry_out, result} !== expected) begin
$error("FAIL: a=%h b=%h op=%b → got %h, expected %h",
a, b, op, {carry_out, result}, expected);
fail_cnt++;
end else
pass_cnt++;
end
end
$display("=== Results: %0d PASS, %0d FAIL ===", pass_cnt, fail_cnt);
if (fail_cnt > 0) $fatal(1, "Test FAILED");
else $display("All tests PASSED");
$finish;
end
// VCD 파형 덤프
initial begin
$dumpfile("tb_alu.vcd");
$dumpvars(0, tb_alu);
end
endmodule
코드 설명
ref_model함수: DUT와 동일한 연산을 순수 소프트웨어로 계산합니다. DUT 출력과 비교하여 일치 여부를 자동으로 판단합니다$urandom_range: 지정 범위 내 무작위 값을 생성하여, 수동으로 벡터를 작성하는 것보다 훨씬 넓은 입력 공간을 탐색합니다- 2사이클 대기: 파이프라인 1단계를 가정하여, 입력 전달과 결과 수집을 분리합니다. DUT 파이프라인 깊이에 따라 조정해야 합니다
$dumpfile/$dumpvars: 시뮬레이션 파형을 VCD 파일로 저장합니다. GTKWave 등의 도구로 시각적 디버깅이 가능합니다$fatal: 실패 시 시뮬레이션을 즉시 중단하여, CI/CD 파이프라인에서 자동 검증 결과를 전달할 수 있습니다
시뮬레이션 도구 비교
HDL 시뮬레이션 도구는 오픈소스에서 상용까지 다양한 선택지가 있습니다. 프로젝트 규모, 사용 언어, 예산에 따라 적합한 도구가 달라집니다.
| 도구 | 라이선스 | 지원 언어 | 속도 | SV 지원 | cocotb 호환 |
|---|---|---|---|---|---|
| Icarus Verilog | GPL (무료) | Verilog, SV 일부 | 보통 | 제한적 | 지원 |
| Verilator | LGPL (무료) | Verilog, SV (합성 서브셋) | 매우 빠름 | 합성 가능 서브셋 | 지원 |
| GHDL | GPL (무료) | VHDL-87/93/2002/2008 | 빠름 | 해당 없음 | 지원 |
| ModelSim/QuestaSim | 상용 | Verilog, SV, VHDL | 보통 | 완전 지원 | 지원 |
| VCS | 상용 (Synopsys) | Verilog, SV, VHDL | 빠름 | 완전 지원 | 지원 |
| Xcelium | 상용 (Cadence) | Verilog, SV, VHDL | 빠름 | 완전 지원 | 지원 |
오픈소스 도구 실행 예제
# Icarus Verilog — 컴파일 후 시뮬레이션 실행
iverilog -g2012 -o sim_out top.sv tb_top.sv
vvp sim_out
gtkwave dump.vcd & # 파형 뷰어
# Verilator — C++ 테스트벤치와 연동 (사이클 정확 시뮬레이션)
verilator --cc --exe --trace --build \
-CFLAGS "-std=c++17" \
top.sv sim_main.cpp
./obj_dir/Vtop
# GHDL — VHDL 시뮬레이션
ghdl -a --std=08 pkg.vhd top.vhd tb_top.vhd
ghdl -e --std=08 tb_top
ghdl -r --std=08 tb_top --vcd=dump.vcd --stop-time=1ms
Verilator는 RTL을 C++ 모델로 컴파일하여 네이티브 속도로 실행하므로, 대규모 설계(수백만 게이트)에서 이벤트 기반 시뮬레이터(Icarus, ModelSim) 대비 10~100배 빠른 성능을 보입니다. 다만 시뮬레이션 전용 구문(#delay, fork/join)을 지원하지 않으므로, 합성 가능 RTL 검증에 특화되어 있습니다.
파형 분석과 디버깅
시뮬레이션 결과를 시각적으로 분석하기 위해 파형 뷰어를 사용합니다. 파형 덤프 포맷에 따라 파일 크기와 로딩 속도가 크게 달라집니다.
| 포맷 | 확장자 | 특징 | 도구 |
|---|---|---|---|
| VCD | .vcd | 표준 포맷, 큰 파일 크기, 모든 도구 호환 | GTKWave, Icarus, GHDL |
| FST | .fst | GTKWave 전용 압축 포맷, VCD 대비 5~50배 작음 | GTKWave, Verilator |
| WDB | .wdb | Xilinx 전용 포맷, Vivado Simulator에서 생성 | Vivado |
| FSDB | .fsdb | Synopsys 전용 포맷, Verdi 파형 뷰어에서 사용 | VCS, Verdi |
파형 덤프 패턴
// VCD 전체 덤프
initial begin
$dumpfile("sim.vcd");
$dumpvars(0, tb_top); // 0 = 모든 계층
end
// 특정 모듈만 덤프 (파일 크기 절약)
initial begin
$dumpfile("dut_only.vcd");
$dumpvars(1, tb_top.dut); // 1 = 해당 모듈만
$dumpvars(0, tb_top.dut.fsm); // 0 = FSM 하위 전체
end
// 시간 범위 제한 (디버깅 구간만)
initial begin
#1000; // 1000ns 이후부터 덤프 시작
$dumpfile("debug_range.vcd");
$dumpvars(0, tb_top);
#5000;
$dumpoff; // 덤프 중단
end
시뮬레이션 디버깅 기법
$display/$monitor: 텍스트 기반 신호 추적입니다.$monitor는 감시 대상 신호가 변할 때마다 자동 출력합니다$strobe: 현재 시뮬레이션 스텝의 마지막에 값을 출력합니다.$display와 달리 논블로킹 할당이 완료된 후의 값을 보여줍니다- 즉시 어서션:
assert (condition) else $error("msg");로 잘못된 상태를 즉시 감지합니다 - GTKWave 활용: 신호 그룹화(grouping), 아날로그 뷰(analog view), 트리거 마커(trigger marker)를 사용하여 복잡한 파형을 효과적으로 분석할 수 있습니다
- 계층적 접근: 테스트벤치에서
tb_top.dut.internal_sig와 같이 DUT 내부 신호를 직접 참조하여 관찰할 수 있습니다 (시뮬레이션 전용)
코드 커버리지
코드 커버리지(Code Coverage)는 시뮬레이션이 RTL 코드의 어느 부분을 실행했는지를 정량적으로 측정합니다. 기능 커버리지(Functional Coverage)와 함께 검증 완료 기준을 구성합니다. FPGA 문서의 검증 섹션도 함께 참조하세요.
| 커버리지 유형 | 측정 대상 | 산업 표준 목표 |
|---|---|---|
| 라인(Line) | 실행된 코드 라인 수 | 95% 이상 |
| 분기(Branch) | if/else, case 분기 실행 여부 | 90% 이상 |
| 조건(Condition) | 복합 조건의 개별 부울 조합 | 85% 이상 |
| 토글(Toggle) | 각 신호의 0→1, 1→0 전이 발생 여부 | 90% 이상 |
| FSM 상태 | 방문된 상태 수 | 100% |
| FSM 전이 | 실행된 상태 전이 수 | 100% |
# Verilator 커버리지 수집
verilator --cc --exe --trace --coverage top.sv sim_main.cpp
./obj_dir/Vtop
verilator_coverage --annotate logs/coverage.dat
# VCS 커버리지 수집 (상용)
vcs -cm line+branch+cond+tgl+fsm top.sv tb_top.sv
./simv -cm line+branch+cond+tgl+fsm
urg -dir simv.vdb -report coverage_report
코드 커버리지 vs 기능 커버리지
- 코드 커버리지: RTL 코드가 얼마나 실행되었는지를 측정합니다. 높은 코드 커버리지는 필요 조건이지만 충분 조건은 아닙니다
- 기능 커버리지: 설계 사양의 기능이 얼마나 검증되었는지를 측정합니다. SystemVerilog의
covergroup/coverpoint로 정의합니다 - 커버리지 홀(Coverage Hole): 커버리지 미달 영역을 분석하여 추가 테스트 시나리오를 생성합니다. 도달 불가능한 코드(dead code)는 커버리지에서 제외(exclude)할 수 있습니다
- 회귀 테스트(Regression): 커버리지를 누적(merge)하여 전체 테스트 스위트의 커버리지를 추적합니다. CI/CD 파이프라인에 통합하여 커버리지 하락을 자동 감지합니다
UVM 개요
UVM(Universal Verification Methodology)은 IEEE 1800.2 표준으로 정의된 SystemVerilog 기반 검증 방법론입니다. 객체 지향 클래스 라이브러리를 제공하여, 재사용 가능한 검증 환경을 구축할 수 있습니다. 대규모 ASIC 프로젝트와 복잡한 FPGA SoC에서 산업 표준으로 사용됩니다.
UVM 최소 예제 — 시퀀스 아이템, 시퀀스, 드라이버
// 트랜잭션 정의 — 검증 대상의 입력/출력을 추상화
class alu_item extends uvm_sequence_item;
`uvm_object_utils(alu_item)
rand logic [7:0] a, b;
rand logic [1:0] op;
logic [8:0] result; // DUT 출력 (non-rand)
constraint c_op { op inside {[0:3]}; }
function new(string name = "alu_item");
super.new(name);
endfunction
endclass
// 시퀀스 — 트랜잭션 스트림 생성
class alu_sequence extends uvm_sequence #(alu_item);
`uvm_object_utils(alu_sequence)
function new(string name = "alu_sequence");
super.new(name);
endfunction
task body();
repeat (100) begin
alu_item item = alu_item::type_id::create("item");
start_item(item);
assert(item.randomize());
finish_item(item);
end
endtask
endclass
// 드라이버 — 트랜잭션을 핀 레벨 신호로 변환
class alu_driver extends uvm_driver #(alu_item);
`uvm_component_utils(alu_driver)
virtual alu_if vif; // 가상 인터페이스
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
alu_item item;
seq_item_port.get_next_item(item);
@(posedge vif.clk);
vif.a <= item.a;
vif.b <= item.b;
vif.op <= item.op;
@(posedge vif.clk);
item.result = {vif.carry, vif.result};
seq_item_port.item_done();
end
endtask
endclass
코드 설명
uvm_sequence_item: 검증 대상과 주고받는 데이터 단위(트랜잭션)를 정의합니다.rand필드는 constrained random 생성 대상이 됩니다uvm_sequence:body()태스크에서 트랜잭션 스트림을 생성합니다.start_item/finish_item으로 시퀀서와 핸드셰이크합니다uvm_driver: 시퀀서에서 트랜잭션을 받아 가상 인터페이스(virtual interface)를 통해 DUT의 핀 레벨 신호로 변환합니다- UVM 팩토리:
`uvm_object_utils/`uvm_component_utils매크로는 UVM 팩토리에 클래스를 등록하여, 테스트별로 드라이버나 시퀀스를 오버라이드할 수 있게 합니다
UVM 도입 기준: 소규모 프로젝트(수천 LUT)에서는 앞서 소개한 자체 검증 테스트벤치로 충분합니다. UVM은 IP를 여러 프로젝트에서 재사용하거나, 팀 간 검증 환경을 공유해야 하는 대규모 프로젝트에서 진가를 발휘합니다. UVM의 constrained random, 기능 커버리지, 시퀀스 계층 구조는 이 페이지의 constrained random 검증과 기능 커버리지 섹션에서 다룬 개념을 체계적으로 통합합니다.
cocotb — Python 기반 검증
cocotb(Coroutine-based Co-simulation Testbench)는 Python으로 HDL 테스트벤치를 작성할 수 있는 오픈소스 프레임워크입니다. SystemVerilog 테스트벤치를 작성하지 않고도 Icarus Verilog, Verilator, GHDL, 상용 시뮬레이터와 연동하여 DUT를 구동하고 검증할 수 있습니다. Python의 async/await 코루틴으로 시뮬레이션 이벤트를 제어합니다.
# test_counter.py — cocotb 테스트벤치 예제
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
@cocotb.test()
async def test_counter_enable(dut):
"""인에이블 카운터 기본 동작 검증"""
# 100MHz 클록 생성
cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
# 리셋 시퀀스
dut.rst_n.value = 0
dut.en.value = 0
await Timer(50, units="ns")
dut.rst_n.value = 1
await RisingEdge(dut.clk)
# 인에이블 활성화 후 10 사이클 카운트
dut.en.value = 1
for expected in range(1, 11):
await RisingEdge(dut.clk)
actual = int(dut.count.value)
assert actual == expected, \
f"cycle {expected}: got {actual}, expected {expected}"
# 인에이블 비활성화 — 카운터 정지 확인
dut.en.value = 0
frozen = int(dut.count.value)
for _ in range(5):
await RisingEdge(dut.clk)
assert int(dut.count.value) == frozen, "카운터가 정지하지 않았습니다"
dut._log.info("테스트 통과: 인에이블 카운터 정상 동작")
# Makefile 기반 실행 (cocotb 표준 패턴)
SIM ?= icarus
TOPLEVEL_LANG ?= verilog
VERILOG_SOURCES = counter_enable.sv
TOPLEVEL = counter_enable
MODULE = test_counter
include $(shell cocotb-config --makefiles)/Makefile.sim
# 실행: make SIM=icarus
# Verilator: make SIM=verilator EXTRA_ARGS="--trace"
# GHDL: make SIM=ghdl TOPLEVEL_LANG=vhdl
pip install cocotb로 설치합니다.
형식 검증 (Formal Verification)
시뮬레이션은 입력 벡터를 선택하여 특정 시나리오를 확인하는 반면, 형식 검증(Formal Verification)은 수학적 증명(Mathematical Proof)을 통해 모든 가능한 입력 조합에 대해 설계의 속성(Property)이 성립하는지를 검증합니다. 시뮬레이션으로는 도달하기 어려운 코너 케이스(Corner Case)를 자동으로 탐색할 수 있으므로, 프로토콜 준수, 상호 배제(Mutual Exclusion), 데드락 부재 등의 검증에 매우 효과적입니다.
형식 검증 기본 개념
형식 검증은 크게 모델 검사(Model Checking)와 정리 증명(Theorem Proving)으로 나뉘며, RTL 설계에서는 모델 검사가 주로 사용됩니다. 핵심 기법과 용어는 다음과 같습니다.
- BMC(Bounded Model Checking): 유한한 클록 사이클(bound) 내에서 속성 위반을 탐색합니다. 반례(Counter-Example)를 빠르게 찾을 수 있지만, 위반이 없는 것이 무한 사이클에서도 성립함을 보장하지는 않습니다
- k-Induction: BMC의 한계를 보완하는 기법입니다. 기저 사례(Base Case)에서 k 사이클까지 속성이 성립함을 확인하고, 귀납 단계(Induction Step)에서 k 사이클 성립을 가정하여 k+1 사이클 성립을 증명합니다. 두 단계 모두 통과하면 무한 사이클에 대한 완전 증명이 됩니다
- 상태 공간 폭발(State Space Explosion): 레지스터 비트 수에 따라 상태 공간이 지수적으로 증가하므로, 형식 검증은 전체 SoC보다는 개별 모듈이나 인터페이스 단위에 적용하는 것이 실용적입니다. assume 제약을 통해 탐색 공간을 줄이는 것이 핵심 전략입니다
- assert: 설계가 반드시 만족해야 하는 속성입니다. 위반 시 반례가 생성됩니다
- assume: 입력에 대한 전제 조건입니다. 형식 엔진이 이 조건을 만족하는 입력만 탐색합니다
- cover: 특정 상태에 도달 가능한지를 확인합니다. 도달 가능하면 해당 경로(트레이스)를 생성합니다
assert, assume, cover)을 이해한 후 진행하세요.
고급 SVA(SystemVerilog Assertion) 기법
SVA(SystemVerilog Assertion)는 형식 검증과 시뮬레이션 모두에서 사용할 수 있는 속성 기술 언어입니다. 기본적인 assert property 외에, 복잡한 프로토콜 검증에 필요한 고급 시퀀스 연산자와 속성 구문을 지원합니다.
[*0:$](무한 반복): 시퀀스가 0회 이상 무한 반복될 수 있음을 표현합니다. 예:req ##1 busy[*0:$] ##1 ack는 요청 후 임의의 사이클 동안 busy가 유지된 뒤 ack가 오는 패턴입니다first_match: 여러 매칭 중 가장 먼저 성공하는 것만 취합니다. 무한 반복과 함께 사용하면 가장 빠른 응답 시점을 캡처합니다within: 한 시퀀스가 다른 시퀀스의 시간 범위 안에서 완료되어야 함을 표현합니다- 다중 클록 속성:
@(posedge clk_a)와@(posedge clk_b)를 하나의 속성에서 사용하여 클록 도메인 간 프로토콜을 검증합니다 - 활성 속성(Liveness):
s_eventually는 "언젠가 반드시 발생"하는 속성을 표현합니다. 데드락 부재, 응답 보장 등의 검증에 사용됩니다. BMC로는 증명할 수 없으며, k-Induction이나 PDR 엔진이 필요합니다
AXI 핸드셰이크 형식 검증기 예제
// AXI4 Write Channel 형식 검증 — assert/assume 기반
module axi_write_formal (
input logic clk,
input logic rst_n,
// AW 채널
input logic AWVALID,
input logic AWREADY,
input logic [7:0] AWLEN,
// W 채널
input logic WVALID,
input logic WREADY,
input logic WLAST,
// B 채널
input logic BVALID,
input logic BREADY
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// AXI 규칙: VALID가 어서트되면 READY가 올 때까지 유지해야 합니다
aw_stable: assert property (
AWVALID && !AWREADY |=> AWVALID
);
w_stable: assert property (
WVALID && !WREADY |=> WVALID
);
// WLAST는 AWLEN+1번째 전송에서 반드시 어서트되어야 합니다
logic [8:0] w_count;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) w_count <= '0;
else if (WVALID && WREADY && WLAST) w_count <= '0;
else if (WVALID && WREADY) w_count <= w_count + 1;
// B 응답은 W 전송 완료 후에만 발생해야 합니다
b_after_w: assert property (
BVALID |-> w_count == '0
);
// 활성 속성: AWVALID 어서트 후 반드시 응답이 와야 합니다
aw_response: assert property (
AWVALID && AWREADY |=> s_eventually(BVALID && BREADY)
);
// 도달 가능성: 정상 쓰기 완료 시퀀스가 가능한지 확인합니다
cover_write: cover property (
AWVALID && AWREADY ##1 (WVALID && WREADY)[*1:4]
##0 WLAST ##1 BVALID && BREADY
);
endmodule
SymbiYosys 실전 활용
SymbiYosys(sby)는 오픈소스 형식 검증 프레임워크로, Yosys 합성 엔진과 다양한 SAT/SMT 솔버를 통합합니다. .sby 설정 파일 하나로 엔진 선택, 바운드 설정, 멀티 태스크 실행을 관리할 수 있습니다.
.sby 파일 구조
[options]: 검증 모드(mode bmc,mode prove,mode cover)와 바운드 깊이(depth)를 설정합니다[engines]: 사용할 솔버를 지정합니다.smtbmc는 범용적으로 안정적이며,abc pdr는 귀납 증명에 강합니다. 여러 엔진을 나열하면 순서대로 시도합니다[script]: Yosys 스크립트입니다.read -formal로 소스를 읽고,prep -top으로 최상위 모듈을 지정합니다[files]: 검증 대상 소스 파일 목록입니다
반례(Counter-Example) 분석: 검증 실패 시 SymbiYosys는 VCD 또는 FST 파형 파일을 생성합니다. GTKWave 등의 파형 뷰어로 열어 위반이 발생하는 사이클과 신호 값을 추적합니다. assert가 실패한 시점부터 역추적하여 원인 신호를 분석하는 것이 효율적입니다.
아비터 상호 배제 + 기아 방지 형식 증명 예제
// 2-포트 라운드 로빈 아비터 — 형식 검증
module arbiter_formal (
input logic clk,
input logic rst_n,
input logic req0, req1,
output logic gnt0, gnt1
);
logic last_grant;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
gnt0 <= 1'b0;
gnt1 <= 1'b0;
last_grant <= 1'b0;
end else begin
gnt0 <= 1'b0;
gnt1 <= 1'b0;
case ({req1, req0})
2'b01: begin gnt0 <= 1'b1; last_grant <= 1'b0; end
2'b10: begin gnt1 <= 1'b1; last_grant <= 1'b1; end
2'b11: if (last_grant)
begin gnt0 <= 1'b1; last_grant <= 1'b0; end
else
begin gnt1 <= 1'b1; last_grant <= 1'b1; end
default: ;
endcase
end
// 형식 검증 속성
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// 상호 배제: gnt0과 gnt1이 동시에 활성화되면 안 됩니다
mutex: assert property (!(gnt0 && gnt1));
// 기아 방지: 요청이 지속되면 반드시 허가가 와야 합니다
no_starve0: assert property (
req0 |-> s_eventually(gnt0)
);
no_starve1: assert property (
req1 |-> s_eventually(gnt1)
);
// 도달 가능성: 양쪽 동시 요청 시나리오
cover_both: cover property (
req0 && req1 ##1 gnt0 ##1 req0 && req1 ##1 gnt1
);
endmodule
SymbiYosys 설정 파일
# arbiter.sby — 아비터 형식 검증 설정
[tasks]
bmc # BMC로 반례 탐색
prove # k-Induction 완전 증명
cover # 도달 가능성 확인
[options]
bmc: mode bmc
bmc: depth 30
prove: mode prove
prove: depth 30
cover: mode cover
cover: depth 30
[engines]
bmc: smtbmc
prove: abc pdr
cover: smtbmc
[script]
read -formal arbiter_formal.sv
prep -top arbiter_formal
[files]
arbiter_formal.sv
# SymbiYosys 실행
sby arbiter.sby bmc # BMC 태스크만 실행
sby arbiter.sby prove # 완전 증명 실행
sby -f arbiter.sby # 전체 태스크 재실행 (기존 결과 덮어쓰기)
# 결과 확인
ls arbiter_bmc/engine_0/ # PASS 시 빈 디렉터리, FAIL 시 trace.vcd 생성
gtkwave arbiter_bmc/engine_0/trace.vcd # 반례 파형 분석
합성 가이드라인
합성(Synthesis)은 HDL 코드를 실제 하드웨어(게이트 넷리스트)로 변환하는 과정입니다. 올바른 합성 결과를 얻으려면, 합성 도구가 이해할 수 있는 코딩 패턴을 따르고, 적절한 최적화 지시문을 사용해야 합니다.
합성이란 무엇인가
합성은 요리 레시피(HDL 코드)를 실제 주방 설비(논리 게이트, 플립플롭)로 변환하는 과정입니다. 레시피에 '소금을 넣습니다'고 쓰면, 합성 도구는 정확히 어느 선반에서 소금을 가져와 어떤 냄비에 넣을지를 결정합니다.
가장 간단한 예로 assign y = a & b;를 합성 도구가 처리하는 3단계를 살펴보겠습니다.
- RTL 파싱: 합성 도구가 소스 코드를 읽고, "a와 b의 AND 연산 결과를 y에 연결하라"는 의미를 추출합니다. 이 단계에서 문법 오류와 타입 불일치가 검출됩니다
- 논리 최적화: 불필요한 게이트를 제거하고, 공통 부분식(Common Subexpression)을 추출하여 게이트 수를 줄입니다. 예를 들어
(a & b) | (a & c)는a & (b | c)로 최적화될 수 있습니다 - 기술 매핑(Technology Mapping): 최적화된 논리를 대상 디바이스에 매핑합니다. FPGA의 경우 LUT(Look-Up Table)에 매핑하여 진리표로 표현하고, ASIC의 경우 표준 셀 라이브러리(NAND2, NOR2, DFF 등)에 매핑합니다
합성이 실패하는 3가지 주요 원인:
- 비합성 구문 사용:
#10딜레이,$display,initial블록의 복잡한 초기화 등 시뮬레이션 전용 구문이 합성 대상 코드에 포함된 경우입니다 - 타이밍 불만족: 조합 논리 경로(Critical Path)의 지연 시간이 클록 주기보다 길어서 타이밍 제약을 만족하지 못하는 경우입니다
- 리소스 초과: FPGA에서 LUT, BRAM, DSP 블록 등의 가용 리소스를 초과하거나, ASIC에서 면적 제약을 벗어나는 경우입니다
initial 블록의 복잡한 초기화, real 타입 연산, fork/join 병렬 실행은 합성 도구가 처리하지 못합니다. 반드시 합성 가능 여부를 확인한 후 RTL 코드를 작성하세요.
합성 가능/불가능 구문의 상세 구분은 아래 합성 가능 vs 시뮬레이션 전용 구문을 참고하세요.
합성 가능 vs 시뮬레이션 전용 구문
합성 도구는 HDL의 일부 구문만 하드웨어로 변환할 수 있습니다. 시뮬레이션 전용 구문은 테스트벤치에서만 사용해야 하며, 합성 대상 코드에 포함되면 오류가 발생합니다.
SystemVerilog 구문 분류
| 분류 | 구문 | 설명 |
|---|---|---|
| 합성 가능 | always_ff | 순차 논리 (플립플롭) 기술 |
always_comb | 조합 논리 기술 | |
assign | 연속 할당 (와이어 연결) | |
if/else, case | 조건 분기 (MUX로 합성) | |
module 인스턴스화 | 계층적 설계 | |
generate | 반복/조건부 하드웨어 구조 생성 | |
interface, struct | 신호 번들링과 타입 정의 | |
| 산술/논리/비교 연산자 | 가산기, 비교기, 시프터 등으로 합성 | |
| 시뮬레이션 전용 | #delay | 시간 지연 (물리적 의미 없음) |
initial | 초기화 블록 (FPGA에서는 일부 지원) | |
$display, $monitor | 텍스트 출력 시스템 태스크 | |
$readmemh, $readmemb | 파일에서 메모리 초기화 | |
fork/join | 병렬 스레드 실행 | |
wait | 이벤트 대기 | |
real 타입 | 부동소수점 (합성 불가) |
VHDL 구문 분류
| 분류 | 구문 | 설명 |
|---|---|---|
| 합성 가능 | process(clk), process(all) | 감도 리스트 기반 조합/순차 논리 |
신호 할당 (<=) | concurrent/sequential signal assignment | |
component 인스턴스화 | 계층적 설계 | |
generate | for generate, if generate | |
generic | 파라미터화 | |
| 시뮬레이션 전용 | wait for / wait until | 시간/이벤트 대기 |
report / assert | 텍스트 출력과 검증 (합성 도구가 무시) | |
file I/O | 파일 읽기/쓰기 | |
real / time 타입 | 시뮬레이션 전용 데이터 타입 |
리소스 추론 패턴
합성 도구는 RTL 코드의 패턴을 인식하여 FPGA의 전용 하드웨어 리소스(BRAM, DSP, SRL 등)로 자동 추론합니다. 올바른 코딩 패턴을 따르면 리소스 활용 효율이 크게 향상됩니다.
LUT 추론 — 조합 논리
// 조합 논리 → LUT로 합성
always_comb begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcase
end
BRAM 추론 — 블록 RAM
// BRAM 추론 조건: 2D 배열 + 클록 동기 읽기/쓰기
(* ram_style = "block" *) // 합성 지시문 (선택)
logic [31:0] mem [1024]; // 32비트 × 1024 = 4KB
// 동기 쓰기 포트
always_ff @(posedge clk)
if (wr_en)
mem[wr_addr] <= wr_data;
// 동기 읽기 포트 (BRAM 추론 핵심)
always_ff @(posedge clk)
rd_data <= mem[rd_addr];
분산 RAM 추론
// 분산 RAM: 소규모 배열 + 비동기 읽기
(* ram_style = "distributed" *)
logic [7:0] lut_ram [32];
always_ff @(posedge clk)
if (wr_en)
lut_ram[wr_addr] <= wr_data;
assign rd_data = lut_ram[rd_addr]; // 비동기 읽기 → 분산 RAM
DSP 추론 — 곱셈-누산
// DSP48 추론: a * b + c 패턴
(* use_dsp = "yes" *)
logic signed [17:0] a, b;
logic signed [47:0] c, result;
always_ff @(posedge clk)
result <= a * b + c; // DSP48E2 하나로 매핑
SRL 추론 — 시프트 레지스터
// SRL(Shift Register LUT) 추론 패턴
logic [15:0] delay_line;
always_ff @(posedge clk)
delay_line <= {delay_line[14:0], data_in}; // 16단 시프트
assign data_out = delay_line[15]; // 16 클록 지연
합성 최적화 지시문
합성 지시문(Synthesis Directive)은 합성 도구에게 특정 최적화를 지시하거나 제한하는 주석(attribute) 형태의 지시입니다. FPGA 벤더마다 지원되는 지시문이 다릅니다.
| 용도 | Xilinx (Vivado) | Intel (Quartus) |
|---|---|---|
| 신호 유지 (최적화 방지) | (* keep = "true" *) | (* preserve *) |
| 완전 유지 (계층 관통 방지) | (* dont_touch = "true" *) | (* noprune *) |
| 팬아웃 제한 | (* max_fanout = 50 *) | (* maxfan = 50 *) |
| 비동기 레지스터 표시 | (* ASYNC_REG = "TRUE" *) | (타이밍 제약으로 처리) |
| RAM 스타일 지정 | (* ram_style = "block" *) | (* ramstyle = "M20K" *) |
| ROM 스타일 지정 | (* rom_style = "block" *) | (* romstyle = "M20K" *) |
| DSP 사용 강제 | (* use_dsp = "yes" *) | (* multstyle = "dsp" *) |
// CDC 동기화기에 ASYNC_REG 지시문 적용
(* ASYNC_REG = "TRUE" *)
logic [1:0] sync_ff;
always_ff @(posedge dst_clk)
sync_ff <= {sync_ff[0], async_in};
assign sync_out = sync_ff[1];
// 팬아웃 제한으로 타이밍 개선
(* max_fanout = 32 *)
logic enable_distributed;
always_ff @(posedge clk)
enable_distributed <= global_enable;
합성 경고와 대처
합성 경고는 잠재적인 설계 문제를 알려주는 중요한 피드백입니다. 다음은 가장 흔한 합성 경고와 대처 방법입니다.
- 래치 추론 경고 (
WARNING: [Synth 8-327] inferring latch)- 원인:
always_comb블록에서else또는default누락 - 해결: 블록 시작 시 모든 출력에 기본값을 할당하거나, 모든 분기에서 출력을 명시적으로 할당합니다
- 원인:
- 미사용 신호 경고 (
WARNING: signal has no load)- 의도적인 경우:
(* keep = "true" *)지시문으로 최적화를 방지하거나, 디버그 포트로 활용합니다 - 비의도적인 경우: 코드 리뷰를 통해 불필요한 신호를 제거합니다
- 의도적인 경우:
- 다중 드라이버 경고 (
ERROR: multiple drivers)- 원인: 하나의 신호가 여러
always블록이나assign에서 구동됩니다 - 해결: 각 신호는 하나의 드라이버에서만 할당되도록 설계를 재구성합니다. 양방향(tristate)이 필요하면 명시적
inout포트를 사용합니다
- 원인: 하나의 신호가 여러
- 조합 루프 경고 (
WARNING: combinational loop detected)- 원인: 조합 논리의 출력이 피드백 없이 자신의 입력으로 돌아갑니다
- 해결: 피드백 경로에 레지스터(플립플롭)를 삽입하여 루프를 끊습니다
타이밍과 전력 인식 설계
HDL 코드 수준에서 타이밍(Timing)과 전력(Power)을 고려한 설계는 합성 후 타이밍 클로저(Timing Closure)와 전력 목표 달성에 직접적인 영향을 미칩니다. 합성 도구의 최적화에만 의존하지 않고, RTL 단계에서 아키텍처적 결정을 내리는 것이 효과적입니다.
타이밍 인식 RTL 설계
타이밍 위반(Timing Violation)은 조합 논리의 전파 지연(Propagation Delay)이 클록 주기를 초과할 때 발생합니다. 크리티컬 패스(Critical Path)는 가장 긴 전파 지연을 가진 조합 논리 경로이며, 설계의 최대 동작 주파수를 결정합니다.
- 파이프라인 삽입(Pipeline Insertion): 긴 조합 논리 경로를 여러 스테이지로 나누어 레지스터를 삽입합니다. 지연 시간(Latency)은 증가하지만 처리량(Throughput)은 유지됩니다
- 논리 복제(Logic Duplication): 팬아웃(Fan-out)이 큰 신호의 구동 부하를 줄이기 위해 동일한 논리를 복제합니다. 합성 도구의
(* keep *)속성으로 복제된 논리가 최적화되지 않도록 보호합니다 - 연산 재배치(Operation Reordering):
(a + b) + c를a + (b + c)로 재배치하여 캐리 체인 깊이를 줄입니다. 트리 구조의 덧셈기가 체인 구조보다 지연이 짧습니다 - 레지스터 리타이밍(Register Retiming): 합성 도구가 자동으로 레지스터 위치를 이동하여 크리티컬 패스를 균등화합니다. Vivado의
-retiming옵션, Synopsys Design Compiler의optimize_registers로 활성화합니다
곱셈+덧셈 파이프라인 예제
// 파이프라인 적용 전 — 긴 크리티컬 패스
always_ff @(posedge clk)
result <= (a * b) + c; // 곱셈기 + 덧셈기가 한 사이클에 동작
// 파이프라인 적용 후 — 2 스테이지
logic [31:0] mul_result;
logic [15:0] c_d1; // c를 1 사이클 지연시켜 정렬
// 스테이지 1: 곱셈
always_ff @(posedge clk) begin
mul_result <= a * b;
c_d1 <= c; // 데이터 정렬용 지연 레지스터
end
// 스테이지 2: 덧셈
always_ff @(posedge clk)
result <= mul_result + c_d1;
저전력 HDL 설계 기법
전력 소비는 동적 전력(Dynamic Power)과 정적 전력(Static/Leakage Power)으로 구성됩니다. 동적 전력은 P = α × C × V² × f (α: 스위칭 활동도, C: 부하 커패시턴스, V: 전압, f: 주파수)로 결정되며, RTL 수준에서 스위칭 활동도(α)를 줄이는 것이 가장 효과적입니다.
- 클록 게이팅(Clock Gating): 동작하지 않는 레지스터의 클록을 차단하여 불필요한 스위칭을 방지합니다. ASIC에서는 ICG(Integrated Clock Gating) 셀을 사용하고, FPGA에서는 CE(Clock Enable) 입력으로 구현합니다
- 오퍼랜드 분리(Operand Isolation): 연산 결과가 사용되지 않을 때 입력을 고정하여 연산기 내부 스위칭을 방지합니다. 곱셈기, 나눗셈기 등 넓은 데이터패스에 효과적입니다
- BRAM 접근 최적화: 불필요한 BRAM 읽기를 차단하여 메모리 전력을 줄입니다. 읽기 인에이블(
re) 신호를 활용합니다
클록 게이팅 예제
// ASIC — ICG(Integrated Clock Gating) 셀 인스턴스화
module icg_cell (
input logic clk,
input logic en,
output logic gclk
);
logic en_latch;
// 래치 기반 글리치 방지 — 클록 로우 구간에서 인에이블 캡처
always_latch
if (!clk) en_latch <= en;
assign gclk = clk & en_latch;
endmodule
// FPGA — CE(Clock Enable) 기반 (별도 ICG 불필요)
always_ff @(posedge clk)
if (ce) data_q <= data_d; // ce=0이면 FF 유지 → 스위칭 없음
UPF(Unified Power Format) 개요: ASIC 설계에서는 IEEE 1801 UPF를 사용하여 전력 도메인(Power Domain), 전력 상태(Power State), 리텐션(Retention), 아이솔레이션(Isolation) 전략을 RTL과 별도로 기술합니다. 합성/P&R 도구가 UPF를 읽어 전력 관리 셀을 자동 삽입합니다.
시뮬레이션/합성 불일치 디버깅
시뮬레이션에서는 정상 동작하지만 합성 후 실제 하드웨어에서 다르게 동작하는 경우는 HDL 설계에서 가장 까다로운 문제 중 하나입니다. 주요 원인과 디버깅 방법은 다음과 같습니다.
- 래치 추론:
always_comb블록에서 모든 분기에 할당이 없으면 래치가 생성됩니다. 시뮬레이션에서는 초기값이 X로 동작하지만, 합성 후에는 이전 값을 유지하여 다른 결과를 만듭니다 - initial 블록 의존: ASIC에서는 initial 블록이 무시되므로, 레지스터 초기값에 의존하는 코드는 합성 후 동작이 달라집니다
- 블로킹/논블로킹 혼용:
always_ff에서 블로킹 할당(=)을 사용하면 시뮬레이션과 합성의 동작이 달라질 수 있습니다. 순차 논리에서는 반드시 논블로킹 할당(<=)을 사용해야 합니다 - 메타스테이빌리티: CDC 경로에 동기화기가 없으면 시뮬레이션에서는 문제가 나타나지 않지만, 실제 하드웨어에서는 간헐적 오류가 발생합니다
합성 리포트 핵심 확인 항목: 합성 완료 후 반드시 확인해야 하는 항목은 (1) 래치 추론 경고, (2) 미사용 신호/포트 경고, (3) 리소스 사용량(LUT/FF/BRAM/DSP), (4) 예상 최대 주파수(Estimated Fmax)입니다. Vivado에서는 report_utilization과 report_timing_summary 명령으로 확인합니다.
IP 재사용과 패키징
효율적인 FPGA/ASIC 개발을 위해서는 검증된 IP(Intellectual Property) 블록을 재사용하는 것이 필수적입니다. 잘 설계된 IP는 프로젝트 간에 재사용할 수 있으며, 팀 간 협업과 오픈소스 생태계에도 기여할 수 있습니다.
모듈러 설계 원칙
재사용 가능한 IP를 설계하기 위해서는 다음 원칙을 따릅니다.
- 표준 인터페이스 사용: AXI4, AXI4-Lite, AXI4-Stream, Wishbone 등의 표준 버스 인터페이스를 채택하면, 다양한 SoC 환경에 통합하기 쉽습니다
- 파라미터화 전략: 데이터 폭(WIDTH), 버퍼 깊이(DEPTH), FIFO 크기 등을
parameter/generic으로 외부에서 설정할 수 있게 합니다. 과도한 파라미터화는 검증 부담을 증가시키므로, 실제 사용 시나리오에 맞는 범위를 정의합니다 - 문서화: 포트 설명, 타이밍 다이어그램, 레지스터 맵, 제약 조건을 문서에 포함합니다. WaveDrom 같은 도구로 타이밍 다이어그램을 JSON으로 관리할 수 있습니다
- 자체 포함(Self-Contained): IP 내부에서 외부 글로벌 신호에 의존하지 않도록 합니다. 클록과 리셋은 명시적 포트로 받습니다
- 테스트 환경 포함: IP와 함께 테스트벤치, 시뮬레이션 스크립트, 예상 결과를 배포합니다
Vivado IP Packager
Xilinx Vivado의 IP Packager는 RTL 모듈을 IP-XACT 표준의 재사용 가능한 IP 코어로 패키징합니다. 패키징된 IP는 Vivado IP Catalog에 등록되어 블록 디자인(Block Design)에서 드래그 앤 드롭으로 사용할 수 있습니다.
- 포트 매핑: RTL 포트를 AXI, AXI-Lite, AXI-Stream 등의 표준 인터페이스에 바인딩합니다
- 주소 맵 정의: AXI-Lite 슬레이브의 레지스터 주소와 크기를 정의합니다
- 인터럽트: 인터럽트 출력 포트를 정의하면, SoC 통합 시 인터럽트 컨트롤러에 자동 연결됩니다
- 드라이버 생성: Vitis에서 사용할 베어메탈 드라이버 스텁(stub)을 자동 생성합니다
# Vivado Tcl — RTL 프로젝트를 IP로 패키징
ipx::package_project -root_dir ./ip_repo/my_ip_1.0 \
-vendor mycompany -library user -taxonomy /UserIP
# AXI-Lite 인터페이스 바인딩
ipx::add_bus_interface S_AXI [ipx::current_core]
set_property abstraction_type_vlnv \
xilinx.com:interface:aximm_rtl:1.0 \
[ipx::get_bus_interfaces S_AXI]
# 주소 공간 정의 (4KB)
ipx::add_memory_map S_AXI [ipx::current_core]
set_property range 4096 [ipx::get_address_blocks \
reg0 -of_objects [ipx::get_memory_maps S_AXI]]
# IP 패키징 완료
ipx::create_xgui_files [ipx::current_core]
ipx::save_core [ipx::current_core]
FuseSoC 패키지 관리
FuseSoC는 HDL 프로젝트의 의존성 관리와 빌드 자동화를 위한 오픈소스 패키지 관리자입니다. Python의 pip이나 Rust의 cargo와 유사한 역할을 합니다. 프로젝트의 소스 파일, 의존성, 빌드 설정을 .core 파일(CAPI2 포맷)에 기술합니다.
# my_fifo.core — FuseSoC CAPI2 패키지 파일
CAPI=2:
name: myorg:ip:sync_fifo:1.0.0
description: Parameterized synchronous FIFO with BRAM inference
filesets:
rtl:
files:
- rtl/sync_fifo.sv
- rtl/fifo_ctrl.sv
file_type: systemVerilogSource
tb:
files:
- tb/tb_sync_fifo.sv
file_type: systemVerilogSource
depend:
- myorg:ip:axi_utils:1.0.0
targets:
default: &default
filesets: [rtl]
toplevel: sync_fifo
sim:
<<: *default
filesets: [rtl, tb]
toplevel: tb_sync_fifo
default_tool: verilator
tools:
verilator:
mode: cc
verilator_options: [--trace, --coverage]
icarus:
iverilog_options: [-g2012]
synth_vivado:
<<: *default
default_tool: vivado
tools:
vivado:
part: xc7a100tcsg324-1
# FuseSoC 사용법
pip install fusesoc
# 라이브러리 등록
fusesoc library add my_cores ./cores
# 시뮬레이션 실행
fusesoc run --target=sim myorg:ip:sync_fifo
# Vivado 합성
fusesoc run --target=synth_vivado myorg:ip:sync_fifo
서드파티 IP 통합
직접 설계하는 것 외에도, 벤더 IP 카탈로그와 오픈소스 IP를 활용하면 개발 시간을 크게 단축할 수 있습니다.
벤더 IP 카탈로그
- Xilinx IP Catalog: MIG(메모리 컨트롤러), PCIe, Ethernet MAC, DMA, FIFO Generator 등 수백 개의 검증된 IP를 제공합니다. Vivado Block Design에서 GUI로 설정합니다
- Intel IP Catalog: Platform Designer(구 Qsys)를 통해 Nios II, Avalon 버스, DDR 컨트롤러 등의 IP를 제공합니다
오픈소스 IP 소스
- OpenCores: UART, SPI, I2C, Wishbone 인터커넥트 등 기본적인 IP를 제공합니다. 품질이 균일하지 않으므로 코드 리뷰가 필요합니다
- LibreCores: FOSSi Foundation이 운영하는 오픈소스 IP 레지스트리입니다. FuseSoC와 연동됩니다
- lowRISC: OpenTitan(보안 칩), Ibex(RISC-V 코어) 등 산업 수준의 오픈소스 하드웨어를 제공합니다
- GitHub/GitLab: Alex Forencich의 verilog-ethernet, verilog-axi 등 고품질 오픈소스 IP가 다수 있습니다
IP 라이선스 고려사항
- Apache 2.0 / MIT: 상업 제품에 자유롭게 사용할 수 있습니다. OpenTitan, Ibex 등이 채택합니다
- LGPL: 라이브러리로 사용 시 자유롭지만, 수정 시 소스 공개 의무가 있습니다
- 벤더 독점(Proprietary): 해당 벤더 FPGA에서만 사용할 수 있으며, 소스 코드가 비공개입니다. 벤더 종속(vendor lock-in)에 주의해야 합니다
- SOLDERPAD: 하드웨어 전용 오픈소스 라이선스로, Apache 2.0을 하드웨어에 적용한 변형입니다
HDL 비교
Verilog/SystemVerilog와 VHDL은 동일한 하드웨어를 기술할 수 있지만, 문법 철학과 생태계가 다릅니다. 프로젝트의 요구사항, 팀의 경험, 사용할 도구 체인을 고려하여 선택해야 합니다.
| 항목 | Verilog / SystemVerilog | VHDL |
|---|---|---|
| 문법 스타일 | C 계열 (간결한 문법) | Ada/Pascal 계열 (명시적이고 장황한 문법) |
| 타입 시스템 | 약한 타입 (암묵적 형변환 허용) | 강한 타입 (명시적 형변환 필수) |
| 시뮬레이션 속도 | 상대적으로 빠름 | 타입 검사 오버헤드로 상대적으로 느림 |
| 산업계 채택 | 미국, 아시아 지역에서 주류 | 유럽, 방산/항공우주 분야에서 주류 |
| 오픈소스 도구 | Yosys (합성), Icarus Verilog, Verilator (시뮬레이션) | GHDL (시뮬레이션), ghdl-yosys-plugin (합성 연동) |
| 테스트벤치 프레임워크 | SystemVerilog UVM, cocotb | OSVVM, VUnit, cocotb |
| 대소문자 구분 | 구분함 | 구분하지 않음 |
| 코드 재사용 단위 | module, interface, package | entity/architecture, package, component |
| 제네릭/파라미터 | parameter, #() 구문 | generic, generic map() 구문 |
동일 회로의 HDL 3종 비교
동일한 회로(인에이블 및 동기 리셋이 있는 8비트 카운터)를 Verilog, SystemVerilog, VHDL로 작성하여 문법 차이를 직접 비교합니다.
// Verilog — 8비트 카운터 (enable + sync reset)
module counter_v (
input clk,
input rst_n,
input en,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n)
if (!rst_n) count <= 8'd0;
else if (en) count <= count + 8'd1;
endmodule
// SystemVerilog — 동일 카운터
module counter_sv (
input logic clk,
input logic rst_n,
input logic en,
output logic [7:0] count
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) count <= '0; // '0 = 컨텍스트 폭 자동 결정
else if (en) count <= count + 1;
endmodule
-- VHDL — 동일 카운터
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity counter_vhdl is
port (
clk : in std_logic;
rst_n : in std_logic;
en : in std_logic;
count : out std_logic_vector(7 downto 0)
);
end entity;
architecture rtl of counter_vhdl is
signal cnt : unsigned(7 downto 0) := (others => '0');
begin
count <= std_logic_vector(cnt); -- 명시적 타입 변환 필수
process(clk, rst_n)
begin
if rst_n = '0' then
cnt <= (others => '0');
elsif rising_edge(clk) then
if en = '1' then
cnt <= cnt + 1;
end if;
end if;
end process;
end architecture;
핵심 차이점 요약
- 코드 분량: Verilog(8줄) ≈ SystemVerilog(7줄) < VHDL(18줄). VHDL은 entity/architecture 분리와 명시적 타입 선언으로 코드가 길어집니다
- 타입 처리: Verilog/SystemVerilog는
reg/logic에 직접 산술 연산이 가능하지만, VHDL은unsigned로 변환 후 연산하고std_logic_vector로 재변환해야 합니다 - 리셋 패턴: 세 언어 모두 비동기 리셋을 동일하게 기술하지만, VHDL은
rising_edge()함수를, Verilog/SystemVerilog는posedge키워드를 사용합니다 - 합성 결과: 세 코드 모두 동일한 하드웨어(8개의 D 플립플롭 + AND 게이트)로 합성됩니다. LUT/FF 사용량에 차이가 없습니다
차세대 HDL
Verilog/VHDL의 생산성 한계를 극복하기 위해, 고수준 프로그래밍 언어를 기반으로 한 차세대 HDL이 등장하고 있습니다. 이들은 최종적으로 Verilog/VHDL 코드를 생성하므로, 기존 합성 도구 체인과 호환됩니다.
| HDL | 기반 언어 | 개발 조직 | 특징 | 출력 |
|---|---|---|---|---|
| Chisel | Scala | UC Berkeley / SiFive | RISC-V 생태계의 사실상 표준, 함수형 프로그래밍으로 하드웨어 추상화, FIRRTL 중간 표현 | Verilog |
| SpinalHDL | Scala | 커뮤니티 | Chisel보다 전통 HDL에 가까운 문법, VexRiscv CPU로 검증, 강력한 CDC 검사 내장 | Verilog / VHDL |
| Amaranth | Python | 커뮤니티 (구 nMigen) | Python 생태계 활용, LiteX SoC 빌더와 통합, 오픈소스 FPGA 도구(Yosys/nextpnr)와 긴밀 연동 | Verilog (RTLIL) |
| Clash | Haskell | QBayLogic | 순수 함수형, 타입 시스템으로 하드웨어 오류를 컴파일 타임에 포착, 학술 연구에서 활용 | Verilog / VHDL |
| MyHDL | Python | 커뮤니티 | Python 코루틴 기반 시뮬레이션, 합성 가능 서브셋 지원 | Verilog / VHDL |
차세대 HDL은 파라미터화, 코드 생성, 타입 안전성에서 장점이 있지만, 디버깅 시 생성된 Verilog/VHDL 코드를 읽어야 하고, 벤더 도구의 에러 메시지가 원본 코드와 맞지 않을 수 있습니다. 대규모 프로젝트나 팀 환경에서는 도입 전 학습 비용과 생태계 성숙도를 고려해야 합니다.
Chisel — 4비트 카운터 예제
import chisel3._
import chisel3.util._
class Counter4 extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val count = Output(UInt(4.W))
})
val cnt = RegInit(0.U(4.W))
when (io.en) { cnt := cnt + 1.U }
io.count := cnt
}
// Verilog 생성: sbt "runMain Counter4Main"
object Counter4Main extends App {
emitVerilog(new Counter4)
}
SpinalHDL — 동일한 4비트 카운터
import spinal.core._
class Counter4 extends Component {
val io = new Bundle {
val en = in Bool()
val count = out UInt(4 bits)
}
val cnt = Reg(UInt(4 bits)) init(0)
when (io.en) { cnt := cnt + 1 }
io.count := cnt
}
// Verilog 생성
object Counter4Main extends App {
SpinalVerilog(new Counter4)
}
Amaranth — 동일한 4비트 카운터
from amaranth import *
from amaranth.back.verilog import convert
class Counter4(Elaboratable):
def __init__(self):
self.en = Signal()
self.count = Signal(4)
def elaborate(self, platform):
m = Module()
with m.If(self.en):
m.d.sync += self.count.eq(self.count + 1)
return m
# Verilog 생성
counter = Counter4()
with open("counter4.v", "w") as f:
f.write(convert(counter, ports=[counter.en, counter.count]))
각 도구 체인의 빌드 환경
- Chisel: JDK + sbt(Scala Build Tool) + FIRRTL 컴파일러를 사용합니다.
sbt run명령으로 FIRRTL 중간 표현을 거쳐 Verilog를 생성합니다. UC Berkeley의 Rocket Chip, SiFive의 Freedom SoC가 Chisel로 작성되었습니다 - SpinalHDL: JDK + sbt를 사용하며, Chisel과 동일한 Scala 생태계를 공유합니다.
SpinalVerilog()또는SpinalVhdl()로 직접 Verilog/VHDL을 생성합니다. VexRiscv(RISC-V 코어), SaxonSoc이 대표적인 프로젝트입니다 - Amaranth: Python 3.8+와 Yosys가 필요합니다.
pip install amaranth로 설치하며, Yosys의 RTLIL 중간 표현을 거쳐 Verilog를 생성합니다. LiteX SoC 빌더, Glasgow 디버그 프로브가 Amaranth 기반입니다
HDL 선택 가이드
프로젝트의 규모, 팀 역량, 산업 분야에 따라 적합한 HDL이 달라집니다. 다음 기준을 참고하여 HDL을 선택할 수 있습니다.
프로젝트 규모별 추천
- 소규모 (수천 LUT 이하): Verilog 또는 Amaranth가 적합합니다. 빠른 프로토타이핑이 가능하며, 학습 곡선이 낮습니다
- 중규모 (수만 LUT): SystemVerilog가 가장 균형 잡힌 선택입니다.
interface,struct,package로 코드를 구조화할 수 있으며, UVM 기반 검증이 가능합니다 - 대규모 SoC (수십만 LUT 이상): SystemVerilog + UVM 또는 Chisel/SpinalHDL이 권장됩니다. 파라미터화된 IP 생성, 자동화된 레지스터 맵 생성, 대규모 검증 환경이 필수적입니다
팀 역량별 추천
- 하드웨어 엔지니어 중심: Verilog/SystemVerilog/VHDL이 자연스럽습니다. 하드웨어 구조를 직접 제어할 수 있으며, 합성 결과를 예측하기 쉽습니다
- 소프트웨어 엔지니어 혼합 팀: Chisel(Scala), SpinalHDL(Scala), Amaranth(Python)이 진입 장벽을 낮춥니다. 객체 지향 패턴, 제네릭, 패키지 관리 등 익숙한 개발 패러다임을 활용할 수 있습니다
산업별 관행
| 산업 분야 | 주요 HDL | 배경 |
|---|---|---|
| 방위/항공 | VHDL | DO-254/MIL-STD 인증 요구, 강타입 안전성, 유럽 항공 산업 전통 |
| 소비자 가전/모바일 | SystemVerilog | 높은 생산성, UVM 검증 생태계, ASIC 설계 플로우 표준 |
| 네트워크/데이터센터 | SystemVerilog | 고성능 ASIC/FPGA, 대규모 IP 재사용, P4 연계 |
| 학술/연구 | Chisel / Amaranth | 빠른 아키텍처 탐색, RISC-V 생태계(Rocket/BOOM), 오픈소스 도구 체인 |
| 자동차 | VHDL / SystemVerilog | ISO 26262 기능 안전성, AUTOSAR 호환 IP |
| FPGA 프로토타이핑 | Verilog / SystemVerilog | 벤더 도구 호환성, IP 카탈로그 활용 |
어떤 HDL을 선택하든, 최종 합성 도구가 지원하는 언어로 변환되어야 합니다. 차세대 HDL을 사용하더라도 생성된 Verilog/VHDL 코드를 읽고 디버깅할 수 있는 역량은 필수적입니다.
RTL 코딩 스타일
RTL(Register Transfer Level) 코딩 스타일은 합성 가능한 하드웨어를 올바르게 기술하기 위한 코딩 규칙과 패턴의 모음입니다. 잘못된 코딩 스타일은 의도치 않은 래치 생성, 타이밍 실패, 시뮬레이션/합성 불일치 등의 문제를 초래합니다.
합성 가능 패턴 vs 시뮬레이션 전용 구문
HDL 코드는 크게 합성 가능(Synthesizable) 구문과 시뮬레이션 전용(Simulation-only) 구문으로 나뉩니다. 합성 도구는 시뮬레이션 전용 구문을 무시하거나 오류를 발생시킵니다.
- 합성 가능:
always_ff,always_comb,assign,if/else,case, 모듈 인스턴스화, 산술/논리 연산 - 시뮬레이션 전용:
#10(딜레이),initial블록(FPGA에서는 일부 지원),$display,$readmemh,fork/join,wait
initial 블록은 FPGA와 ASIC에서 동작이 다르므로 주의가 필요합니다. FPGA에서는 비트스트림 로드 시 레지스터 초기값으로 사용할 수 있지만, ASIC에서는 완전히 무시됩니다. real 타입은 부동소수점 연산으로 합성이 불가능하며, 아날로그 모델링에만 사용됩니다. fork/join은 병렬 스레드를 생성하는 시뮬레이션 전용 구문으로, UVM 테스트벤치에서 여러 시퀀스를 동시에 실행할 때 활용됩니다.
| 구문 | FPGA 합성 | ASIC 합성 | 시뮬레이션 | 비고 |
|---|---|---|---|---|
initial | 초기값으로 사용 | 무시 | 시작 시 실행 | Xilinx: INIT 속성으로 변환 |
real | 불가 | 불가 | 부동소수점 | 아날로그 모델링 전용 |
fork/join | 불가 | 불가 | 병렬 스레드 | UVM 테스트벤치 전용 |
#delay | 무시 | 무시 | 시간 지연 | 게이트 레벨 SDF 백어노테이션과 별개 |
$display | 무시 | 무시 | 텍스트 출력 | 합성 시 경고 없이 제거됨 |
레지스터 추론 패턴
always_ff 블록에서 클록 에지에 할당되는 모든 신호는 플립플롭(Flip-Flop)으로 합성됩니다. 합성 도구가 의도한 대로 레지스터를 추론하려면 일관된 코딩 패턴을 따라야 합니다.
- 리셋이 있는 레지스터:
if (!rst_n) q <= '0; else if (en) q <= d; - 리셋이 없는 레지스터:
if (en) q <= d;(FPGA에서는 초기값으로 대체 가능) - 시프트 레지스터:
{shift_reg[N-2:0], data_in}패턴이면 SRL(Shift Register LUT)로 자동 추론됩니다
SRL16/SRL32 추론 패턴: Xilinx FPGA의 LUT는 16비트 또는 32비트 시프트 레지스터(SRL16E/SRLC32E)로 동작할 수 있습니다. 합성 도구가 SRL을 추론하려면 리셋이 없는 시프트 레지스터 패턴이어야 합니다.
// SRL 추론 패턴 — 리셋 없는 시프트 레지스터
logic [15:0] delay_line;
always_ff @(posedge clk)
delay_line <= {delay_line[14:0], data_in};
assign data_out = delay_line[15]; // 16 클록 지연
// 주의: 리셋이 포함되면 SRL 추론이 불가능합니다
// 리셋이 필요하면 (* shreg_extract = "no" *) 속성을 사용하여
// 일반 FF 체인으로 합성되도록 유도합니다
카운터 캐리 체인: 넓은 카운터는 캐리 체인(Carry Chain)을 통해 효율적으로 합성됩니다. +1 패턴은 자동으로 CARRY4(7시리즈)/CARRY8(UltraScale) 프리미티브를 사용합니다.
BRAM 추론 조건: 메모리 배열이 BRAM으로 추론되려면 동기 읽기(클록 에지에서 읽기), 적절한 크기(일반적으로 4Kbit 이상), 단일 또는 이중 포트 패턴이 필요합니다. 비동기 읽기를 포함하면 분산 RAM(LUT RAM)으로 합성됩니다.
// BRAM 추론 — 동기 읽기 필수
logic [7:0] mem [0:1023]; // 8K bit → BRAM 추론 대상
always_ff @(posedge clk) begin
if (we) mem[addr] <= wdata;
rdata <= mem[addr]; // 동기 읽기 → BRAM
end
// 주의: assign rdata = mem[addr]; (비동기 읽기) → 분산 RAM
래치 방지
의도치 않은 래치(Latch)는 FPGA 설계에서 가장 흔한 실수 중 하나입니다. 래치는 always_comb 또는 always @(*) 블록에서 모든 조건 분기에서 출력이 할당되지 않을 때 생성됩니다.
if문에서else누락: 조건이 거짓일 때 이전 값을 유지해야 하므로 래치가 추론됩니다case문에서default누락: 명시되지 않은 case 값에서 래치가 추론됩니다- 방지 방법: 블록 시작 부분에서 모든 출력에 기본값을 할당하거나, SystemVerilog의
always_comb를 사용하면 합성 도구가 래치 추론 시 경고를 발생시킵니다 unique case/priority case(SystemVerilog): 조건의 완전성과 우선순위를 명시적으로 선언하여 래치와 우선순위 인코더 추론을 제어합니다
래치가 생성되는 코드 패턴과 수정
// [잘못된 코드] else 누락 → 래치 추론
always_comb begin
if (sel)
y = a;
// else 없음 → sel=0일 때 y는 이전 값 유지 → 래치
end
// [올바른 코드] 기본값 할당으로 래치 방지
always_comb begin
y = '0; // 기본값 — 모든 경로에서 할당 보장
if (sel)
y = a;
end
// [잘못된 코드] case에서 일부 출력 미할당 → 래치
always_comb begin
case (state)
IDLE: begin ready = 1'b1; valid = 1'b0; end
ACTIVE: begin ready = 1'b0; /* valid 미할당 → 래치 */ end
default: ; // ready, valid 모두 미할당 → 래치
endcase
end
Vivado에서 래치가 추론되면 다음과 같은 경고 메시지가 발생합니다.
WARNING: [Synth 8-327] inferring latch for variable 'y_reg' [source.sv:42]
SystemVerilog의 always_comb는 Verilog의 always @(*)와 달리, 합성 도구가 래치 추론을 감지하면 오류 수준 진단을 발생시킵니다. 따라서 새 설계에서는 반드시 always_comb를 사용하는 것을 권장합니다.
Ready-Valid 핸드셰이크 패턴
Ready-Valid(또는 Valid-Ready) 프로토콜은 디지털 설계에서 가장 기본적인 흐름 제어(Flow Control) 메커니즘입니다. 송신 측이 valid를 어서트하여 데이터가 준비되었음을 알리고, 수신 측이 ready를 어서트하여 데이터를 받을 수 있음을 알립니다. 두 신호가 동시에 하이(high)인 클록 에지에서 전송이 성립합니다. AMBA AXI, AXI-Stream 등 대부분의 온칩 프로토콜이 이 패턴을 기반으로 합니다.
위 다이어그램에서 녹색 영역은 valid && ready가 모두 하이인 사이클로, 데이터 전송이 성립합니다. 빨간 영역은 valid는 하이지만 ready가 로우인 스톨(Stall) 구간으로, 송신 측은 데이터를 유지해야 합니다.
스키드 버퍼(Skid Buffer) — 파이프라인 레지스터 슬라이스
스키드 버퍼는 Ready-Valid 채널에 삽입하는 1단계 파이프라인 레지스터입니다. 수신 측이 ready를 내리는 순간 이미 받아들인 데이터를 내부 버퍼에 보관하여 데이터 손실을 방지합니다. AXI 인터커넥트의 레지스터 슬라이스가 대표적인 응용입니다.
// 스키드 버퍼 — Ready-Valid 파이프라인 레지스터 슬라이스
module skid_buffer #(
parameter int WIDTH = 8
)(
input logic clk,
input logic rst_n,
// 업스트림 (입력)
input logic i_valid,
output logic i_ready,
input logic [WIDTH-1:0] i_data,
// 다운스트림 (출력)
output logic o_valid,
input logic o_ready,
output logic [WIDTH-1:0] o_data
);
logic buf_valid;
logic [WIDTH-1:0] buf_data;
// 업스트림 ready: 버퍼가 비어 있으면 항상 수용 가능
assign i_ready = !buf_valid;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
buf_valid <= 1'b0;
buf_data <= '0;
end else if (i_valid && i_ready && o_valid && !o_ready) begin
// 입력 수신 + 출력 스톨 → 버퍼에 저장
buf_valid <= 1'b1;
buf_data <= i_data;
end else if (o_ready) begin
// 다운스트림이 소비 → 버퍼 비움
buf_valid <= 1'b0;
end
// 출력 MUX: 버퍼에 데이터가 있으면 버퍼 우선
assign o_valid = buf_valid || i_valid;
assign o_data = buf_valid ? buf_data : i_data;
endmodule
코드 설명
i_ready = !buf_valid: 내부 버퍼가 비어 있으면 항상 입력을 수용합니다. 버퍼가 차 있으면 입력을 거부하여 데이터 손실을 방지합니다- 버퍼 저장 조건: 입력이 들어오는 동시에 출력이 스톨되면(
!o_ready), 입력 데이터를 내부 레지스터에 저장합니다. 이것이 "스키드(미끄러짐)"라는 이름의 유래입니다 - 출력 MUX: 버퍼에 데이터가 있으면 버퍼 데이터를 출력하고, 없으면 입력을 직접 통과(combinational path)시킵니다
- 사용 사례: AXI 인터커넥트의 레지스터 슬라이스, 파이프라인 스테이지 간 백프레셔 전파, 모듈 간 인터페이스 타이밍 완화에 활용됩니다
이 페이지의 AXI-Lite 레지스터 인터페이스 예제도 Ready-Valid 핸드셰이크를 기반으로 동작합니다. AXI4의 5개 채널(AW, W, B, AR, R)은 각각 독립된 Ready-Valid 채널이며, 형식 검증 섹션의 AXI 핸드셰이크 검증기도 이 프로토콜의 속성을 검증합니다.
클록 도메인 교차(CDC) 코딩 규칙
서로 다른 클록 도메인 간 데이터 전송은 메타스테이빌리티(Metastability)를 유발할 수 있습니다. 모든 CDC 설계의 기본 빌딩 블록은 2-FF 동기화기입니다. 이 패턴은 1비트 신호를 안전하게 교차시키며, FPGA 합성 시 ASYNC_REG 속성을 반드시 적용해야 합니다.
// 2-FF 동기화기 — 1비트 CDC의 최소 패턴
module sync_2ff #(
parameter int STAGES = 2 // 2 이상 (고속 클록에서는 3 권장)
)(
input logic clk_dst, // 수신 클록
input logic rst_dst_n,
input logic async_in, // 다른 클록 도메인의 신호
output logic sync_out
);
(* ASYNC_REG = "TRUE" *) logic [STAGES-1:0] shreg;
always_ff @(posedge clk_dst or negedge rst_dst_n)
if (!rst_dst_n) shreg <= '0;
else shreg <= {shreg[STAGES-2:0], async_in};
assign sync_out = shreg[STAGES-1];
endmodule
그레이 코드 포인터 — 멀티비트 CDC의 핵심
멀티비트 신호를 그대로 다른 클록 도메인으로 전달하면, 각 비트의 전파 지연 차이로 잘못된 값이 샘플링됩니다. 비동기 FIFO의 읽기/쓰기 포인터 교환에는 그레이 코드(Gray Code)를 사용합니다. 그레이 코드는 인접한 값 사이에서 단 1비트만 변하므로, 2-FF 동기화기로 안전하게 전달할 수 있습니다.
// 바이너리 → 그레이 코드 변환
function automatic logic [N-1:0] bin2gray(logic [N-1:0] bin);
return bin ^ (bin >> 1);
endfunction
// 그레이 코드 → 바이너리 변환
function automatic logic [N-1:0] gray2bin(logic [N-1:0] gray);
logic [N-1:0] bin;
bin[N-1] = gray[N-1];
for (int i = N-2; i >= 0; i--)
bin[i] = bin[i+1] ^ gray[i];
return bin;
endfunction
리셋 전략
FPGA 설계에서 리셋은 동기 리셋, 비동기 리셋, 비동기 어서트/동기 디어서트의 세 가지 방식으로 구분됩니다. FPGA 벤더에 따라 권장 방식이 다르며, 프로젝트에서 하나의 방식을 선택하고 일관되게 적용하는 것이 중요합니다.
// (1) 동기 리셋 — Xilinx 권장 (FDRE 프리미티브 효율적)
always_ff @(posedge clk)
if (!rst_n) q <= '0;
else q <= d;
// (2) 비동기 리셋 — Intel 권장 (ALM aclr 효율적)
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) q <= '0;
else q <= d;
// (3) 비동기 어서트 + 동기 디어서트 — 가장 안전한 패턴
// 리셋 동기화기 (별도 모듈로 분리 권장)
logic rst_sync_n;
logic [1:0] rst_pipe;
always_ff @(posedge clk or negedge arst_n)
if (!arst_n) rst_pipe <= 2'b00;
else rst_pipe <= {rst_pipe[0], 1'b1};
assign rst_sync_n = rst_pipe[1];
// 이후 모든 로직에서 rst_sync_n을 동기 리셋으로 사용
always_ff @(posedge clk)
if (!rst_sync_n) q <= '0;
else q <= d;
| 패턴 | 감도 리스트 | Xilinx 리소스 | Intel 리소스 | 특징 |
|---|---|---|---|---|
| 동기 리셋 | @(posedge clk) | FDRE (최적) | ALM sclr (LUT 소모) | 타이밍 분석 용이, 글리치 면역 |
| 비동기 리셋 | @(posedge clk or negedge rst_n) | FDCE (LUT 소모) | ALM aclr (최적) | 즉시 리셋, 리커버리/리무벌 타이밍 필요 |
| 비동기 어서트 + 동기 디어서트 | 동기화기 + 동기 리셋 | 2-FF + FDRE | 2-FF + ALM | 가장 안전, 멀티 클록 도메인 환경 권장 |
명명 규칙
일관된 명명 규칙(Naming Convention)은 코드 가독성과 유지보수성을 크게 향상시킵니다. 팀 내에서 하나의 규칙을 선택하고 일관되게 적용하는 것이 중요합니다.
신호 명명 규칙
| 접두사/접미사 | 의미 | 예시 |
|---|---|---|
i_ / _i | 입력 포트 | i_data 또는 data_i |
o_ / _o | 출력 포트 | o_valid 또는 valid_o |
r_ | 레지스터 출력 | r_state, r_counter |
w_ | 와이어/조합 논리 출력 | w_next_state, w_sum |
_n | 액티브 로우(active low) 신호 | rst_n, cs_n |
_d / _q | 플립플롭 입력(D) / 출력(Q) | data_d, data_q |
_ff | 동기화 플립플롭 | async_in_ff1, async_in_ff2 |
클록/리셋 명명
- 클록:
clk(단일 도메인),clk_100m,clk_axi,clk_pixel(다중 도메인) - 리셋:
rst_n(액티브 로우),rst(액티브 하이),arst_n(비동기 리셋) - 클록 인에이블:
clk_en,ce
모듈 명명
- 모듈: snake_case를 사용하며, 기능을 명확히 기술합니다. 예:
uart_rx,axi_lite_slave,sync_fifo - 테스트벤치:
tb_접두사를 붙입니다. 예:tb_uart_rx - 패키지:
_pkg접미사를 붙입니다. 예:axi_pkg,types_pkg
린팅 도구
린팅(Linting) 도구는 합성 전에 코딩 규칙 위반, 잠재적 버그, 스타일 불일치를 자동으로 검출합니다. CI/CD 파이프라인에 통합하면 코드 품질을 지속적으로 유지할 수 있습니다.
| 도구 | 라이선스 | 대상 언어 | 특징 |
|---|---|---|---|
| Verible | Apache 2.0 (Google) | SystemVerilog | 린터 + 포매터 + 언어 서버(LSP), CI 통합 용이 |
| Slang | MIT | SystemVerilog | IEEE 1800-2017 완전 구현 파서, 진단 메시지 우수 |
| vsg | GPL (오픈소스) | VHDL | VHDL Style Guide, 규칙 커스터마이즈 가능 |
| Spyglass CDC | 상용 (Synopsys) | SV / VHDL | CDC 검증 특화, 산업 표준 |
| Ascent Lint | 상용 (Real Intent) | SV / VHDL | 합성 전 정적 분석, 100+ 규칙 |
# Verible 린터 실행
verible-verilog-lint --rules_config .rules.verible top.sv
verible-verilog-lint --generate_markdown > lint_rules.md # 규칙 목록
# Verible 포매터 — 코드 스타일 자동 정리
verible-verilog-format --inplace top.sv
# vsg (VHDL Style Guide) 실행
vsg -f top.vhd --configuration vsg_config.yaml
vsg -f top.vhd --fix # 자동 수정
Verible 린트 출력 예제
다음은 Verible 린터가 검출하는 대표적인 위반 사례입니다. 각 진단 메시지는 [규칙 이름]을 포함하며, .rules.verible 파일에서 개별적으로 활성화/비활성화할 수 있습니다.
$ verible-verilog-lint --rules_config .rules.verible top.sv
top.sv:15:5: Use 'always_ff' instead of 'always' for sequential logic. [always-ff-non-blocking]
top.sv:23:9: 'case' statement is missing 'default' branch. [case-missing-default]
top.sv:31:5: Blocking assignment '=' used in 'always_ff' block; use '<='. [blocking-assignment-in-always-ff]
top.sv:1:8: Module name 'MyCounter' should be lower_snake_case. [module-filename]
top.sv:12:1: Line length exceeds 120 characters (found 138). [line-length]
# .rules.verible — 규칙 설정 파일
-line-length=length:120
+always-ff-non-blocking
+case-missing-default
+blocking-assignment-in-always-ff
-unpacked-dimensions-range-ordering
코드 리뷰 체크리스트
always_ff에서 논블로킹 할당(<=)만 사용하는지 확인합니다always_comb에서 블로킹 할당(=)만 사용하는지 확인합니다- 모든
case/if분기에서 출력이 할당되어 래치가 추론되지 않는지 확인합니다 - CDC 경로에 2-FF 동기화기 또는 핸드셰이크가 적용되었는지 확인합니다
- 리셋 후 모든 출력 포트가 안전한 초기값을 가지는지 확인합니다
- 파라미터에 적절한 기본값과 범위 검증(
$error기반 generate-time check)이 있는지 확인합니다 - 합성 지시문(
(* keep *),(* ASYNC_REG *))이 적절히 적용되었는지 확인합니다 - 모듈/신호 명명이 팀 규칙을 따르는지 확인합니다
참고자료
IEEE 표준
- IEEE 1364-2005 — Verilog HDL. 현재는 IEEE 1800으로 통합되었으나, 레거시 프로젝트에서 여전히 참조됩니다
- IEEE 1800-2017 — SystemVerilog. 설계와 검증을 통합한 현행 표준입니다. UVM(Universal Verification Methodology)의 기반 언어입니다
- IEEE 1076-2019 — VHDL. 2008 버전에서 도입된 기능을 보완하고, 보호(protected) 타입, 인터페이스 등을 개선한 최신 표준입니다
- IEEE 1685-2014 — IP-XACT. IP 블록의 메타데이터(포트, 레지스터 맵, 버스 인터페이스)를 XML로 기술하는 표준입니다
참고서적
- RTL Hardware Design Using VHDL (Pong P. Chu) — VHDL RTL 코딩의 고전적인 교과서입니다
- SystemVerilog for Verification (Chris Spear, Greg Tumbush) — SystemVerilog 검증 기법을 체계적으로 다룹니다
- FPGA Prototyping by SystemVerilog Examples (Pong P. Chu) — SystemVerilog를 활용한 FPGA 프로토타이핑 실습서입니다
- Digital Design and Computer Architecture (Harris & Harris) — 디지털 설계와 컴퓨터 아키텍처를 HDL로 실습하며 배우는 교재입니다
오픈소스 학습 자료
- HDLBits — Verilog/SystemVerilog 온라인 연습 문제 모음입니다. 브라우저에서 바로 코드를 작성하고 시뮬레이션할 수 있습니다
- ASIC World — Verilog/VHDL/SystemVerilog 튜토리얼과 예제 코드를 제공합니다
- FPGA4Fun — FPGA 프로젝트 기반의 실습 튜토리얼입니다. UART, VGA, 오디오 등 실용적 예제가 풍부합니다
- Nandland — FPGA 입문자를 위한 Verilog/VHDL 튜토리얼과 개발 보드 프로젝트를 제공합니다
- ZipCPU Blog — 형식 검증, AXI 프로토콜, FPGA 설계 기법에 관한 심도 있는 블로그입니다