VHDL 문법 레퍼런스: 설계 단위부터 프로세스/타입/generate까지(단일 포스트)
VHDL을 정리할 때 “몇 개 키워드만 외우는 방식”은 실전에서 잘 안 굴러간다. 설계 단위에서 시작해서 타입, 프로세스, generate, 패키지, 속성까지 이어지는 문법이 서로 얽혀 있기 때문이다. 이 글은 책처럼 읽는 글이 아니라, 작업 중에 필요한 구문을 빠르게 찾아보기 위한 단일 레퍼런스로 구성한다. 목적은 “문법을 빠뜨려서 헤매는 시간”을 줄이는 데 있다.
작성 기준은 VHDL-2008이며, FPGA 합성(RTL)에서 자주 쓰는 패턴에 우선순위를 둔다. 시뮬레이션 전용 문법은 별도로 표시한다12.
환경/전제
- 표준 기준: IEEE VHDL(1076), VHDL-2008 중심
- 합성 기준: FPGA 합성기가 해석 가능한 RTL 관점
- 산술 패키지:
ieee.numeric_std사용
빠른 목차(찾아보기)
- 설계 단위:
library/use,entity,architecture,package,package body,configuration - 선언:
constant,signal,variable,type/subtype,alias,attribute - 타입:
std_logic,std_logic_vector,unsigned/signed,record,array,enum - 동시 문장: 동시 할당, 컴포넌트/엔티티 인스턴스,
generate - 순차 문장:
process,if/case/loop,exit/next/return,assert/report - 서브프로그램:
function,procedure,impure/pure, 오버로딩
library / use
가장 흔한 기본 조합은 std_logic_1164와 numeric_std다.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.numeric_std.all;
산술 의미(unsigned/signed)와 변환(unsigned(x), std_logic_vector(u))의 기준을 numeric_std로 고정한다.
entity
entity는 인터페이스 계약이다. 포트와 제너릭의 스코프를 명확히 나누는 것이 유지보수에 유리하다.
entity my_block is
generic (
WIDTH : natural := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
din : in std_logic_vector(WIDTH-1 downto 0);
dout : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
generic ( WIDTH : natural := 8 );
폭/깊이 같은 파라미터는 natural/positive로 제한하면 합성 제약을 자연스럽게 유도할 수 있다.
architecture
architecture는 구현이다. 동시 문장(concurrent statements)이 기본이며, process 내부만 순차 문장(sequential statements)이다.
architecture rtl of my_block is
signal r : unsigned(WIDTH-1 downto 0);
begin
-- concurrent statements live here
end architecture;
architecture rtl of my_block is
rtl이라는 이름은 관례일 뿐이며, 중요한 것은 “동일 entity에 여러 architecture가 존재할 수 있다”는 구조다.
port / generic 타입 규칙(자주 쓰는 조합)
std_logic: 단일 비트 신호std_logic_vector(N-1 downto 0): 외부 버스 표현unsigned/signed: 내부 산술 표현
signal u : unsigned(WIDTH-1 downto 0);
signal v : std_logic_vector(WIDTH-1 downto 0);
signal u : unsigned(WIDTH-1 downto 0);
산술은 unsigned/signed로 하고, 포트/상위 인터페이스에서는 std_logic_vector로 유지하는 분리가 안전하다.
constant / signal / variable
세 객체는 “값이 어디에 존재하는가”와 “업데이트 시점”이 다르다.
constant: 변경 불가 값signal: 이벤트/델타 사이클 기반 업데이트(예약)variable: 프로세스 내부 즉시 업데이트(즉시)
constant INIT : unsigned(7 downto 0) := x"00";
signal r : unsigned(7 downto 0);
process(clk)
variable next_r : unsigned(7 downto 0);
begin
if rising_edge(clk) then
next_r := r;
r <= next_r;
end if;
end process;
variable next_r : unsigned(7 downto 0);
다음 상태 계산을 변수로 만든 뒤 마지막에 signal <=로 한 번만 반영하는 패턴이 안정적이다.
type / subtype
타입 정의는 패키지로 올리는 편이 재사용성이 높다. subtype은 범위/제약을 붙여 의미를 강화하는 데 유용하다.
type state_t is (IDLE, RUN, DONE);
subtype byte_u is unsigned(7 downto 0);
subtype byte_u is unsigned(7 downto 0);
“8비트 unsigned”를 매번 반복하지 않고 의미 이름으로 고정할 수 있다.
record
레코드는 관련 신호를 묶어 인터페이스를 단순화한다. 버스/채널 구조를 정리할 때 특히 유용하다.
type axi_like_t is record
valid : std_logic;
ready : std_logic;
data : std_logic_vector(31 downto 0);
end record;
signal ch : axi_like_t;
type axi_like_t is record
레코드는 “신호 다발의 의미 단위”를 코드로 고정하는 도구다.
array
배열은 1차원만이 아니라 2차원 형태로도 자주 쓰인다. 합성기 제약을 고려해 범위를 명확히 두는 편이 좋다.
type mem_t is array (0 to 255) of std_logic_vector(31 downto 0);
signal mem : mem_t;
type mem_t is array (0 to 255) of std_logic_vector(31 downto 0);
메모리 추론을 기대하는 경우, 인덱스 범위와 접근 패턴이 더 중요해진다.
process
순차 문장은 process 안에서만 유효하다. 클럭 프로세스는 rising_edge(clk)로 고정하는 것이 기본이다.
process(clk)
begin
if rising_edge(clk) then
-- sequential statements
end if;
end process;
if rising_edge(clk) then
클럭 에지 조건을 템플릿처럼 고정하면, 레지스터 추론 패턴이 흔들리지 않는다.
if / elsif / else (순차)
순차 if는 우선순위를 가진다. 동일 프로세스에서 여러 분기가 같은 신호를 할당할 때 “마지막 할당만 유효”처럼 보일 수 있으므로, 다음 상태 변수 패턴과 같이 쓰는 편이 좋다.
if cond_a = '1' then
next_r := x"01";
elsif cond_b = '1' then
next_r := x"02";
else
next_r := x"00";
end if;
elsif cond_b = '1' then
우선순위가 의도인지 확인하는 것이 핵심이다.
case (순차)
열거형 상태 머신에서 case가 가장 흔하다. 합성 관점에서는 when others를 명시해 안전성을 확보하는 편이 좋다.
case state is
when IDLE =>
next_state := RUN;
when RUN =>
next_state := DONE;
when DONE =>
next_state := IDLE;
when others =>
next_state := IDLE;
end case;
when others =>
정의되지 않은 상태를 어떻게 회복시킬지 정책을 코드로 고정한다.
loop / for / while (순차)
합성 가능한 루프는 “고정 반복” 중심이다. 즉 런타임 반복이 아니라, 정적 펼침(unrolling)에 가까운 형태로 해석된다.
for i in 0 to WIDTH-1 loop
tmp(i) := a(i) xor b(i);
end loop;
for i in 0 to WIDTH-1 loop
반복 범위가 제너릭에 의해 결정되더라도, 합성 시점에 정적으로 결정될 수 있어야 한다.
exit / next
루프 제어를 명시한다. 합성에서 사용 가능 여부는 패턴과 도구에 따라 다를 수 있으므로, 팀 규칙을 정해두는 편이 좋다1.
for i in 0 to 15 loop
exit when hit = '1';
end loop;
exit when hit = '1';
합성 가능성은 “고정 반복 + 조건 종료” 패턴에 달려 있다.
assert / report (주로 시뮬레이션)
검증에는 유용하지만 합성에서는 보통 무시되거나 제한된다. 따라서 “시뮬레이션 전용”으로 취급하는 편이 안전하다.
assert (WIDTH > 0)
report "WIDTH must be positive"
severity failure;
severity failure;
시뮬레이터에서 실패를 강제하는 장치다.
동시 할당(concurrent assignment)
동시 문장은 architecture의 begin 아래에서 병렬로 존재한다.
sum <= unsigned(a) + unsigned(b);
y <= std_logic_vector(sum);
sum <= unsigned(a) + unsigned(b);
산술과 표현 변환의 경계를 명확히 분리한다.
selected / conditional signal assignment
조건부 동시 할당은 조합 MUX를 표현하는 수단이다.
y <= a when sel = '0' else b;
y <= a when sel = '0' else b;
우선순위가 없는 2:1 선택 형태다.
component / instantiation (권장: entity instantiation)
현대 스타일에서는 component 선언 없이 entity work.xxx(rtl) 형태로 직접 인스턴스화하는 편이 단순하다.
u0: entity work.counter(rtl)
generic map (
WIDTH => 8
)
port map (
clk => clk,
rst_n => rst_n,
q => q
);
u0: entity work.counter(rtl)
바인딩이 명시적이라 파일 구조가 커져도 추적이 쉽다.
generic map / port map
맵핑은 “명시적 이름 매핑”이 기본이다. 포지션 매핑은 유지보수 비용을 올린다.
generic map ( WIDTH => 16 )
port map (
clk => clk,
rst_n => rst_n,
q => q
);
generic map ( WIDTH => 16 )
파라미터의 의미가 코드에 남는다.
generate (if / for)
generate는 런타임 분기가 아니라, 엘라보레이션 시점 구조 생성이다.
gen_small: if WIDTH <= 8 generate
y <= std_logic_vector(unsigned(a) + unsigned(b));
end generate;
gen_bits: for i in 0 to WIDTH-1 generate
y(i) <= x(i) and en;
end generate;
gen_small: if WIDTH <= 8 generate
“구조 선택”을 제너릭 기반으로 고정한다.
function / procedure
함수는 “반환값” 중심, 프로시저는 “부수효과(파라미터 업데이트)” 중심이다. 합성에서 함수는 조합 로직으로 잘 매핑되는 경우가 많다.
package math_pkg is
function sat_inc(x : unsigned) return unsigned;
end package;
package body math_pkg is
function sat_inc(x : unsigned) return unsigned is
begin
if x = (others => '1') then
return x;
else
return x + 1;
end if;
end function;
end package body;
function sat_inc(x : unsigned) return unsigned;
의미 있는 연산을 함수로 올려두면, RTL 본문이 읽기 쉬워진다.
package / package body
패키지는 “공유 선언 레이어”다. 타입/상수/함수 선언이 여기에 들어간다.
package util_pkg is
subtype byte_u is unsigned(7 downto 0);
constant ZERO : byte_u := (others => '0');
end package;
subtype byte_u is unsigned(7 downto 0);
프로젝트 규모가 커질수록 패키지 경계가 곧 설계의 경계가 된다.
configuration (드묾)
대형 시뮬레이션 환경에서 바인딩을 강제하는 용도로 등장한다. FPGA RTL에서는 보통 사용 빈도가 낮다1.
configuration cfg of my_block is
for rtl
end for;
end configuration;
configuration cfg of my_block is
툴 플로우가 바인딩을 이미 결정해 주는 경우가 많다.
attribute
속성은 표준 속성과 벤더 속성이 섞인다. 벤더 속성은 툴 문서에 의존하므로, 사용 시점을 제한하는 편이 좋다3.
attribute keep : string;
attribute keep of r : signal is "true";
attribute keep of r : signal is "true";
합성 최적화를 막기 위한 힌트로 쓰이지만, 남용하면 오히려 타이밍/리소스가 악화될 수 있다.
std_logic 계열과 산술 변환 규칙(실전 요약)
VHDL에서 가장 흔한 컴파일/리뷰 포인트는 “변환 지점이 여기저기 흩어지는 문제”다. 기본 원칙은 다음처럼 고정하는 편이 낫다.
- 산술 타입 고정:
unsigned/signed - 포트 타입 고정:
std_logic_vector - 변환 지점 제한: 인터페이스 경계에서만 수행
sum_u <= unsigned(a_slv) + unsigned(b_slv);
y_slv <= std_logic_vector(sum_u);
y_slv <= std_logic_vector(sum_u);
표현 변환은 가능한 한 마지막에 모은다.
마무리 메모
이 글은 “표준 문서의 모든 생산 규칙”을 그대로 옮긴 글이 아니라, 실제 RTL을 쓰면서 가장 자주 만나는 문법 요소를 빠르게 찾기 위한 참고서 형태로 구성했다. 여기서 다룬 문법 요소의 합성 가능 범위는 툴/버전에 따라 달라질 수 있으므로, 의존성이 생기는 지점(속성, 일부 루프 제어, 검증용 문장)은 벤더 문서를 함께 확인하는 편이 안전하다.
References
-
IEEE Standard for VHDL Language Reference Manual - VHDL(IEEE 1076) 언어 정의 문서다. ↩ ↩2 ↩3
-
GHDL Documentation - VHDL-2008 지원 범위와 시뮬레이션 관점의 구문을 확인할 수 있다. ↩
-
Xilinx UG901 Vivado Synthesis - 합성 옵션/속성/추론 규칙 등 벤더 의존 영역 확인용 문서다. ↩