Using Impure Functions in VHDL: Enhancing FSM Readability and Maintainability
In VHDL, an impure function can access or modify any signal within its lexical scope, including signals that are not listed as parameters. This capability gives the function side effects—its return value may vary even when the same arguments are supplied, and it may alter external signals that are not directly returned.
While pure functions are confined to deterministic behaviour, impure functions are invaluable for cleaning up complex state‑machine logic by moving repetitive or side‑effecting tasks into a single, well‑named subprogram.
Note: Impure functions are best declared inside a process. Declaring them at the architecture or package level offers no advantage, as the function’s scope would be limited to the signals that are already in scope at compile time.
When and Why to Use Impure Functions
Typical scenarios include:
- Resetting a counter when a timer expires.
- Checking whether a countdown has finished without duplicating the counter comparison logic.
- Encapsulating signal manipulation that would otherwise clutter the main FSM.
Using an impure function keeps the core state‑machine logic readable while still preserving the side‑effecting behaviour required by the design.
Syntax
To declare an impure function, simply prepend the keyword impure before function:
impure function CounterExpired(Minutes : integer := 0; Seconds : integer := 0) return boolean;
Refer to the function tutorial for full syntax details.
Exercise: FSM Refactoring with an Impure Function
Previously we calculated time delays using a pure function CounterVal and manually reset the counter whenever the timer expired. The following impure function consolidates this logic:
Testbench (T22_ImpureFunctionTb)
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
-- Low clock frequency to accelerate 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
-- DUT instantiation
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);
-- Clock generation
Clk <= not Clk after ClockPeriod / 2;
-- Testbench sequence
process is
begin
wait until rising_edge(Clk);
wait until rising_edge(Clk);
-- Release reset
nRst <= '1';
wait;
end process;
end architecture;
Traffic Lights Module (T22_TrafficLights)
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
-- Pure function to compute clock cycles for a given time
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;
-- FSM state definition
type t_State is (NorthNext, StartNorth, North, StopNorth,
WestNext, StartWest, West, StopWest);
signal State : t_State;
-- Counter for clock periods, up to one minute
signal Counter : integer range 0 to ClockFrequencyHz * 60;
begin
process(Clk) is
-- Impure function that reads and drives Counter
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 state and signals
State <= NorthNext;
Counter <= 0;
NorthRed <= '1';
NorthYellow <= '0';
NorthGreen <= '0';
WestRed <= '1';
WestYellow <= '0';
WestGreen <= '0';
else
-- Default outputs
NorthRed <= '0';
NorthYellow <= '0';
NorthGreen <= '0';
WestRed <= '0';
WestYellow <= '0';
WestGreen <= '0';
Counter <= Counter + 1;
case State is
when NorthNext =>
NorthRed <= '1';
WestRed <= '1';
if CounterExpired(Seconds => 5) then
State <= StartNorth;
end if;
when StartNorth =>
NorthRed <= '1';
NorthYellow <= '1';
WestRed <= '1';
if CounterExpired(Seconds => 5) then
State <= North;
end if;
when North =>
NorthGreen <= '1';
WestRed <= '1';
if CounterExpired(Minutes => 1) then
State <= StopNorth;
end if;
when StopNorth =>
NorthYellow <= '1';
WestRed <= '1';
if CounterExpired(Seconds => 5) then
State <= WestNext;
end if;
when WestNext =>
NorthRed <= '1';
WestRed <= '1';
if CounterExpired(Seconds => 5) then
State <= StartWest;
end if;
when StartWest =>
NorthRed <= '1';
WestRed <= '1';
WestYellow <= '1';
if CounterExpired(Seconds => 5) then
State <= West;
end if;
when West =>
NorthRed <= '1';
WestGreen <= '1';
if CounterExpired(Minutes => 1) then
State <= StopWest;
end if;
when StopWest =>
NorthRed <= '1';
WestYellow <= '1';
if CounterExpired(Seconds => 5) then
State <= NorthNext;
end if;
end case;
end if;
end if;
end process;
end architecture;
Waveform Insight
After issuing the run 5 min command in ModelSim, the simulation shows identical behaviour to the original FSM—only the code has become cleaner. The counter reset logic is now encapsulated within CounterExpired, reducing duplication and potential errors.
Analysis
The impure function transfers the counter comparison and reset from multiple FSM states into a single subprogram. This change improves readability: CounterExpired(Seconds => 5) clearly conveys intent, whereas Counter = CounterVal(Seconds => 5) is more verbose.
While impure functions can obscure side effects if overused, keeping them local to a process ensures that the surrounding code remains deterministic and easy to audit. In practice, they are more frequently employed in testbenches where correctness is important but the risk of subtle bugs is lower than in synthesizable RTL.
Key Takeaways
- Impure functions can read or drive signals not listed as parameters.
- Declare them inside a process to leverage the local signal scope.
- Use them to encapsulate repetitive side‑effecting logic, improving code clarity and reducing duplication.
Proceed to the next tutorial to explore advanced VHDL techniques.
VHDL
- Leveraging In‑Process Procedures for Cleaner VHDL FSM Design
- Mastering VHDL Functions: A Practical Guide to Efficient Design
- Using Procedures in VHDL: Simplify Your Design with Reusable Code
- Mastering VHDL Port Map Instantiation: A Practical Guide
- Mastering the Case-When Statement in VHDL: Efficient Multiplexer Design
- Mastering Signed and Unsigned Types in VHDL: A Practical Guide
- Mastering VHDL Wait Statements: Wait On, Wait Until, and Combined Usage
- Mastering While Loops in VHDL: Dynamic Iteration Control
- Mastering For‑Loops in VHDL: A Practical Guide
- Mastering Loop and Exit Constructs in VHDL: A Practical Guide