An impure function can read or write any signal within its scope, also those that are not on the parameter list. We say that the function has side effects.
What we mean by side effects is that it is not guaranteed that the function will return the same value every time it is called with the same parameters. If the function can read signals that are not on the parameter list, the return value may depend on these shadow parameters as well. Also, the function may be altering external signals which are not assigned from its return value.
This blog post is part of the Basic VHDL Tutorials series.
Although we can declare impure functions anywhere we can declare a normal, pure function, it only makes sense to use them within processes. When declared in the architecture where we normally declare our signals, none of the signals will be in its scope at compile time. Thus, an impure function cannot do anything more than a pure function can when declared in the architecture or within a package.
The motivation for using impure functions is chiefly decluttering of the code. We could manipulate any signal with a pure function simply by adding it to the parameter list, but if the parameter list becomes too long, it would obfuscate rather than simplify.
The syntax for declaring an impure function is simply writing
impure function instead of
function when declaring it. Refer to the function tutorial for the syntax of a generic function.
In the previous tutorial, we simplified our finite-state machine (FSM) code by using a function for calculating time delay values. We provided the Minutes and Seconds parameters to specify for how long we wanted to delay each state change. If the
CounterVal function returned
true, the time had expired and it was time to go to the next FSM state. In the same process, we also had to reset the
Counter signal, otherwise, the function wouldn’t work in the next state. The timer would already be expired.
Counter signal would always be set to
0 when the function returned true. Wouldn’t it be better if this happened in the
CounterVal function instead of multiple places in the state machine code?
In this video tutorial we will improve the FSM code from the previous tutorial using an impure function:
The final code for the impure function testbench:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity T22_ImpureFunctionTb is end entity; architecture sim of T22_ImpureFunctionTb is -- We are using a low clock frequency to speed up the simulation constant ClockFrequencyHz : integer := 100; -- 100 Hz constant ClockPeriod : time := 1000 ms / ClockFrequencyHz; signal Clk : std_logic := '1'; signal nRst : std_logic := '0'; signal NorthRed : std_logic; signal NorthYellow : std_logic; signal NorthGreen : std_logic; signal WestRed : std_logic; signal WestYellow : std_logic; signal WestGreen : std_logic; begin -- The Device Under Test (DUT) i_TrafficLights : entity work.T22_TrafficLights(rtl) generic map(ClockFrequencyHz => ClockFrequencyHz) port map ( Clk => Clk, nRst => nRst, NorthRed => NorthRed, NorthYellow => NorthYellow, NorthGreen => NorthGreen, WestRed => WestRed, WestYellow => WestYellow, WestGreen => WestGreen); -- 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 traffic lights module:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity T22_TrafficLights is generic(ClockFrequencyHz : integer); port( Clk : in std_logic; nRst : in std_logic; -- Negative reset NorthRed : out std_logic; NorthYellow : out std_logic; NorthGreen : out std_logic; WestRed : out std_logic; WestYellow : out std_logic; WestGreen : out std_logic); end entity; architecture rtl of T22_TrafficLights is -- Calculate the number of clock cycles in minutes/seconds function CounterVal(Minutes : integer := 0; Seconds : integer := 0) return integer is variable TotalSeconds : integer; begin TotalSeconds := Seconds + Minutes * 60; return TotalSeconds * ClockFrequencyHz -1; end function; -- Enumerated type declaration and state signal declaration type t_State is (NorthNext, StartNorth, North, StopNorth, WestNext, StartWest, West, StopWest); signal State : t_State; -- Counter for counting clock periods, 1 minute max signal Counter : integer range 0 to ClockFrequencyHz * 60; begin process(Clk) is -- This impure function reads and drives the Counter signal -- which is not on the parameter list. impure function CounterExpired(Minutes : integer := 0; Seconds : integer := 0) return boolean is begin if Counter = CounterVal(Minutes, Seconds) then Counter <= 0; return true; else return false; end if; end function; begin if rising_edge(Clk) then if nRst = '0' then -- Reset values State <= NorthNext; Counter <= 0; NorthRed <= '1'; NorthYellow <= '0'; NorthGreen <= '0'; WestRed <= '1'; WestYellow <= '0'; WestGreen <= '0'; else -- Default values NorthRed <= '0'; NorthYellow <= '0'; NorthGreen <= '0'; WestRed <= '0'; WestYellow <= '0'; WestGreen <= '0'; Counter <= Counter + 1; case State is -- Red in all directions when NorthNext => NorthRed <= '1'; WestRed <= '1'; -- If 5 seconds have passed if CounterExpired(Seconds => 5) then State <= StartNorth; end if; -- Red and yellow in north/south direction when StartNorth => NorthRed <= '1'; NorthYellow <= '1'; WestRed <= '1'; -- If 5 seconds have passed if CounterExpired(Seconds => 5) then State <= North; end if; -- Green in north/south direction when North => NorthGreen <= '1'; WestRed <= '1'; -- If 1 minute has passed if CounterExpired(Minutes => 1) then State <= StopNorth; end if; -- Yellow in north/south direction when StopNorth => NorthYellow <= '1'; WestRed <= '1'; -- If 5 seconds have passed if CounterExpired(Seconds => 5) then State <= WestNext; end if; -- Red in all directions when WestNext => NorthRed <= '1'; WestRed <= '1'; -- If 5 seconds have passed if CounterExpired(Seconds => 5) then State <= StartWest; end if; -- Red and yellow in west/east direction when StartWest => NorthRed <= '1'; WestRed <= '1'; WestYellow <= '1'; -- If 5 seconds have passed if CounterExpired(Seconds => 5) then State <= West; end if; -- Green in west/east direction when West => NorthRed <= '1'; WestGreen <= '1'; -- If 1 minute has passed if CounterExpired(Minutes => 1) then State <= StopWest; end if; -- Yellow in west/east direction when StopWest => NorthRed <= '1'; WestYellow <= '1'; -- If 5 seconds have passed if CounterExpired(Seconds => 5) then State <= NorthNext; end if; end case; end if; end if; end process; end architecture;
The waveform after we entered the
run 5 min command in the ModelSim console:
As we can see from the waveform, the module output remains unchanged after we added the impure function. We haven’t changed the logic at all, only the code.
The evaluation of the
Counter signal has been moved from the FSM code into the new impure function
Counter <= 0; line for clearing the
Counter signal has also been moved into the impure function.
The result is a more readable FSM code which can be more easily maintained. This is subjective, but for me
CounterExpired(Seconds => 5) is easier on the eyes than
Counter = CounterVal(Seconds => 5).
How far you should go with using impure functions is entirely up to you and whoever pays for your services. Some people feel that they should be used with caution because it can be harder to see through all the causes and effects of an algorithm hidden in a subprogram. Others, like me, feel that as long as you make your intentions clear, the easier to read code actually makes it less error-prone.
For this reason, you are more likely to find impure functions in testbench code than in production modules. Testbenches are typically more complex than the module they are testing, and the requirement for code correctness is less strict than for RTL code.
- Impure functions can read or drive signals that are not on its parameter list
- It only makes sense to declare impure functions within a process