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 코딩 스타일을 종합적으로 정리합니다.

전제 조건: 디지털 논리회로 문서의 조합 논리, 순차 논리, 플립플롭 개념을 먼저 이해하세요. HDL은 이러한 디지털 회로를 텍스트로 기술하는 언어입니다.
일상 비유: HDL은 건축 설계도와 비슷합니다. 소프트웨어가 "이렇게 실행하라"는 절차서라면, HDL은 "이런 구조의 회로를 만들어라"는 설계 도면입니다. 한 장의 도면에 있는 모든 방은 동시에 존재하듯, HDL로 기술한 모든 회로 블록은 병렬로 동작합니다.

핵심 요약

  • Verilog/SystemVerilog — C 계열 문법의 HDL로, 미국·아시아 산업계에서 주류입니다. SystemVerilog(IEEE 1800)는 검증 기능과 객체 지향 프로그래밍을 추가한 확장입니다
  • VHDL — Ada 계열 문법의 HDL로, 유럽·방산·항공우주 분야에서 주류입니다. 강한 타입 시스템으로 컴파일 시점 오류 검출이 우수합니다
  • RTL (Register Transfer Level) — 합성 가능한 수준의 하드웨어 기술 방법으로, 레지스터 간 데이터 흐름과 조합 논리를 기술합니다
  • 합성 (Synthesis) — HDL 코드를 실제 게이트 레벨 넷리스트로 변환하는 과정입니다. 시뮬레이션 전용 구문은 합성되지 않습니다

단계별 이해

  1. 1단계 — 기본 문법: 모듈(module/entity) 구조, 포트 선언, 와이어/레지스터 타입을 이해합니다
  2. 2단계 — 조합/순차 논리: always_comb/always_ff (Verilog) 또는 process (VHDL)로 조합 논리와 플립플롭을 기술합니다
  3. 3단계 — FSM 패턴: 상태 머신 코딩 스타일(3-프로세스, 2-프로세스)을 익힙니다
  4. 4단계 — 파라미터화와 재사용: parameter/generic, generate로 범용 모듈을 설계합니다
  5. 5단계 — 검증 기법: 테스트벤치, SVA 어서션, constrained random, 기능 커버리지를 활용합니다
  6. 6단계 — RTL 코딩 규칙: 래치 방지, 리셋 전략, CDC 규칙 등 합성 품질을 높이는 패턴을 따릅니다

처음 읽는 RTL의 해석 순서

HDL 입문에서 가장 중요한 습관은 코드를 위에서 아래로 "실행"한다고 생각하지 않는 것입니다. RTL(Register Transfer Level)을 읽을 때는 문장 순서가 아니라 회로 역할 순서로 읽어야 합니다. 가장 안전한 읽기 순서는 다음과 같습니다.

  1. 모듈 경계 — 입력과 출력 포트를 보고, 이 블록이 시스템에서 무엇을 받아 무엇을 내보내는지 먼저 파악합니다
  2. 클록·리셋 — 어떤 신호가 상태를 갱신하는지 확인합니다. always_ff 또는 process(clk)가 보이면 순차 논리가 있는 뜻입니다
  3. 조합 논리assign, always_comb, process(all) 블록이 현재 입력으로 무엇을 계산하는지 봅니다
  4. 상태 변수count, state, busy처럼 다음 사이클로 넘어가는 값이 무엇인지 확인합니다
  5. 인터페이스 규칙 — 유효(valid), 준비(ready), 칩 선택(chip select), 리셋 극성처럼 블록 외부와의 약속을 마지막에 정리합니다
1. 포트 입력은 무엇입니까? 출력은 무엇입니까? clk, rst_n, data_in, valid 2. 조합 논리 지금 입력으로 무엇을 계산합니까? next_count = count + 1 irq = valid & ready 3. 레지스터 클록 에지에서 무엇을 저장합니까? count 레지스터 4. 출력과 규칙 외부 블록이 이 값을 어떻게 해석합니까? valid/ready 약속 리셋 직후 초기값 현재 상태가 다시 조합 논리의 입력이 됩니다

입문 단계에서는 이 흐름만 정확히 지켜도 코드를 훨씬 덜 두려워하게 됩니다. 특히 조합 논리와 순차 논리를 분리해서 읽는 습관은 래치 생성, 블로킹/논블로킹 할당 혼동, 리셋 누락 같은 초보자 실수를 줄이는 데 매우 효과적입니다.

가장 작은 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_countcount를 분리해서 보는 것입니다. next_count는 "이번 사이클에 계산한 후보값"이고, count는 "실제로 저장되어 다음 사이클까지 유지되는 상태"입니다. 입문자가 가장 빨리 성장하는 지점이 바로 이 차이를 몸에 익히는 순간입니다.

입문 단계에서 자주 하는 실수

실수왜 발생합니까?교정 방법
조합 논리에서 일부 분기에만 값을 할당합니다소프트웨어의 if 문처럼 생각하기 때문입니다기본값을 먼저 두고, 모든 분기에서 값이 정해지도록 작성합니다
=<=를 섞어 씁니다하드웨어와 시뮬레이션 스케줄 차이를 체감하지 못했기 때문입니다조합 논리는 블로킹, 순차 논리는 논블로킹이라는 기본 규칙을 먼저 고정합니다
리셋이 필요한 상태와 불필요한 상태를 구분하지 못합니다모든 레지스터를 무조건 리셋하려는 습관 때문입니다관측 가능 상태, 프로토콜 초기값, 디버그 필요성을 기준으로 리셋 대상을 정합니다
테스트벤치 없이 코드를 합성합니다CPU 프로그램처럼 "일단 돌려 보면 됩니다"라고 생각하기 때문입니다작은 모듈이라도 최소한의 자체 검증 테스트벤치를 먼저 붙입니다

하드웨어와 소프트웨어의 차이

HDL을 배우는 소프트웨어 개발자가 가장 먼저 넘어야 할 벽은 사고 방식의 전환입니다. 프로그래밍 언어는 CPU가 명령을 하나씩 실행하는 절차를 기술하지만, HDL은 동시에 존재하는 회로 구조를 기술합니다.

순차 실행 vs 병렬 실행

소프트웨어에서 for 문으로 1,000번 반복하면 CPU는 1,000 사이클(또는 그 이상)을 소모합니다. 반면 하드웨어에서 1,000개의 AND 게이트를 배치하면, 모든 게이트가 동시에 1 사이클 만에 결과를 출력합니다.

// 소프트웨어 관점: 순차 실행 (의사 코드)
// 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을 주기적으로 반복하며, 이 주기를 기준으로 레지스터가 데이터를 저장합니다.

clk posedge negedge posedge T (주기) = 1/f D (입력) 이전 값 새 값 Tsetup Thold Q (출력) 이전 값 유지 새 값 (posedge에서 캡처) ← Tco (클록→출력 지연) 셋업 타임: 데이터가 에지 전에 안정되어야 하는 시간 홀드 타임: 에지 후에도 유지해야 하는 시간

셋업 타임(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 디버깅의 기본 도구로, 각 신호의 시간에 따른 변화를 시각적으로 확인할 수 있습니다.

4비트 카운터 시뮬레이션 파형 0ns 20ns 40ns 60ns 80ns 100ns clk rst_n 0 (리셋) 1 (동작) count [3:0] 0000 0000 0001 0010 0011 0100 0101 0110 0111 posedge clk 파형은 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
권장 사항: SystemVerilog의 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로 전파됩니다.

always_ff #1 data_out <= write_data always_ff #2 data_out <= '0 X (충돌) 불확정 상태 해결: 단일 always + if/else MUX 우선순위를 명시적으로 결정

의도하지 않은 래치

조합 논리에서 모든 조건 경로에 출력을 할당하지 않으면, 합성 도구가 이전 값을 유지하기 위해 래치(Latch)를 추론합니다. 래치는 타이밍 분석을 어렵게 만들고, 대부분의 FPGA/ASIC 설계에서 의도하지 않은 버그의 원인이 됩니다.

if (sel) y = a; (else 없음) sel=0일 때 y는? 이전 값 유지 → 래치 해결 else y = 0; 또는 블록 시작에 기본값 할당 합성 경고: "WARNING: [Synth 8-327] inferring latch for variable 'y'" case 문에서 default를 생략해도 동일한 문제가 발생합니다
// 문제: 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를 기본 언어로 사용합니다.

input ports clk rst_n data_in enable wire (input) 조합 논리 always_comb / assign wire 레지스터 always_ff reg / logic output ports data_out valid ready wire / reg module my_module (...) wire vs reg wire — 연속 할당 reg — 절차 할당 logic — SV 통합 SystemVerilog의 logic 타입은 wire와 reg를 통합하여 컨텍스트에 따라 결정됩니다

모듈 구조

모듈은 레고 블록과 같습니다. 각 블록에는 연결 지점(포트)이 있고, 내부에 특정 기능의 회로를 담고 있습니다. 작은 모듈들을 조합하여 복잡한 시스템을 만들 수 있습니다. 소프트웨어의 함수(function)와 비슷하지만, 각 모듈은 물리적인 회로 블록으로 합성되어 동시에 동작합니다.

Verilog의 기본 설계 단위는 모듈(module)입니다. 모듈은 입출력 포트를 가지며, 내부에 회로 동작을 기술합니다.

wire전선(직접 연결)과 같아서 입력이 바뀌면 출력이 즉시 반영됩니다. reg메모장(값을 기억)과 같아서 클록 에지에서만 새 값을 기록합니다. SystemVerilog의 logic은 만능 타입으로, 컴파일러가 사용 맥락에 따라 자동으로 wire 또는 reg로 결정합니다.

사용 위치wirereglogic (SystemVerilog)
assign 문의 대상
always 블록의 대상
모듈 입력 포트✓ (기본)
모듈 출력 포트조합 논리 시순차 논리 시
실무 팁: SystemVerilog 프로젝트에서는 logic만 사용하면 됩니다. wire/reg 구분은 Verilog-2001 호환이 필요한 경우에만 고려하세요.

모듈 인스턴스화 — 구조적 설계

레고 블록을 조립하듯, 작은 모듈을 큰 모듈 안에 인스턴스화(Instantiation)하여 복잡한 시스템을 구성합니다. 이를 구조적 설계(Structural Design)라 합니다. 지금까지의 예제는 모두 행위(Behavioral) 기술이었지만, 실제 프로젝트에서는 두 방식을 혼합하여 사용합니다.

full_adder (상위 모듈) a → b → cin → half_adder u0 a ← b ← → sum (w_s0) → carry (w_c0) w_s0 half_adder u1 a ← b ← → sum → carry (w_c1) → sum w_c0 | w_c1 → cout
// 하위 모듈: 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 인스턴스는 독립된 하드웨어 블록으로 합성되어 동시에 동작합니다
wire (조합 논리) vs reg (순차 논리) 타이밍 비교 clk input_a wire_y 즉시 반응 input_d reg_q posedge clk에서 반영 조합 논리 (wire) 순차 논리 (reg)

조합 논리와 순차 논리

디지털 회로는 크게 조합 논리(Combinational Logic)순차 논리(Sequential Logic)로 나뉩니다. 이 구분은 HDL 코딩에서 가장 중요한 개념입니다.

always_comb 조합 논리 (Combinational) MUX / 게이트 assign 문 입력 변화 → 즉시 출력 갱신 클록 불필요 블로킹 할당 (=) 래치 추론 시 경고 always_ff 순차 논리 (Sequential) D 플립플롭 레지스터 배열 클록 에지에서만 출력 갱신 posedge clk 필수 논블로킹 할당 (<=) FF/BRAM 추론 always_latch 래치 (보통 비의도적) 레벨 감지 래치 투명 래치 enable HIGH 시 입력 통과 else 누락 시 추론됨 타이밍 분석 어려움 FPGA에서 권장하지 않음

조합 논리 vs 순차 논리 — 타이밍 동작 차이

위 다이어그램은 회로 구조를 보여주지만, 실제 신호가 어떻게 전파되는지는 타이밍 파형으로 이해하는 것이 직관적입니다. 조합 논리는 입력 변화에 즉시 반응하고, 순차 논리는 다음 클록 에지에서 출력이 갱신됩니다.

clk 입력(sel) 0 1 입력 변화 조합 출력 ↑ 즉시 반응 (Tpd 후) 순차 출력 ↑ 다음 posedge에서 갱신

always_comb는 SystemVerilog에서 always @(*)를 대체하는 권장 구문입니다. 차이점은 (1) 감도 리스트를 자동으로 구성하여 누락 실수를 방지하고, (2) 래치가 추론되면 합성 도구가 오류를 발생시킵니다(always @(*)는 경고만 발생). 새 설계에서는 반드시 always_comb를 사용하는 것을 권장합니다.

블로킹과 논블로킹 할당

Verilog에서 가장 흔한 실수 중 하나는 할당 연산자의 잘못된 사용입니다. 두 종류의 할당 연산자는 시뮬레이션 동작과 합성 결과에 직접적인 영향을 미칩니다.

이 규칙을 지키지 않으면 시뮬레이션과 합성 결과가 달라지는 심각한 버그가 발생할 수 있습니다. 순차 논리 블록에서 블로킹 할당을 사용하면, 레지스터 간 데이터 전달 순서가 시뮬레이터의 이벤트 스케줄링에 의존하게 되어 비결정적 동작이 나타납니다.

이 차이를 가장 명확히 보여주는 예제가 시프트 레지스터입니다. 블로킹 할당(=)을 사용하면 모든 레지스터가 같은 값으로 덮어쓰여지고, 논블로킹 할당(<=)을 사용해야 정상적으로 데이터가 시프트됩니다.

// 잘못된 예: 블로킹 할당으로 시프트 레지스터 (동작 오류)
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 논블로킹

블로킹 (=) — 잘못된 동작 T1 T2 T3 T4 din A B stg1 A B stg2 A B stg3 A B 논블로킹 (<=) — 올바른 동작 T1 T2 T3 T4 din A B stg1 A B stg2 A B stg3 A 모든 stage가 같은 사이클에 같은 값! → 시프트가 아닌 복사(broadcast) 각 stage가 1클록씩 순차 지연 → 정상적인 데이터 시프트
황금 규칙: 조합 논리(always_comb)에서는 블로킹 할당(=)을, 순차 논리(always_ff)에서는 논블로킹 할당(<=)을 사용합니다. 하나의 always 블록에서 두 종류의 할당을 절대 섞지 않습니다.

파라미터화

재사용 가능한 IP를 만들기 위해서는 모듈을 파라미터화(Parameterization)해야 합니다.

parameterlocalparam의 핵심 차이는 외부 오버라이드 가능 여부입니다. 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 a 1 b 0 sel y 진리표 sel y 0 b 1 a

두 코드는 동일한 하드웨어(MUX)로 합성됩니다. assign은 단순한 표현에, always_comb는 복잡한 조합 논리에 적합합니다. 4:1 MUX는 2비트 selcase문으로 확장할 수 있으며, 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 쪽)가 활성화된 위치를 이진 코드로 출력합니다. 인터럽트 컨트롤러에서 여러 인터럽트 요청 중 가장 높은 우선순위를 선택하거나, 아비터에서 여러 요청자 중 하나를 선택할 때 핵심적으로 사용됩니다.

req[7:0] 예: 8'b0010_1100 Priority Encoder MSB 우선 — casez 구현 입력 0이면 valid=0 idx[2:0] → 3'd5 (bit[5]) valid → 1 (요청 있음)
// 파라미터화 우선순위 인코더
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 펄스 동기화 등 거의 모든 디지털 설계에서 활용됩니다.

signal signal_d ← 1클록 지연 pos_edge sig & ~sig_d neg_edge ~sig & sig_d
// 에지 검출기 — 상승/하강 에지 펄스 생성
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 점멸 타이머, 타이머 프리스케일러 등 클록 주파수 변환이 필요한 곳에 사용됩니다.

clk_in counter 0 1 2 3 0 1 2 3 0 1 clk_out T_out = 4 × T_in (4분주)
// 짝수 분주기 — 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가 됩니다.

counter duty 75% pwm_out counter < duty → HIGH
// 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 계산에 활용됩니다.

bit[3] MSB bit[2] bit[1] bit[0] LSB out 다항식: x⁴ + x³ + 1 (탭: bit[3], bit[2])
// 갈루아(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
비트 수최대 길이 다항식 (탭)주기
4x⁴ + x³ + 115
8x⁸ + x⁶ + x⁵ + x⁴ + 1255
16x¹⁶ + x¹⁵ + x¹³ + x⁴ + 165,535
32x³² + x²² + x² + x + 14,294,967,295
주의: LFSR은 초기값(시드)이 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으로 구현합니다.

IDLE cnt = 0 DEBOUNCE cnt++ btn_in 변화 감지 cnt == MAX → btn_out 갱신 cnt < MAX 변화 없음
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
다음 단계: 시뮬레이션과 테스트벤치 방법론 섹션에서는 참조 모델(Reference Model)과 스코어보드(Scoreboard) 패턴, 파일 기반 테스트 벡터, 커버리지 수집 등 더 체계적인 검증 기법을 다룹니다.

SystemVerilog 확장

SystemVerilog(IEEE 1800)는 Verilog를 기반으로 설계와 검증 양쪽 모두를 강화한 언어입니다. 주요 확장 기능은 다음과 같습니다.

logic 타입은 Verilog의 wirereg 구분을 없앱니다. 기존에는 연속 할당(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);

packedunpacked 배열의 차이도 중요합니다. packed 배열(logic [7:0][3:0] data)은 연속된 비트 벡터로 합성되어 비트 슬라이싱이 가능합니다. unpacked 배열(logic [7:0] mem [0:15])은 독립적인 요소의 배열로, 메모리(BRAM)로 추론됩니다.

시뮬레이션 전용 타입으로는 string(문자열), 큐(int q[$]), 연관 배열(int aa[string])이 있습니다. 이들은 합성되지 않으며 테스트벤치에서만 사용합니다.

unique casepriority 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_rvalids_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)으로 나뉩니다.

주요 시퀀스 연산자

입문 예제 — 카운터 어서션

어서션을 처음 접한다면, 복잡한 프로토콜보다 간단한 카운터 속성부터 시작하는 것이 효과적입니다. 다음 예제는 이 페이지의 인에이블 카운터에 대해 기본적인 안전성 속성을 검증합니다.

// 카운터 기본 어서션 — 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) 시스템이 특징입니다.

library ieee, work, 사용자 정의 use ieee.std_logic_1164.all package 타입, 상수, 함수 선언 package body 구현부 entity (외부 인터페이스) port ( clk, rst_n : in std_logic; generic ( WIDTH : positive := 8; use work.pkg.all architecture rtl of entity_name is signal 선언 내부 연결선 process 조합/순차 로직 component 하위 모듈 인스턴스 generate 반복/조건부 구조 하나의 entity에 복수 architecture 가능 behavioral — 동작 수준 기술 rtl — 레지스터 전송 수준 structural — 구조적 연결

Entity/Architecture 구조

Entity는 전자부품의 데이터시트(핀 배치표)에 해당하고, Architecture는 내부 회로도에 해당합니다. 같은 핀 배치(Entity)로 시뮬레이션용 동작 모델과 합성용 RTL 모델을 따로 만들 수 있어서, 설계의 추상화 수준을 분리할 수 있습니다.

VHDL의 기본 설계 단위는 엔티티(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이 즉시 반영되지 않는 이유는 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은 할당 즉시 새 값이 반영됩니다.

시뮬레이션 시간 Δ0 Δ1 Δ2 (안정) Signal 사용 시 Variable 사용 시 sig_b 5 sig_a 0 ← a <= b+1 (예약) 6 ← Δ1에서 반영 sig_c 0 ← c <= a (이전 값=0!) 0 ← a의 이전 값 반영 sig_b 5 v_temp 6 ← v := b+1 (즉시!) sig_c 0 ← c <= v (6!) 6 ← 의도대로!

VHDL의 강한 타입 시스템

C 언어에서 intfloat에 대입하면 묵시적으로 변환되지만, VHDL에서는 컴파일 에러가 발생합니다. 번거롭지만, 하드웨어에서 8비트 신호를 16비트 포트에 연결하는 실수를 설계 시점에 잡아줍니다. 실리콘에서 버그를 발견하는 비용은 RTL에서 발견하는 비용의 1000배 이상이므로, 이 엄격함은 큰 가치가 있습니다.

VHDL은 Verilog에 비해 훨씬 엄격한 타입 시스템을 가지고 있습니다. 이는 타입 불일치 오류를 컴파일 시점에 잡아내어 설계 안전성을 높이지만, 코드가 다소 장황해지는 단점이 있습니다.

타입 간 변환이 필요할 때는 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)에 따라 조합 논리 또는 순차 논리를 기술합니다.

감도 리스트에서 신호를 누락하면, 합성 결과와 시뮬레이션 동작이 달라집니다. 합성 도구는 감도 리스트와 무관하게 로직을 추론하지만, 시뮬레이터는 감도 리스트에 있는 신호가 변할 때만 프로세스를 실행합니다.

-- 감도 리스트 누락 문제
-- 잘못: 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)에서 지원됩니다.

시뮬레이션과 테스트벤치 방법론

HDL 설계에서 시뮬레이션은 합성 전에 설계의 기능적 정확성을 검증하는 핵심 단계입니다. 효과적인 테스트벤치(Testbench) 설계와 적절한 시뮬레이션 도구 선택은 설계 품질을 결정짓는 중요한 요소입니다.

테스트벤치 구조

테스트벤치는 DUT(Design Under Test)를 감싸는 시뮬레이션 전용 환경입니다. 수동으로 파형을 확인하는 방식은 대규모 설계에서 비효율적이므로, 자체 검증(Self-Checking) 테스트벤치 패턴을 권장합니다.

자체 검증 테스트벤치의 기본 구조는 다음과 같습니다.

클록 생성 패턴

시뮬레이션에서 클록은 합성 불가능한 #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 VerilogGPL (무료)Verilog, SV 일부보통제한적지원
VerilatorLGPL (무료)Verilog, SV (합성 서브셋)매우 빠름합성 가능 서브셋지원
GHDLGPL (무료)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.fstGTKWave 전용 압축 포맷, VCD 대비 5~50배 작음GTKWave, Verilator
WDB.wdbXilinx 전용 포맷, Vivado Simulator에서 생성Vivado
FSDB.fsdbSynopsys 전용 포맷, 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

시뮬레이션 디버깅 기법

코드 커버리지

코드 커버리지(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 기능 커버리지

UVM 개요

UVM(Universal Verification Methodology)은 IEEE 1800.2 표준으로 정의된 SystemVerilog 기반 검증 방법론입니다. 객체 지향 클래스 라이브러리를 제공하여, 재사용 가능한 검증 환경을 구축할 수 있습니다. 대규모 ASIC 프로젝트와 복잡한 FPGA SoC에서 산업 표준으로 사용됩니다.

uvm_test 시퀀스 선택, 설정 오버라이드 uvm_env uvm_agent sequencer 트랜잭션 중재 driver 핀 레벨 구동 monitor 트랜잭션 수집 scoreboard 기대값 비교 Pass / Fail 판정 DUT virtual interface

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
cocotb 장단점: Python 생태계(NumPy, Matplotlib 등)를 직접 활용할 수 있으며, CI/CD 파이프라인에 pytest와 통합하기 쉽습니다. 반면 UVM 수준의 재사용 방법론이나 기능 커버리지 인프라는 제공하지 않으므로, 대규모 ASIC 검증보다는 IP 단위 기능 검증과 빠른 프로토타이핑에 적합합니다. pip install cocotb로 설치합니다.
심화 주제: 형식 검증은 SAT/SMT 솔버와 수학적 증명에 기반한 고급 검증 기법입니다. 기본 테스트벤치 구성(시뮬레이션과 테스트벤치 방법론)에 익숙해진 후 학습하세요.

형식 검증 (Formal Verification)

시뮬레이션은 입력 벡터를 선택하여 특정 시나리오를 확인하는 반면, 형식 검증(Formal Verification)은 수학적 증명(Mathematical Proof)을 통해 모든 가능한 입력 조합에 대해 설계의 속성(Property)이 성립하는지를 검증합니다. 시뮬레이션으로는 도달하기 어려운 코너 케이스(Corner Case)를 자동으로 탐색할 수 있으므로, 프로토콜 준수, 상호 배제(Mutual Exclusion), 데드락 부재 등의 검증에 매우 효과적입니다.

형식 검증 기본 개념

형식 검증은 크게 모델 검사(Model Checking)정리 증명(Theorem Proving)으로 나뉘며, RTL 설계에서는 모델 검사가 주로 사용됩니다. 핵심 기법과 용어는 다음과 같습니다.

RTL 설계 .sv / .v 모듈 속성 (Properties) assert / assume / cover SAT / SMT 솔버 BMC · k-Induction · PDR PASS 증명 완료 FAIL 반례 생성 Cover 도달 경로 트레이스 반례 분석 → 설계 수정 또는 속성 보강
심화 주제: 고급 SVA 패턴은 검증 전문가를 위한 내용입니다. 기본 어서션(assert, assume, cover)을 이해한 후 진행하세요.

고급 SVA(SystemVerilog Assertion) 기법

SVA(SystemVerilog Assertion)는 형식 검증과 시뮬레이션 모두에서 사용할 수 있는 속성 기술 언어입니다. 기본적인 assert property 외에, 복잡한 프로토콜 검증에 필요한 고급 시퀀스 연산자와 속성 구문을 지원합니다.

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 파일 구조

반례(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 코드를 실제 하드웨어(게이트 넷리스트)로 변환하는 과정입니다. 올바른 합성 결과를 얻으려면, 합성 도구가 이해할 수 있는 코딩 패턴을 따르고, 적절한 최적화 지시문을 사용해야 합니다.

RTL Code .sv / .vhd Simulation 기능 검증 Synthesis 논리 합성 Netlist 게이트 레벨 Place&Route 배치 및 배선 출력 .bit / GDSII Pass → Fail → RTL 수정 타이밍 미달 → 합성 제약 조정 리소스 초과 → RTL 아키텍처 수정 ① 작성 ② 검증 ③ 합성 ④ 넷리스트 ⑤ 구현 ⑥ 배포

합성이란 무엇인가

합성은 요리 레시피(HDL 코드)를 실제 주방 설비(논리 게이트, 플립플롭)로 변환하는 과정입니다. 레시피에 '소금을 넣습니다'고 쓰면, 합성 도구는 정확히 어느 선반에서 소금을 가져와 어떤 냄비에 넣을지를 결정합니다.

가장 간단한 예로 assign y = a & b;를 합성 도구가 처리하는 3단계를 살펴보겠습니다.

  1. RTL 파싱: 합성 도구가 소스 코드를 읽고, "a와 b의 AND 연산 결과를 y에 연결하라"는 의미를 추출합니다. 이 단계에서 문법 오류와 타입 불일치가 검출됩니다
  2. 논리 최적화: 불필요한 게이트를 제거하고, 공통 부분식(Common Subexpression)을 추출하여 게이트 수를 줄입니다. 예를 들어 (a & b) | (a & c)a & (b | c)로 최적화될 수 있습니다
  3. 기술 매핑(Technology Mapping): 최적화된 논리를 대상 디바이스에 매핑합니다. FPGA의 경우 LUT(Look-Up Table)에 매핑하여 진리표로 표현하고, ASIC의 경우 표준 셀 라이브러리(NAND2, NOR2, DFF 등)에 매핑합니다

합성이 실패하는 3가지 주요 원인:

  1. 비합성 구문 사용: #10 딜레이, $display, initial 블록의 복잡한 초기화 등 시뮬레이션 전용 구문이 합성 대상 코드에 포함된 경우입니다
  2. 타이밍 불만족: 조합 논리 경로(Critical Path)의 지연 시간이 클록 주기보다 길어서 타이밍 제약을 만족하지 못하는 경우입니다
  3. 리소스 초과: 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 인스턴스화계층적 설계
generatefor 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;

합성 경고와 대처

합성 경고는 잠재적인 설계 문제를 알려주는 중요한 피드백입니다. 다음은 가장 흔한 합성 경고와 대처 방법입니다.

심화 주제: 타이밍과 전력 인식 설계는 합성과 구현 경험이 있는 중급 이상의 설계자를 위한 내용입니다. HDL 기초 문법과 합성 개념(합성 가이드라인)을 먼저 이해하세요.

타이밍과 전력 인식 설계

HDL 코드 수준에서 타이밍(Timing)과 전력(Power)을 고려한 설계는 합성 후 타이밍 클로저(Timing Closure)와 전력 목표 달성에 직접적인 영향을 미칩니다. 합성 도구의 최적화에만 의존하지 않고, RTL 단계에서 아키텍처적 결정을 내리는 것이 효과적입니다.

타이밍 인식 RTL 설계

타이밍 위반(Timing Violation)은 조합 논리의 전파 지연(Propagation Delay)이 클록 주기를 초과할 때 발생합니다. 크리티컬 패스(Critical Path)는 가장 긴 전파 지연을 가진 조합 논리 경로이며, 설계의 최대 동작 주파수를 결정합니다.

파이프라인 적용 전 REG 긴 조합 논리 (곱셈 + 덧셈 + MUX) REG Tpd > Tclk → 위반 2단계 파이프라인 적용 후 REG 스테이지 1: 곱셈 REG 스테이지 2: 덧셈+MUX REG Tpd < Tclk 비교 파이프라인 전: Fmax = 1/Tpd_total ≈ 150 MHz (예시) 파이프라인 후: Fmax = 1/max(Tpd_stage1, Tpd_stage2) ≈ 300 MHz 대가: 1 클록 사이클 추가 지연 (Latency +1)

곱셈+덧셈 파이프라인 예제

// 파이프라인 적용 전 — 긴 크리티컬 패스
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 수준에서 스위칭 활동도(α)를 줄이는 것이 가장 효과적입니다.

클록 게이팅 예제

// 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 설계에서 가장 까다로운 문제 중 하나입니다. 주요 원인과 디버깅 방법은 다음과 같습니다.

불일치 발견 HW 결과 ≠ SIM 결과 합성 리포트 확인 래치/경고/CDC 위반 래치 경고 확인 CDC 경로 분석 할당 유형 점검 RTL 수정 + 재검증 게이트 레벨 SIM 확인 주요 합성 경고 메시지 Vivado [Synth 8-327]: inferring latch — 래치 추론 감지 Vivado [Synth 8-3332]: sequential element unused — 미사용 레지스터 제거 Vivado [DRC DPOP-2]: combinational loop — 조합 논리 루프 감지

합성 리포트 핵심 확인 항목: 합성 완료 후 반드시 확인해야 하는 항목은 (1) 래치 추론 경고, (2) 미사용 신호/포트 경고, (3) 리소스 사용량(LUT/FF/BRAM/DSP), (4) 예상 최대 주파수(Estimated Fmax)입니다. Vivado에서는 report_utilizationreport_timing_summary 명령으로 확인합니다.

IP 재사용과 패키징

효율적인 FPGA/ASIC 개발을 위해서는 검증된 IP(Intellectual Property) 블록을 재사용하는 것이 필수적입니다. 잘 설계된 IP는 프로젝트 간에 재사용할 수 있으며, 팀 간 협업과 오픈소스 생태계에도 기여할 수 있습니다.

모듈러 설계 원칙

재사용 가능한 IP를 설계하기 위해서는 다음 원칙을 따릅니다.

Vivado IP Packager

Xilinx Vivado의 IP Packager는 RTL 모듈을 IP-XACT 표준의 재사용 가능한 IP 코어로 패키징합니다. 패키징된 IP는 Vivado IP Catalog에 등록되어 블록 디자인(Block Design)에서 드래그 앤 드롭으로 사용할 수 있습니다.

# 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 카탈로그

오픈소스 IP 소스

IP 라이선스 고려사항

HDL 비교

Verilog/SystemVerilog와 VHDL은 동일한 하드웨어를 기술할 수 있지만, 문법 철학과 생태계가 다릅니다. 프로젝트의 요구사항, 팀의 경험, 사용할 도구 체인을 고려하여 선택해야 합니다.

항목Verilog / SystemVerilogVHDL
문법 스타일C 계열 (간결한 문법)Ada/Pascal 계열 (명시적이고 장황한 문법)
타입 시스템약한 타입 (암묵적 형변환 허용)강한 타입 (명시적 형변환 필수)
시뮬레이션 속도상대적으로 빠름타입 검사 오버헤드로 상대적으로 느림
산업계 채택미국, 아시아 지역에서 주류유럽, 방산/항공우주 분야에서 주류
오픈소스 도구Yosys (합성), Icarus Verilog, Verilator (시뮬레이션)GHDL (시뮬레이션), ghdl-yosys-plugin (합성 연동)
테스트벤치 프레임워크SystemVerilog UVM, cocotbOSVVM, VUnit, cocotb
대소문자 구분구분함구분하지 않음
코드 재사용 단위module, interface, packageentity/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;

핵심 차이점 요약

차세대 HDL

Verilog/VHDL의 생산성 한계를 극복하기 위해, 고수준 프로그래밍 언어를 기반으로 한 차세대 HDL이 등장하고 있습니다. 이들은 최종적으로 Verilog/VHDL 코드를 생성하므로, 기존 합성 도구 체인과 호환됩니다.

HDL기반 언어개발 조직특징출력
ChiselScalaUC Berkeley / SiFiveRISC-V 생태계의 사실상 표준, 함수형 프로그래밍으로 하드웨어 추상화, FIRRTL 중간 표현Verilog
SpinalHDLScala커뮤니티Chisel보다 전통 HDL에 가까운 문법, VexRiscv CPU로 검증, 강력한 CDC 검사 내장Verilog / VHDL
AmaranthPython커뮤니티 (구 nMigen)Python 생태계 활용, LiteX SoC 빌더와 통합, 오픈소스 FPGA 도구(Yosys/nextpnr)와 긴밀 연동Verilog (RTLIL)
ClashHaskellQBayLogic순수 함수형, 타입 시스템으로 하드웨어 오류를 컴파일 타임에 포착, 학술 연구에서 활용Verilog / VHDL
MyHDLPython커뮤니티Python 코루틴 기반 시뮬레이션, 합성 가능 서브셋 지원Verilog / VHDL

차세대 HDL은 파라미터화, 코드 생성, 타입 안전성에서 장점이 있지만, 디버깅 시 생성된 Verilog/VHDL 코드를 읽어야 하고, 벤더 도구의 에러 메시지가 원본 코드와 맞지 않을 수 있습니다. 대규모 프로젝트나 팀 환경에서는 도입 전 학습 비용과 생태계 성숙도를 고려해야 합니다.

Chisel Scala DSL FIRRTL 중간 표현 SpinalHDL Scala DSL Amaranth Python DSL RTLIL Yosys IR Verilog (.sv / .v 생성) 합성 도구 Vivado / Quartus / Yosys 기존 도구 체인 호환 → Bitstream / GDSII 대표 프로젝트 Rocket / BOOM (RISC-V) VexRiscv / SaxonSoc LiteX SoC / Glasgow

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]))

각 도구 체인의 빌드 환경

HDL 선택 가이드

프로젝트의 규모, 팀 역량, 산업 분야에 따라 적합한 HDL이 달라집니다. 다음 기준을 참고하여 HDL을 선택할 수 있습니다.

프로젝트 규모별 추천

팀 역량별 추천

산업별 관행

산업 분야주요 HDL배경
방위/항공VHDLDO-254/MIL-STD 인증 요구, 강타입 안전성, 유럽 항공 산업 전통
소비자 가전/모바일SystemVerilog높은 생산성, UVM 검증 생태계, ASIC 설계 플로우 표준
네트워크/데이터센터SystemVerilog고성능 ASIC/FPGA, 대규모 IP 재사용, P4 연계
학술/연구Chisel / Amaranth빠른 아키텍처 탐색, RISC-V 생태계(Rocket/BOOM), 오픈소스 도구 체인
자동차VHDL / SystemVerilogISO 26262 기능 안전성, AUTOSAR 호환 IP
FPGA 프로토타이핑Verilog / SystemVerilog벤더 도구 호환성, IP 카탈로그 활용

어떤 HDL을 선택하든, 최종 합성 도구가 지원하는 언어로 변환되어야 합니다. 차세대 HDL을 사용하더라도 생성된 Verilog/VHDL 코드를 읽고 디버깅할 수 있는 역량은 필수적입니다.

RTL 코딩 스타일

RTL(Register Transfer Level) 코딩 스타일은 합성 가능한 하드웨어를 올바르게 기술하기 위한 코딩 규칙과 패턴의 모음입니다. 잘못된 코딩 스타일은 의도치 않은 래치 생성, 타이밍 실패, 시뮬레이션/합성 불일치 등의 문제를 초래합니다.

합성 가능 패턴 vs 시뮬레이션 전용 구문

HDL 코드는 크게 합성 가능(Synthesizable) 구문과 시뮬레이션 전용(Simulation-only) 구문으로 나뉩니다. 합성 도구는 시뮬레이션 전용 구문을 무시하거나 오류를 발생시킵니다.

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)으로 합성됩니다. 합성 도구가 의도한 대로 레지스터를 추론하려면 일관된 코딩 패턴을 따라야 합니다.

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 @(*) 블록에서 모든 조건 분기에서 출력이 할당되지 않을 때 생성됩니다.

래치가 생성되는 코드 패턴과 수정

// [잘못된 코드] 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 등 대부분의 온칩 프로토콜이 이 패턴을 기반으로 합니다.

clk valid ready data D0 D1 (stall) D1 D2 전송 스톨 (ready=0) 전송 전송

위 다이어그램에서 녹색 영역은 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
주의: 멀티비트 데이터 버스(예: 32비트 데이터)를 그레이 코드로 변환하는 것은 올바른 CDC 방법이 아닙니다. 데이터 버스는 핸드셰이크 프로토콜 또는 비동기 FIFO를 통해 교차해야 합니다. 그레이 코드는 순차적으로 증가/감소하는 포인터(카운터)에만 적합합니다.
상세 문서: CDC 패턴의 SystemVerilog RTL 구현, MTBF 계산, 비동기 FIFO 전체 코드, SDC/XDC 타이밍 제약은 FPGA — 타이밍과 클록 도메인 교차 페이지를 참고하세요. 기초 개념은 디지털 논리회로 — 비동기 설계와 CDC를 참고하세요.

리셋 전략

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 + FDRE2-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

클록/리셋 명명

모듈 명명

린팅 도구

린팅(Linting) 도구는 합성 전에 코딩 규칙 위반, 잠재적 버그, 스타일 불일치를 자동으로 검출합니다. CI/CD 파이프라인에 통합하면 코드 품질을 지속적으로 유지할 수 있습니다.

도구라이선스대상 언어특징
VeribleApache 2.0 (Google)SystemVerilog린터 + 포매터 + 언어 서버(LSP), CI 통합 용이
SlangMITSystemVerilogIEEE 1800-2017 완전 구현 파서, 진단 메시지 우수
vsgGPL (오픈소스)VHDLVHDL Style Guide, 규칙 커스터마이즈 가능
Spyglass CDC상용 (Synopsys)SV / VHDLCDC 검증 특화, 산업 표준
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

코드 리뷰 체크리스트

참고자료

IEEE 표준

참고서적

오픈소스 학습 자료