1편에서 설계 단위(entity/architecture/package)의 경계를 정리했다. 그 다음에 실제 RTL을 쓰기 시작하면 거의 반드시 두 가지 질문이 나온다. 첫째, std_logic_vector로 연산을 하면 왜 갑자기 변환이 필요해지는가. 둘째, signalvariable을 섞어 쓰면 시뮬레이션 결과가 왜 달라지는가. 이 글은 그 질문을 문법 조각이 아니라 시간 모델(time model)타입 시스템(type system) 관점에서 정리한다.

기준은 VHDL-2008이며, 합성 가능한 RTL을 전제로 한다1. 일부 예시는 “합성 불가/시뮬레이션 전용”임을 명시한다.

타입 계층: std_logic_vector는 “비트열”이고 숫자가 아니다

std_logic_vector는 흔히 “버스”로 쓰이지만, 본질적으로는 비트의 배열이다. 숫자 연산 의미를 붙이는 순간, 연산의 주체 타입을 unsigned 또는 signed로 바꾸는 것이 일관된 스타일이다2.

  • std_logic_vector: 표현/인터페이스 계층
  • unsigned/signed: 산술/의미 계층
  • 변환: 의미 ↔ 표현의 경계에서만 수행

이 분리를 지키면 코드가 길어지는 대신, 합성기/시뮬레이터의 해석 차이를 줄이고, 리뷰/디버깅이 쉬워진다.

전체 코드 예제: 조합 + 순차 혼합에서의 타입/변환

아래 예제는 입력 a, b를 더해서 누산하고, 결과를 std_logic_vector로 출력하는 형태다. 의도는 “숫자 연산은 unsigned로, 포트는 std_logic_vector로”를 눈에 보이게 만드는 것이다.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity adder_accum is
  generic (
    WIDTH : natural := 8
  );
  port (
    clk   : in  std_logic;
    rst_n : in  std_logic;
    a     : in  std_logic_vector(WIDTH-1 downto 0);
    b     : in  std_logic_vector(WIDTH-1 downto 0);
    y     : out std_logic_vector(WIDTH-1 downto 0)
  );
end entity;

architecture rtl of adder_accum is
  signal acc : unsigned(WIDTH-1 downto 0);
  signal sum : unsigned(WIDTH-1 downto 0);
begin
  -- concurrent assignment: 조합 논리
  sum <= unsigned(a) + unsigned(b);

  -- sequential statements inside process: 순차 논리
  process(clk)
  begin
    if rising_edge(clk) then
      if rst_n = '0' then
        acc <= (others => '0');
      else
        acc <= acc + sum;
      end if;
    end if;
  end process;

  -- 표현 계층으로 변환
  y <= std_logic_vector(acc);
end architecture;

여기서 “타입 경계”를 만드는 라인은 다음 세 군데다.

sum <= unsigned(a) + unsigned(b);

a, b는 포트에서 std_logic_vector이므로 숫자 의미를 가지지 않는다. 산술은 unsigned로 수행하고, 그 결과 sum은 산술 타입으로 유지한다.

acc <= acc + sum;

산술 타입끼리의 연산이라 변환이 개입하지 않는다. 이 상태가 유지될수록 RTL은 읽기 쉬워진다.

y <= std_logic_vector(acc);

출력 포트는 “인터페이스 계층”으로 유지하고 싶으므로 마지막에 표현으로 변환한다. 이 위치가 변환의 단일 지점이 된다.

동시(concurrent)와 순차(sequential)의 경계

architecture ... begin ... end 구간은 기본적으로 “동시 문장”이 배치되는 영역이다. 그 안에 process가 들어가면, process 내부만 “순차 문장”으로 평가된다.

  • 동시 문장: 여러 식이 병렬로 존재하는 선언(조합 논리 모델)
  • 순차 문장: process 안에서 위에서 아래로 실행되는 문장(시간 축을 갖는 모델)

이 차이를 모르면 “왜 이 줄이 바로 반영되지 않는가” 같은 문제가 반복된다. 그 대표 사례가 signal vs variable이다.

signal과 variable: “업데이트 시점”이 다르다

요약하면 다음과 같다.

  • signal <=: 델타 사이클 이후에 값이 반영되는 예약(scheduled update)
  • variable :=: 그 시점에서 즉시 값이 바뀌는 즉시 업데이트(immediate update)

그래서 같은 프로세스 안에서도 signal만 쓰면 중간 계산이 꼬이는 것처럼 보일 수 있고, variable을 섞으면 “순간적으로” 의도에 맞는 계산이 가능해진다.

예제: 같은 프로세스 안에서의 누산(변수 사용)

아래는 흔히 쓰는 패턴이다. 같은 클럭 에지에서 여러 조건을 처리하면서 최종 결과를 한 번만 레지스터에 싣고 싶을 때, 내부 계산을 variable로 하고 마지막에 signal에 할당하는 방식이다.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity saturating_counter is
  generic ( WIDTH : natural := 8 );
  port (
    clk   : in  std_logic;
    rst_n : in  std_logic;
    en    : in  std_logic;
    q     : out std_logic_vector(WIDTH-1 downto 0)
  );
end entity;

architecture rtl of saturating_counter is
  signal r : unsigned(WIDTH-1 downto 0);
begin
  process(clk)
    variable next_r : unsigned(WIDTH-1 downto 0);
  begin
    if rising_edge(clk) then
      if rst_n = '0' then
        r <= (others => '0');
      else
        next_r := r;
        if en = '1' then
          if r /= (others => '1') then
            next_r := r + 1;
          end if;
        end if;
        r <= next_r;
      end if;
    end if;
  end process;

  q <= std_logic_vector(r);
end architecture;

핵심 라인은 두 개다.

variable next_r : unsigned(WIDTH-1 downto 0);
...
next_r := r;
...
r <= next_r;

next_r는 프로세스 내부의 계산 버퍼다. 조건 분기가 여러 번 있어도 마지막에 r <= next_r;만 남기면, 합성기는 이를 “다음 상태 계산 + 레지스터”로 쉽게 해석한다. 반대로 분기마다 r <= ...를 여러 번 쓰면, 시뮬레이션에서는 “마지막 할당만 유효”가 되어 의도와 다르게 읽힐 수 있다.

sensitivity list: VHDL-2008의 process(all)와 실무 감각

조합 프로세스에서 민감도 리스트가 빠지면 시뮬레이션과 합성 간 불일치가 생길 수 있다. VHDL-2008에서는 process(all)로 이를 완화할 수 있다1.

  • 조합 논리 프로세스: process(all) 또는 모든 입력 신호를 민감도에 포함
  • 순차 논리 프로세스: process(clk) (비동기 리셋이 있으면 process(clk, rst_n) 등)

다만 많은 FPGA 툴이 민감도 리스트 누락을 경고로만 처리하기도 해서, “경고인데도 회로가 나오니 괜찮다”는 착시가 생긴다. 이 영역은 문법보다 검증 습관의 문제에 가깝다.

다음 편으로의 연결

2편까지 이해하면 “문법은 맞는데 회로가 이상하다” 수준의 문제 대부분은 피할 수 있다. 다음 단계는 합성 관점에서 안전한 클럭/리셋 패턴을 일관되게 쓰는 일이다. 3편에서는 rising_edge(clk) 기반의 순차 템플릿, 동기/비동기 리셋의 트레이드오프, 그리고 generate/generic으로 구조를 확장하는 방식을 다룬다.

References

  1. IEEE Standard for VHDL Language Reference Manual - process(all) 등 VHDL-2008 문법 정의를 포함한다.  2

  2. IEEE numeric_std (overview) - unsigned/signed 산술 의미를 표준으로 제공하는 패키지 계열이다.