hardware descriptions

Bandpass and Band stop Filtering with VHDL

get the VHDL sources

https://github.com/johonkanen/ac_inout_psu/tree/bandpass_filter

To minimize code rewriting and maximize reuse, object oriented programming languages use inheritance to add existing module features to new modules. This type of code is much easier to reuse as parts of further modules.

In VHDL inheritance like code structure can be achieved by instantiating components inside components and by using processes and functions on signals created from records. Since both functions and procedures can be nested and records can contain records as members, we can create a procedure which builds on functionalities from previously created records. This allows larger and more complex functions to be created from smaller and simpler ones that are easier to test and reuse. This is a great way to significantly increase the level of abstraction and expressiveness of the VHDL source code.

With VHDL the abstractions come at very low cost. As the VHDL source code is actually describing hardware and the end product is a synthesized logic circuit there is no overhead associated with nested function or procedure calls. Thus in VHDL applications, abstractions come at no penalty on performance or resource use. In fact it is much easier to share functionality between modules when high level of abstraction is used and thus most likely less resources are consumed!

When testing a multiplier with uart, a first order filter was designed previously. To make the filter easier to use we refactor the filter into an instantiable object. This is accomplished by using a record and then defining procedures for creating and using the filter through the record type signal. Also a constant for initializing the filter signal is provided.

				
					library math_library;
    use math_library.multiplier_pkg.all;
 
package first_order_filter_pkg is
 
------------------------------------------------------------------------
    type first_order_filter is record
        process_counter : natural range 0 to 15;
        filter_is_ready : boolean;
        filter_is_busy : boolean;
        filter_input : int18;
        filter_output : int18;
        filter_memory : int18; 
    end record;
 
    constant init_filter_state : first_order_filter := (process_counter => 9, filter_is_ready => false, filter_is_busy => false, filter_input => 0, filter_output => 0, filter_memory => 0); 
 
end package first_order_filter_pkg;
				
			

The previously designed filter is moved into a create_first_order_filter procedure, which builds the filter structure

				
					procedure create_first_order_filter
(
    signal filter : inout first_order_filter;
    signal multiplier : inout multiplier_record;
    constant b0 : int18;
    constant b1 : int18
) is
    constant a1 : int18 := 2**17-1-b1-b0;
    alias filter_is_ready is filter.filter_is_ready;
    alias filter_is_busy  is filter.filter_is_busy;
    alias process_counter is filter.process_counter;
    alias filter_memory   is filter.filter_memory;
    alias filter_input    is filter.filter_input;
    alias filter_output   is filter.filter_output;
    variable y : int18;
begin
        filter_is_ready <= false;
        filter_is_busy <= true;
        CASE process_counter is
            WHEN 0 =>
                multiply(multiplier, filter_input, b0);
                process_counter <= process_counter + 1;
 
            WHEN 1 =>
                multiply(multiplier, filter_input, b1);
                process_counter <= process_counter + 1;
 
            WHEN 2 =>
                if multiplier_is_ready(multiplier) then
                    y := filter_memory + get_multiplier_result(multiplier, 17);
                    filter_output <= y;
                    multiply(multiplier, y, a1);
                    process_counter <= process_counter + 1;
                end if;
 
            WHEN 3 =>
                filter_memory <= get_multiplier_result(multiplier, 17);
                process_counter <= process_counter + 1;
 
            WHEN 4 => 
 
                if multiplier_is_ready(multiplier) then
                    filter_memory <= filter_memory + get_multiplier_result(multiplier, 17);
                    process_counter <= process_counter + 1;
                    filter_is_ready <= true;
                end if;
 
            WHEN others => -- do nothing
                filter_is_busy <= false;
 
        end CASE; 
     
end create_first_order_filter;
				
			

With the filter done this way, we can easily share the multiplier module with several filters or other functionalities and use the filter object as part of other filters. The interface of the filter object is defined with filter_data procedure, which triggers the filtering, filter_is_ready function to check for the filter being ready and get_filter_output procedure to fetch the filtered data

				
					--------------------------------------------------
    procedure create_first_order_filter (
        signal filter : inout first_order_filter;
        signal multiplier : inout multiplier_record;
        constant b0 : int18;
        constant b1 : int18);
--------------------------------------------------
    procedure filter_data (
        signal filter : out first_order_filter;
        data_to_filter : in int18);
--------------------------------------------------
    function get_filter_output ( filter : in first_order_filter)
        return int18; 
------------------------------------------------------------------------
    function filter_is_ready ( filter : first_order_filter)
        return boolean;
------------------------------------------------------------------------
				
			

With the filter implementation moved inside a first_order_filter_package, filtering can be added by simply declaring signals from first_order_filter and multiplier records and adding the create_first_order_filter procedure call to the process where filtering is done. Because the create filter procedure takes the multiplier record as its argument, the multiplier can be shared in the process where filtering is used.

With the filter object, the filtering can be created with just 4 additional lines of code!

				
					-- architecture declaration 
    signal first_order_filter : first_order_filter_record;
    signal multiplier : multiplier_record;
 
begin
    process(clock)
        if rising_edge(clock) then
            create_multiplier(multiplier);
            create_first_order_filter(multiplier, first_order_filter);
....
            -- trigger filter with filter(first_order_filter, data);
            -- get first order filter output with get_filter_output(first_order_filter)
        end if;
				
			

To demonstrate the inheritance structure a band pass filter is created using the newly created low pass filter object. To do this, first a new record is defined which has two first order filters and the multiplier for the filters.

				
					------------------------------------------------------------------------
    type bandpass_filter_record is record
        high_pass_filter : first_order_filter;
        low_pass_filter : first_order_filter;
        multiplier : multiplier_record;
    end record;
				
			

Then, a create_bandpass_filter procedure is defined which creates a bandpass filter by calling the procedure which creates the first order filters and the multiplier

				
					------------------------------------------------------------------------
    procedure create_bandpass_filter
    (
        signal bandpass_filter : inout bandpass_filter_record
    ) is
    begin
        create_multiplier(bandpass_filter.multiplier);
        create_first_order_filter(bandpass_filter.low_pass_filter, bandpass_filter.multiplier, 50, 3e2);
        create_first_order_filter(bandpass_filter.high_pass_filter, bandpass_filter.multiplier, 1500, 3200);
        if filter_is_ready(bandpass_filter.low_pass_filter) then
            filter_data(bandpass_filter.high_pass_filter, bandpass_filter.low_pass_filter.filter_input - get_filter_output(bandpass_filter.low_pass_filter));
        end if; 
    end create_bandpass_filter;
 
------------------------------------------------------------------------
				
			

The bandpass filter uses the two filters, one with lower bandwidth gains and one with high bandwidth gains in a sequence. The input data is first low pass filtered with the lower bandwidth filter and then high pass version of the input data is passed through the high bandwidth filter.

With the multiplier embedded in the bandpass filter object, only a single signal declaration for the bandpass_filter_record is needed to create the bandpass filter in an application code.

The bandpass filter and the lowpass filters are compiled and uploaded to cyclone 10 lp FPGA and tested with UART. The test code decrements a 100kHz counter. Counter at zero triggers an analog to digital converter, which when ready triggers the filtering and uart data transmision. Data received from uart is used to choose whether the low pass filtered data, high pass filtered data or bandpass filter output is transmitted with uart.

The data is captured with PC. Since the data passed through uart is 16 bit unsigned, the result is divided by 2 and an offset of 32768 is added to the uart data transmission for the high pass and bandpass filtered data

The input data with the different filters are captured with uart and plotted to see the different performance attainable with different filters. The bandpass and high pass filters can be seen to have an offset of 32768, which is added to make the data plot to behave correctly with the used 16 bit unsigned data accessed with uart. The high pass is simply the low pass filter subtracted from the input data and band stop filter is obtained by subtracting the bandstop filtered data from the input.

				
					 --- architectural part of the system_component module
    signal bandpass_filter : bandpass_filter_record;
 
--------------------------------------------------
begin
 
--------------------------------------------------
    test_with_uart : process(clock)
 
        --------------------------------------------------
        function get_square_wave_from_counter
        (
            counter_value : integer
        )
        return int18
        is
        begin
            if counter_value > 32767 then
                return 55e3;
            else
                return 15e3;
            end if;
        end get_square_wave_from_counter;
        --------------------------------------------------
         
    begin
        if rising_edge(clock) then
 
            create_bandpass_filter(bandpass_filter);
 
            idle_adc(spi_sar_adc_data_in);
            init_uart(uart_data_in);
            receive_data_from_uart(uart_data_out, uart_rx_data);
            system_components_FPGA_out.test_ad_mux <= integer_to_std(number_to_be_converted => uart_rx_data, bits_in_word => 3);
 
            uart_transmit_counter <= uart_transmit_counter - 1; 
            if uart_transmit_counter = 0 then
                uart_transmit_counter <= counter_at_100khz;
                start_ad_conversion(spi_sar_adc_data_in); 
            end if; 
 
            if ad_conversion_is_ready(spi_sar_adc_data_out) then
 
                CASE uart_rx_data is
                    WHEN 10 => transmit_16_bit_word_with_uart(uart_data_in, get_filter_output(bandpass_filter.low_pass_filter) );
                    WHEN 11 => transmit_16_bit_word_with_uart(uart_data_in, (bandpass_filter.low_pass_filter.filter_input - get_filter_output(bandpass_filter.low_pass_filter))/2+32768);
                    WHEN 12 => transmit_16_bit_word_with_uart(uart_data_in, get_filter_output(bandpass_filter)/2+32768);
                    WHEN 13 => transmit_16_bit_word_with_uart(uart_data_in, bandpass_filter.low_pass_filter.filter_input - get_filter_output(bandpass_filter));
                    WHEN 14 => transmit_16_bit_word_with_uart(uart_data_in, get_adc_data(spi_sar_adc_data_out));
                    WHEN others =>  transmit_16_bit_word_with_uart(uart_data_in, uart_rx_data); 
                end CASE;
 
                filter_data(bandpass_filter, get_square_wave_from_counter(test_counter));
                test_counter <= test_counter + 1; 
            end if;
 
        end if; --rising_edge
    end process test_with_uart; 
 
------------------------------------------------------------------------ 
    spi_sar_adc_clocks <= (clock => system_components_clocks.clock, reset_n => reset_n); 
    u_spi_sar_adc : spi_sar_adc
    port map( spi_sar_adc_clocks                          ,
          system_components_FPGA_in.spi_sar_adc_FPGA_in   ,
          system_components_FPGA_out.spi_sar_adc_FPGA_out ,
          spi_sar_adc_data_in                             ,
          spi_sar_adc_data_out);
 
------------------------------------------------------------------------ 
    uart_clocks <= (clock => system_components_clocks.clock);
    u_uart : uart
    port map( uart_clocks                          ,
          system_components_FPGA_in.uart_FPGA_in   ,
          system_components_FPGA_out.uart_FPGA_out ,
          uart_data_in                             ,
          uart_data_out);
 
------------------------------------------------------------------------ 
				
			

Lastly it should be noted that the complete design at this point with uart, spi adc driver and the access to the filter input data and its low pass, high pass, band stop and bandpass filtered versions consumes only 719 logic elements, 445 registers and two 9 bit multipliers and can meet timing up to 167 MHz core clock and the application code snippet with the complete process is only 78 lines of code

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top