A procedure is a type of subprogram in VHDL which can help us avoid repeating code. Sometimes the need arises to perform identical operations several places throughout the design. While creating a module might be overkill for minor operations, a procedure is often what you want.
Procedures can be declared within any declarative region. The scope of the procedure will be limited to wherever it’s declared, architecture, package, or process. Whenever you call the procedure, it will behave like the code of the procedure was inserted where it was called from.
A procedure doesn’t return a value like a function does, but you can return values by declaring
inout signals in the parameter list.
This blog post is part of the Basic VHDL Tutorials series.
The basic syntax for creating a procedure is:
procedure <procedure_name> (signal|variable|constant <name1> : in|out|inout <type>;
signal|variable|constant <name2> : in|out|inout <type>;
... ) is
A procedure’s parameter list defines its inputs and outputs, kind of like a mini-module. It can be a signal or a constant, but unlike a module, it can also be a variable. You can declare objects between the “is” and “begin” keywords that are only valid inside the procedure. These may include constants, variables, types, subtypes, and aliases, but not signals.
Unlike functions, procedures may contain wait-statements. Therefore, they are often used in testbenches like simple BFM’s for simulating interfaces, or for checking output from the device under test (DUT).
In the previous tutorial we created a timer module using nested If-Then-Else statements. Each level of If-Then-Else inside of another If-Then-Else adds complexity to the design, and it becomes less readable. On each level of logic, we are basically doing the same operation on a different set of signals. Isn’t there a better way to do this?
In this video tutorial we will learn how to create a procedure in VHDL:
The final code for the procedure testbench:
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 -- We're slowing down the clock to speed up simulation time 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 -- The Device Under Test (DUT) i_Timer : entity work.T19_Timer(rtl) generic map(ClockFrequencyHz => ClockFrequencyHz) port map ( Clk => Clk, nRst => nRst, Seconds => Seconds, Minutes => Minutes, Hours => Hours); -- Process for generating clock Clk <= not Clk after ClockPeriod / 2; -- Testbench sequence process is begin wait until rising_edge(Clk); wait until rising_edge(Clk); -- Take the DUT out of reset nRst <= '1'; wait; end process; end architecture;
The final code for the timer module using a procedure:
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 for counting clock periods 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 the negative reset signal is active if nRst = '0' then Ticks <= 0; Seconds <= 0; Minutes <= 0; Hours <= 0; else -- Cascade counters 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;
The waveform window in ModelSim, zoomed in on the timeline where the
Minutes signal is wrapping:
We can see from the waveform that the wrapping of signals still work as it did in the previous tutorial. That’s because we haven’t actually changed the function on the module, only the way it’s implemented.
The first item on the parameter list for the
IncrementWrap procedure is the
Counter signal. It’s declared using direction
inout for the procedure to be able to both read and set its value.
The second and third items on the parameter list are constants. This means that the values you put in here will appear as constants inside of the procedure. The
WrapValue input together with the
Enable input determines if the
Counter signal is incremented or wrapped.
The last item on the parameter list is a variable with direction
out. The purpose of this output is to inform the caller of the procedure that the counter wrapped. We use it here kind of like a return value.
In the main process we have four calls to the
IncrementWrap procedure. Each of the subsequent calls use the
Wrap variable to enable the counting. It wouldn’t have worked if we had used a signal instead of a variable, because signal values are only updated when a process goes to sleep. We need the output value from one procedure call to be be used as an input to a call on the very next line. Thus, it has to be a variable.
- Procedures can be used as mini-modules to avoid copy-pasting code
- Parameters (inputs/outputs) to a procedure can be signals, variables, or constants
- Unlike functions, procedures can contain wait-statements