Search code examples
vhdlxilinx

How to remove redundant processes in VHDL


I am unfortunately new to VHDL but not new to software development. What is the equivalency to functions in VHDL? Specifically, in the code below I need to debounce four push buttons instead of one. Obviously repeating my process code four times and suffixing each of my signals with a number to make them unique for the four instances is not the professional nor correct way of doing this. How do I collapse all this down into one process "function" to which I can "pass" the signals so I can excise all this duplicate code?

----------------------------------------------------------------------------------
-- Debounced pushbutton examples
----------------------------------------------------------------------------------
library IEEE;

use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity pushbutton is
    generic(
        counter_size : integer := 19           -- counter size (19 bits gives 10.5ms with 50MHz clock)
    );
    port(
        CLK    : in  std_logic;                -- input clock
        BTN    : in  std_logic_vector(0 to 3); -- input buttons
        AN     : out std_logic_vector(0 to 3); -- 7-segment digit anodes ports
        LED    : out std_logic_vector(0 to 3)  -- LEDs
    );
end pushbutton;

architecture pb of pushbutton is
    signal flipflops0   : std_logic_vector(1 downto 0);                               -- input flip flops
    signal flipflops1   : std_logic_vector(1 downto 0);
    signal flipflops2   : std_logic_vector(1 downto 0);
    signal flipflops3   : std_logic_vector(1 downto 0);

    signal counter_set0 : std_logic;                                                  -- sync reset to zero
    signal counter_set1 : std_logic;
    signal counter_set2 : std_logic;
    signal counter_set3 : std_logic;

    signal counter_out0 : std_logic_vector(counter_size downto 0) := (others => '0'); -- counter output
    signal counter_out1 : std_logic_vector(counter_size downto 0) := (others => '0');
    signal counter_out2 : std_logic_vector(counter_size downto 0) := (others => '0');
    signal counter_out3 : std_logic_vector(counter_size downto 0) := (others => '0');

    signal button0      : std_logic;                                                  -- debounce input
    signal button1      : std_logic;
    signal button2      : std_logic;
    signal button3      : std_logic;

    signal result0      : std_logic;                                                  -- debounced signal
    signal result1      : std_logic;
    signal result2      : std_logic;
    signal result3      : std_logic;

begin

    -- Make sure Mercury BaseBoard 7-Seg Display is disabled (anodes are pulled high)
    AN <= (others => '1');

    -- Feed buttons into debouncers
    button0 <= BTN(0);
    button1 <= BTN(1);
    button2 <= BTN(2);
    button3 <= BTN(3);

    -- Start or reset the counter at the right time
    counter_set0 <= flipflops0(0) xor flipflops0(1);
    counter_set1 <= flipflops1(0) xor flipflops1(1);
    counter_set2 <= flipflops2(0) xor flipflops2(1);
    counter_set3 <= flipflops3(0) xor flipflops3(1);

    -- Feed LEDs from the debounce circuitry
    LED(0) <= result0;
    LED(1) <= result1;
    LED(2) <= result2;
    LED(3) <= result3;

    -- Debounce circuit 0
    process (CLK)
    begin
        if (CLK'EVENT and CLK = '1') then
            flipflops0(0) <= button0;
            flipflops0(1) <= flipflops0(0);
            if (counter_set0 = '1') then                  -- reset counter because input is changing
                counter_out0 <= (others => '0');
            elsif (counter_out0(counter_size) = '0') then -- stable input time is not yet met
                counter_out0 <= counter_out0 + 1;
            else                                         -- stable input time is met
                result0 <= flipflops0(1);
            end if;
        end if;
    end process;

    -- Debounce circuit 1
    process (CLK)
    begin
        if (CLK'EVENT and CLK = '1') then
            flipflops1(0) <= button1;
            flipflops1(1) <= flipflops1(0);
            if (counter_set1 = '1') then                  -- reset counter because input is changing
                counter_out1 <= (others => '0');
            elsif (counter_out1(counter_size) = '0') then -- stable input time is not yet met
                counter_out1 <= counter_out1 + 1;
            else                                         -- stable input time is met
                result1 <= flipflops1(1);
            end if;
        end if;
    end process;

    -- Debounce circuit 2
    process (CLK)
    begin
        if (CLK'EVENT and CLK = '1') then
            flipflops2(0) <= button2;
            flipflops2(1) <= flipflops2(0);
            if (counter_set2 = '1') then                  -- reset counter because input is changing
                counter_out2 <= (others => '0');
            elsif (counter_out2(counter_size) = '0') then -- stable input time is not yet met
                counter_out2 <= counter_out2 + 1;
            else                                         -- stable input time is met
                result2 <= flipflops2(1);
            end if;
        end if;
    end process;

    -- Debounce circuit 3
    process (CLK)
    begin
        if (CLK'EVENT and CLK = '1') then
            flipflops3(0) <= button3;
            flipflops3(1) <= flipflops3(0);
            if (counter_set3 = '1') then                  -- reset counter because input is changing
                counter_out3 <= (others => '0');
            elsif (counter_out3(counter_size) = '0') then -- stable input time is not yet met
                counter_out3 <= counter_out3 + 1;
            else                                         -- stable input time is met
                result3 <= flipflops3(1);
            end if;
        end if;
    end process;

end pb;


Solution

  • VHDL has functions but function calls are expressions and not statements or expression statements as in some programming languages. A function call always returns a value of a type and an expression can't represent a portion of a design hierarchy.

    Consider the other subprogram class procedures which are statements instead.

    The debouncer processes and associated declarations can also be simplified without using a procedure:

    library ieee;
    use ieee.std_logic_1164.all;
    use ieee.numeric_std.all;
    
    entity pushbutton is
        generic (
            counter_size:  integer := 19 -- The left bound of debounce counters
        );
        port (
            clk:     in  std_logic;
            btn:     in  std_logic_vector(0 to 3);
            an:      out std_logic_vector(0 to 3);
            led:     out std_logic_vector(0 to 3)
        );
    end entity pushbutton;
    
    architecture pb1 of pushbutton is
        -- There are two flip flops for each of four buttons:
        subtype buttons is std_logic_vector(0 to 3);
        type flip_flops is array (0 to 1) of buttons;
        signal flipflops:   flip_flops;
        signal counter_set: std_logic_vector(0 to 3);
        use ieee.numeric_std.all; 
        type counter is array (0 to 3) of
                            unsigned(counter_size downto 0);
        signal counter_out: counter := (others => (others => '0'));
    begin
        an <= (others => '1');
        counter_set <= flipflops(0) xor flipflops(1);
    
    DEBOUNCE:
        process (clk)
        begin
            if rising_edge (clk) then
                flipflops(0) <= btn;
                flipflops(1) <= flipflops(0);
                for i in 0 to 3 loop
                    if counter_set(i) = '1' then
                        counter_out(i) <=  (others => '0');
                    elsif counter_out(i)(counter_size) = '0' then
                        counter_out(i) <= counter_out(i) + 1;
                    else
                        led(i) <= flipflops(1)(i);
                    end if;
                end loop;
            end if;
        end process;
    end architecture pb1;
    

    Moving part of the design specification into a procedure:

    architecture pb2 of pushbutton is
        -- There are two flip flops for each of four buttons:
        subtype buttons is std_logic_vector(0 to 3);
        type flip_flops is array (0 to 1) of buttons;
        signal flipflops:   flip_flops;
        signal counter_set: std_logic_vector(0 to 3);
        use ieee.numeric_std.all;
        type counter is array (0 to 3) of
                            unsigned(counter_size downto 0);
        signal counter_out: counter := (others => (others => '0'));
        procedure debounce (
        -- Can eliminate formals of mode IN within the scope of their declaration:
            -- signal counter_set:    in  std_logic_vector (0 to 3);
            -- signal flipflops:      in  flip_flops;
            signal counter_out:    inout counter;
            signal led:             out std_logic_vector(0 to 3)
            ) is
        begin
            for i in 0 to 3 loop
                if counter_set(i) = '1' then
                    counter_out(i) <=  (others => '0');
                elsif counter_out(i)(counter_size) = '0' then
                    counter_out(i) <= counter_out(i) + 1;
                else
                    led(i) <= flipflops(1)(i);
                end if;
            end loop;
        end procedure;
    begin
        an <= (others => '1');
        counter_set <= flipflops(0) xor flipflops(1);
    
    DEBOUNCER:
        process (clk)
    
        begin
            if rising_edge (clk) then
                flipflops(0) <= btn;
                flipflops(1) <= flipflops(0);
                -- debounce(counter_set, flipflops, counter_out, led);
                debounce (counter_out, led);
            end if;
        end process;
    end architecture pb2;
    

    Here the procedure serves as a collection of sequential statements and doesn't save any lines of code.

    Sequential procedure calls can be useful to hide repetitious clutter. The clutter has been removed already by consolidating declarations and using the loop statement. There's a balancing act between the design entry effort, code maintenance effort and user readability, which can also be affected by coding style. Coding style is also affected by RTL constructs implying hardware.

    Moving the clock evaluation into a procedure would require the procedure call be be a concurrent statement, similar to an instantiation, which you already have. It doesn't seem worthwhile here should you consolidate signals declared as block declarative items in the architecture body or when using a loop statement.

    Note that result and button declarations have been eliminated. Also the use of package numeric_std and type unsigned for the counters prevents inadvertent assignment to other objects with the same subtype. The counter values are treated as unsigned numbers while counter_set for instance is not.

    Also there's an independent counter for each input being debounced just as in the original. Without independent counters some events might be lost for independent inputs when a single counter is repetitively cleared.

    This code hasn't been validated by simulation, lacking a testbench. With the entity both architectures analyze and elaborate.

    There doesn't appear to be anything here other than sequential statements now found in a for loop that would benefit from a function call. Because a function call returns a value the type of that value would either need to be a composite (here a record type) or be split into separate function calls for each assignment target.

    There's also the generate statement which can elaborate zero or more copies of declarations and concurrent statements (here a process) as block statements with block declarative items. Any signal used only in an elaborated block can be a block declarative item.

    architecture pb3 of pushbutton is
    begin
    DEBOUNCERS:
        for i in btn'range generate
            signal flipflops:       std_logic_vector (0 to 1);
            signal counter_set:     std_logic;
            signal counter_out:     unsigned (counter_size downto 0) := 
                                                                (others => '0');
        begin
            counter_set <= flipflops(0) xor flipflops(1);
    DEBOUNCE:
            process (clk)
            begin
                if rising_edge (clk) then
                    flipflops(0) <= btn(i);
                    flipflops(1) <= flipflops(0);
                    if counter_set = '1' then
                        counter_out <=  (others => '0');
                    elsif counter_out(counter_size) = '0' then
                        counter_out <= counter_out + 1;
                    else
                        led(i) <= flipflops(1);
                    end if;
                end if;
            end process;
        end generate;
    end architecture pb3;
    

    Addendum

    The OP pointed out an error made in the above code due to a lack of simulation and complexity hidden by abstraction when synthesizing architecture pb2. While the time for the debounce counter was given at 10.5 ms (50 MHz clock) the name of the generic (counter_size) is also actually the left bound of the counter (given as an unsigned binary counter using type unsigned).

    The mistake (two flip flops in the synchronizer for each of four buttons) and simply acceding to the OP's naming convention with respect to the counter has been corrected in the above code.

    The OP's synthesis error in the comment relates to the requirement there be a matching element for each element on the left hand or right hand of an aassignment statement.

    Without synthesizing the code (which the OP did) the error can't be found without simulation. Because the only thing necessary to find the particular error assigning flipflops(0) is the clock a simple testbench can be written:

    use ieee.std_logic_1164.all;
    entity pushbutton_tb is
    end entity;
    
    architecture fum of pushbutton_tb is
        signal clk:     std_logic := '0';
        signal btn:     std_logic_vector (0 to 3);
        signal an:      std_logic_vector(0 to 3);
        signal led:     std_logic_vector(0 to 3);
    begin
    CLOCK:
        process
        begin
            wait for 0.5 ms;
            clk <= not clk;
            if now > 50 ms then
                wait;
            end if;
        end process;
    
    DUT:
        entity work.pushbutton (pb2)
            generic map (
                counter_size =>  4  -- FOR SIMULATION
            )
            port map (
                clk => clk,
                btn => btn,
                an => an,
                led => led
            );
    
    STIMULUS:
        process
        begin
            btn <= (others => '0');
            wait for 20 ms;
            btn(0) <= '1';
            wait for 2 ms;
            btn(1) <= '1';
            wait for 3 ms;
            btn(2) <= '1';
            wait for 6 ms;
            btn(3) <= '1';
            wait;
        end process;
    end architecture;
    

    The corrected code and a testbench to demonstrate there are no matching element errors in assignment during simulation.

    Simulation was provided for both architectures with identical results.

    The generic was used to reduce the size of the debounce counters using a 1 millisecond clock in the testbench (to avoid simulation time with 50 MHz clock events that don't add to the narrative).

    Here's the output of the first architecture's simulation:

    pushbutton_tb_pb2

    The caution here is that designs should be simulated. There's a class of VHDL semantic error conditions that are checked only at runtime (or in synthesis).

    Added abstraction for reducing 'uniquified' code otherwise identically performing can introduce such errors.

    The generate statement wouldn't have that issue using names in a design hierarchy:

    pushbutton_tb_generate.jpg

    The concurrent statements and declarations found in a generate statement are replicated in any generated block statements implied by the generate statement. Each block statement represents a portion of a design hierarchy.

    There's been a trade off between design complexity and waveform display organization for debugging.

    A design description depending on hiding repetitious detail should be simulated anyway. Here there are two references to the generate parameter i used in selected names, susceptible to the same errors as ranges should parameter substitution be overlooked.