VHDL에서 프로시저는 설계자가 반복 코드 블록을 피하게 해주는 서브프로그램입니다. 같은 연산이 여러 곳에서 필요할 때 전체 모듈을 만드는 건 과할 수 있습니다. 프로시저는 가벼우면서 재사용 가능한 해결책을 제공합니다.
프로시저는 아키텍처, 패키지, 프로세스 등 모든 선언 영역에 선언 가능하며 그 범위는 해당 영역으로 한정됩니다. 호출 시 프로시저 본문이 마치 제자리에서 작성된 것처럼 실행되어 별도 컴포넌트 오버헤드 없이 모듈러리티 이점을 줍니다.
함수와 달리 프로시저는 직접 값을 반환하지 않습니다. 하지만 파라미터 리스트에 out 또는 inout 시그널을 선언하면 값을 돌려줄 수 있어, 입출력을 가진 작은 모듈처럼 동작하게 할 수 있습니다.
이 글은 Basic VHDL Tutorials 시리즈의 일부입니다.
프로시저 정의의 정식 문법은 아래와 같습니다:
procedure <procedure_name> (signal|variable|constant <name1> : in|out|inout <type>; signal|variable|constant <name2> : in|out|inout <type>; ... ) is <declarations_for_use_within_the_procedure> begin <code_performed_by_the_procedure_here> end procedure;
파라미터 리스트는 프로시저의 입출력을 정의합니다—마치 미니 모듈처럼. 파라미터는 시그널, 변수, 상수가 될 수 있습니다. 프로시저 내부에서 추가 객체(상수, 변수, 타입, 서브타입, 별칭)를 선언할 수 있으며 이들은 해당 블록에서만 유효합니다. 시그널은 로컬로 선언할 수 없습니다.
프로시저는 wait 문장을 포함할 수 있어 테스트벤치에서 특히 유용합니다. 예: 간단한 BFM 드라이버나 DUT 출력 검증용으로.
실습
앞선 튜토리얼에서 타이머 모듈을 깊은 중첩 if-then-else 문으로 만들었습니다. 각 중첩 레벨마다 복잡도가 늘고 가독성이 떨어졌지만, 다른 시그널 세트에 같은 연산을 수행했습니다. 더 나은 접근은 반복 로직을 프로시저로 캡슐화하는 것입니다.
다음 비디오 튜토리얼은 VHDL 프로시저 만들기를 안내합니다:
예제: 프로시저 기반 타이머
테스트벤치 코드는 DUT를 구동하는 프로시저 호출 추가 외엔 동일합니다:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity T19_ProcedureTb is end entity; architecture sim of T19_ProcedureTb is constant ClockFrequencyHz : integer := 10; -- 10 Hz constant ClockPeriod : time := 1000 ms / ClockFrequencyHz; signal Clk : std_logic := '1'; signal nRst : std_logic := '0'; signal Seconds : integer; signal Minutes : integer; signal Hours : integer; begin i_Timer : entity work.T19_Timer(rtl) generic map(ClockFrequencyHz => ClockFrequencyHz) port map ( Clk => Clk, nRst => nRst, Seconds => Seconds, Minutes => Minutes, Hours => Hours); Clk <= not Clk after ClockPeriod / 2; process is begin wait until rising_edge(Clk); wait until rising_edge(Clk); nRst <= '1'; wait; end process; end architecture;
타이머 모듈은 이제 카운터 증가에 프로시저를 활용합니다:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity T19_Timer is generic(ClockFrequencyHz : integer); port( Clk : in std_logic; nRst : in std_logic; -- Negative reset Seconds : inout integer; Minutes : inout integer; Hours : inout integer); end entity; architecture rtl of T19_Timer is signal Ticks : integer; procedure IncrementWrap( signal Counter : inout integer; constant WrapValue : in integer; constant Enable : in boolean; variable Wrapped : out boolean) is begin Wrapped := false; if Enable then if Counter = WrapValue - 1 then Wrapped := true; Counter <= 0; else Counter <= Counter + 1; end if; end if; end procedure; begin process(Clk) is variable Wrap : boolean; begin if rising_edge(Clk) then if nRst = '0' then Ticks <= 0; Seconds <= 0; Minutes <= 0; Hours <= 0; else IncrementWrap(Ticks, ClockFrequencyHz, true, Wrap); IncrementWrap(Seconds, 60, Wrap, Wrap); IncrementWrap(Minutes, 60, Wrap, Wrap); IncrementWrap(Hours, 24, Wrap, Wrap); end if; end if; end process; end architecture;
파형에서 랩어라운드 기능이 원래 중첩-if 구현과 동일하게 동작함을 알 수 있습니다. 변경은 순수 구조적이며 내부 논리는 바뀌지 않았습니다.
첫 파라미터 Counter는 inout 시그널로, 프로시저가 값을 읽고 갱신할 수 있게 합니다. 두 번째와 세 번째 파라미터 WrapValue와 Enable은 상수이며 프로시저 내부에서 컴파일 타임 상수로 취급됩니다.
마지막 파라미터 Wrapped는 out으로 선언된 variable입니다. 이는 해당 호출에서 카운터가 랩어라운드했는지 나타내는 의사 반환값 역할을 합니다.
메인 프로세스에서 IncrementWrap을 네 번 연속 호출해 카운터를 캐스케이드합니다. 이전 호출의 Wrap 변수가 다음 호출의 Enable 입력이 됩니다. 시그널 업데이트는 프로세스 정지 후 가시화되므로 즉각적인 절차적 데이터 흐름을 위해 변수 사용이 필수적입니다.