example project of high level vhdl

There is an example project that includes build scripts for building the project using Efinix Efinity, Lattice Diamond, Intel Quartus and Xilinx Vivado. The example project is tested with FPGA. The build scripts allow a single command to build the project with any of the tools.

The example project uses uart, multiplier, floating point math and the internal bus. The design creates a noisy sine wave and a fixed and a floating point filters to clean the noise from the the sine. The sine, noise, and fixed and floating point filtered versions of the noisy sine are then connected to an internal bus. The bus is connected to an uart which allows communication between the FPGA and a PC. The communication allows streaming the register pointed by a number that is obtained from the UART, thus any register connected to the bus is readable from the uart with a PC.

See the example project here : https://github.com/hVHDL/hVHDL_example_project

Example Project

The project top module is called ‘top’. Intel, Lattice and Xilinx tools use similar top files that instantiate the main clocks and connects signals from project into the physical IO signals in the top file. With Efinix tools the PLL is managed by the build system, thus Efinix tools manage this PLL layer of the design. The efinix_top.vhd file is common for all builds which reduces the need to manage multiple different versions of the project main IO routing layer.

Diamond, Quartus and Vivado top file
library ieee;
    use ieee.std_logic_1164.all;
    use ieee.numeric_std.all;
    use ieee.math_real.all;
	
entity top is
    port (
        clk     : in std_logic ;
        uart_rx : in std_logic ;
        uart_tx : out std_logic
    );
end entity top;
    
architecture rtl of top is

--------------------------------------------------
    component main_clock is
        port ( 
            CLKI: in  std_logic; 
            CLKOP: out  std_logic);
    end component;
--------------------------------------------------

    signal clock_120mhz : std_logic := '0';
    
--------------------------------------------------
begin

--------------------------------------------------
    u_main_clocks : main_clock
    port map(clk, clock_120mhz);
--------------------------------------------------
    u_hvhdl_example : entity work.efinix_top
    port map(
        clock_120mhz => clock_120mhz,
        uart_rx     => uart_rx,
        uart_tx     => uart_tx);

--------------------------------------------------
end rtl;

The efinix top module instantiates the main system interconnect module which instantiates the main design modules and connects them together . The first interesting design feature is visible in the IO names of the routed uart IO signal. The reason for the long names of the uart sources is that the IO are routed through the design using records. The use of records allows syntax checking to catch signal routing bugs in the design. If we change a module with IO, then the syntax checking will see that the name of the module is changed and flags an error if we did not remember to change the signals accordingly.

Efinix top
library ieee;
    use ieee.std_logic_1164.all;
    use ieee.numeric_std.all;

entity efinix_top is
    port (
        clock_120mhz : in std_logic;
        uart_rx      : in std_logic;
        uart_tx      : out std_logic
    );
end entity efinix_top;

architecture rtl of efinix_top is

begin

--------------------------------------------------
    u_hvhdl_example : entity work.hvhdl_example_interconnect
    port map(
        system_clock => clock_120mhz,
        hvhdl_example_interconnect_FPGA_in.communications_FPGA_in.uart_FPGA_in.uart_transreceiver_FPGA_in.uart_rx_fpga_in.uart_rx      => uart_rx,
        hvhdl_example_interconnect_FPGA_out.communications_FPGA_out.uart_FPGA_out.uart_transreceiver_FPGA_out.uart_tx_fpga_out.uart_tx => uart_tx);

end rtl;

Noisy sine generation

The High level coding patterns are visible in the sine generation. The process that creates a sine, noise and 100kHz time level for the signal and filtering is shown below. In the process we create a multiplier, a sincos module that calculates both sine and cosine of an angle and initialize the internal bus node as well as the fixed and floating point filter modules. The full source code can be found here https://github.com/hVHDL/hVHDL_example_project/blob/main/source/hvhdl_example_interconnect/hvhdl_example_interconnect_pkg.vhd

The process creates as down counter, that counts from 1199 to zero. This corresponds to 100kHz time level at 120MHz system clock. When the counter is zero, the sine calculation is requested and both noise and angle are updated. When sine calculation is ready as indicated by the sincos_is_ready function call returning true, the fixed and floating point filters are requested.

sine and timelevel generation
create_noisy_sine : process(system_clock)
begin
    if rising_edge(system_clock) then
        create_multiplier(multiplier);
        create_sincos(multiplier , sincos);

        init_example_filter(floating_point_filter_in);
        init_example_filter(fixed_point_filter_in);

        init_bus(bus_from_interconnect);
        connect_read_only_data_to_address(bus_from_master, bus_from_interconnect, 100, get_sine(sincos)/2 + 32768);
        connect_read_only_data_to_address(bus_from_master, bus_from_interconnect, 101, angle);
        connect_read_only_data_to_address(bus_from_master, bus_from_interconnect, 102, to_integer(signed(prbs7))+32768);
        connect_read_only_data_to_address(bus_from_master, bus_from_interconnect, 103, sine_with_noise/2 + 32768);

        if i > 0 then
            i <= (i - 1);
        else
            i <= 1199;
        end if;

        if i = 0 then
            request_sincos(sincos, angle);
            angle    <= (angle + 10) mod 2**16;
            prbs7    <= prbs7(5 downto 0) & prbs7(6);
            prbs7(6) <= prbs7(5) xor prbs7(0);
        end if;

        if sincos_is_ready(sincos) then
            sine_with_noise <= get_sine(sincos) + to_integer(signed(prbs7)*64);
            request_example_filter(floating_point_filter_in, sine_with_noise);
            request_example_filter(fixed_point_filter_in, sine_with_noise);
        end if;

    end if; --rising_edge
end process testi;	

---------------
    u_floating_point_filter : entity work.example_filter_entity(float)
        generic map(filter_time_constant => filter_time_constant)
        port map(system_clock, floating_point_filter_in, bus_from_master, bus_from_floating_point_filter);

---------------
    u_fixed_point_filter : entity work.example_filter_entity(fixed_point)
        generic map(filter_time_constant => filter_time_constant)
        port map(system_clock, fixed_point_filter_in, bus_from_master, bus_from_fixed_point_filter);

------------------------------------------------------------------------
------------------------------------------------------------------------

Sine, noise, angle and noisy sine are all connected to the internal bus. This is done with a procedure call to connect_read_only_data_to_address. The arguments are the in and out directional bus, address to which the data is readable from and then the data which is connected to the bus. The bus is made to be 16 bit wide, making that the numbers are between 0 and 65535, thus 32768 is added to the signals in order to fit negative numbers into the 16 bit number range.

Filter interface

The filters have a common entity. Since both the fixed and floating point filter take in the noisy sine and then connect the filtered sine into the internal bus, we use a common source file for both. The example_filter_entity.vhd has a package that defines a record and two interface subroutines. These subroutines are init_filter_example and request_filter_example, which allows an abstract interface to the module. With the use of this record, the module which uses the filter does not need to have accurate description on how the filterin is actually used. This allows also changing the interface if this is needed.

filter interface functions and entity description
package example_filter_entity_pkg is

    type example_filter_input_record is record
        filter_is_requested : boolean;
        filter_input        : integer;
    end record;

    constant init_example_filter_input : example_filter_input_record := (false, 0);

--------------------------------------------------
    procedure init_example_filter (
        signal example_filter_input : out example_filter_input_record);

--------------------------------------------------
    procedure request_example_filter (
        signal example_filter_input : out example_filter_input_record;
        data : in integer);
--------------------------------------------------
end package example_filter_entity_pkg;

------------------------------------------------------------------------
------------------------------------------------------------------------
library ieee;
    use ieee.std_logic_1164.all;
    use ieee.numeric_std.all;

    use work.fpga_interconnect_pkg.all;

    use work.example_filter_entity_pkg.all;

entity example_filter_entity is
    generic(filter_time_constant : real);
    port (
        clock : in std_logic;
        example_filter_input : in example_filter_input_record;
        bus_in              : in fpga_interconnect_record;
        bus_out             : out fpga_interconnect_record
    );
end entity example_filter_entity;

Floating point filter implementation

The actual filter implementation is in the two alternate architectures of the filter interface. Since the noisy sine is calculated using fixed point, the floating point filter first transforms the integer to floating point number, then filters the signal and then transforms the result back to fixed point and lastly connects the result to the internal bus.

The floating point module is accessed with call to the create_float_alu, create_denormalizer and create normalizer procedures with assciated signals as arguments. The float alu implements floating point add and subtract functions and a floating point multiplier. The created normalizer and denormalizer are used for transforming between integer and floating point numbers. The actual filter calculation calculates a first order filter using y <= y + (u-y) * filter_gain.

floating point filter implementation
floating_point_filter : process(clock)
    begin
        if rising_edge(clock) then
            init_bus(bus_out);
            connect_read_only_data_to_address(bus_in, bus_out, 106, get_mantissa(get_filter_output(float_filter)));
            connect_read_only_data_to_address(bus_in, bus_out, 107, get_exponent(get_filter_output(float_filter)));
            connect_read_only_data_to_address(bus_in, bus_out, 108, get_integer(denormalizer) + 32768);

            create_float_alu(float_alu);
            create_denormalizer(denormalizer);
            create_normalizer(normalizer);

            if example_filter_input.filter_is_requested then
                to_float(normalizer, example_filter_input.filter_input, 15);
            end if;

            if normalizer_is_ready(normalizer) then
                request_float_filter(float_filter, get_normalizer_result(normalizer));
            end if;

            request_scaling(denormalizer, get_filter_output(float_filter), 14);

        ------------------------------------------------------------------------
            filter_is_ready <= false;
            CASE filter_counter is
                WHEN 0 => 
                    subtract(float_alu, u, y);
                    filter_counter <= filter_counter + 1;
                WHEN 1 =>
                    if add_is_ready(float_alu) then
                        multiply(float_alu  , get_add_result(float_alu) , filter_gain);
                        filter_counter <= filter_counter + 1;
                    end if;

                WHEN 2 =>
                    if multiplier_is_ready(float_alu) then
                        add(float_alu, get_multiplier_result(float_alu), y);
                        filter_counter <= filter_counter + 1;
                    end if;
                WHEN 3 => 
                    if add_is_ready(float_alu) then
                        y <= get_add_result(float_alu);
                        filter_counter <= filter_counter + 1;
                        filter_is_ready <= true;
                    end if;
                WHEN others =>  -- wait for start
            end CASE;
        ------------------------------------------------------------------------

        end if; --rising_edge
    end process floating_point_filter;	

Fixed point filter implementation

The fixed point filter is similarly implemented in its own architecture. The fixed point filter process similarly to the floating point implementation creates a fixed point filter. The filter needs a multiplier and the actual filter record type signal. The create_first_order_filter procedure call creates the state machines that are needed for the fixed point filter calculation. The implementation of the fixed point filter can be found in the math library https://github.com/hVHDL/hVHDL_math_library/blob/main/first_order_filter/first_order_filter_pkg.vhd

The fixed point filter uses a slightly different implementation of the filter than the floating point one, but resulting response is the same.

fixed point filter implementation
fixed_point_filter : process(clock)
begin
    if rising_edge(clock) then
        init_bus(bus_out);
        connect_read_only_data_to_address(bus_in, bus_out, 104, get_filter_output(filter)/2 + 32678);
        connect_read_only_data_to_address(bus_in, bus_out, 105, scaled_sine/2 + 32678);
        create_multiplier(multiplier2);
        create_first_order_filter(filter => filter , multiplier => multiplier2 , time_constant => filter_time_constant);

        if example_filter_input.filter_is_requested then
            filter_data(filter, example_filter_input.filter_input);
            process_counter <= 0;
        end if;

        CASE process_counter is
            WHEN 0 =>
                if filter_is_ready(filter) then
                    multiply(multiplier2, get_filter_output(filter), integer(32768.0*3.3942));
                    process_counter <= process_counter + 1;
                end if;
            WHEN 1 => 
                if multiplier_is_ready(multiplier2) then
                    scaled_sine <= get_multiplier_result(multiplier2, 15);
                    process_counter <= process_counter + 1;
                end if;
            WHEN others => -- wait for start
        end CASE;

    end if; --rising_edge
end process;	

Filtering results

The signals are captured using uart and plotted into figures with matlab. The original sine, noise and combined noisy sine are shown below

../_images/original_sine.png ../_images/noise.png ../_images/noisy_sine.png

Since the fixed and floating point implmentations of the filters have the same response, the filtering results are also almost identical.

../_images/filtered_sine.png

The difference between fixed and floating point can be seen in the zoomed in figures shown below. The fixed point figure is first

../_images/fixed_point_filter_closeup.png ../_images/floating_point_filter_closeup.png

Synthesis results

The synthesis results are shown in figure below the ecp, efinix and intel resource use including both luts and multipliers are quite similar due to 4 input lookup tables. Spartan 7 has six input luts and the compiler manages to use a lot of the 6 input luts, thus the resource use is somewhat smaller. All of the designs meet timing at 120MHz.

../_images/ecp_synthesis_result.png ../_images/efinix_synthesis_result.png ../_images/intel_synthesis_result.PNG ../_images/vivado_synthesis_result.PNG