Real-Time dynamic simulation with FPGA vol 1 : the space of states

Complete sources can be found on the projects github repository from ac_inout_psu/source/math_library/. The FPGA test code is written in /ac_inout_psu/source/system_control/system_components/system_components.vhd

Now with the ethernet communication established, next goal is to develop the power electronics control and protections. This is done against a hardware simulation model for the power electronics. The simulation takes in the modulator signals, feeds them into model of the system and then returns voltages and currents from the model as measurements for the control code.

Simulation in this case means modeling the power electronics using a set of differential equations, referred to as state equations, and numerically integrating them with the FPGA in real-time. Since everything starts with modeling the state-space model is explored first.

States in space

The state equations are actually the equations of motion that describe how the states move in their state-space. State equations are the way to describe any phenomena which is impacted by the magnitude of exitation as well as the length of time it is applied. Although power electronics is modeled here, the state equations do lend themselves to pretty much anything ranging from nuclear decay of atoms to trajectories of integalactic bodies to weather and the trends of the stock market.

Modeling power electronics

The models for power electronic converters are built using two differential equations, the voltage equation for the capacitor and the current equation for the inductor shown here

The way these are read is that the state of the system, here current of an inductor and voltage of a capacitor, changes at a rate that depends on the input applied to it. Since the model describes what the rate of change is, the actual values for the system state can be obtained by integrating the state equations using a numerical integrator.

And that actually it right there!

No further equations are needed, no complexity nor difficulties and no specific test hardware needed. Just press compile, wait for literally a minute and bang, Hardware’s in da loop! Thats a boat load of money saved on not burning devices right there!

Funnily enough this really is all that is required. These two equations are directly usable for simulation, can be directly input to matlab for formal feedback control design and the model can be verified against any response measured from the real system under investigation. Any symbolic mathematical software capable of matrix equations should be able to extract any and all transfer functions directly from the state-space model. In case of nonlinear dynamics the linearized model can also be calculated with math software. All that with just a few minutes of modeling effort!

Modeling LC filter

We can directly observe from the circuit of Figure 1 that the voltage over the inductor is the difference of input voltage and the sum of capacitor voltage and voltage drop over the series resistance. The current of the capacitor is just the difference of input current and load current.

Figure 1. LCR circuit under serious investigation

This yields the following model.

As can be seen from the model, the rate of change is multiplied by the inverse of the inductor and capacitor. Hence, the smaller the inductor is the smaller the required voltage for a set amount of rate of change.

Multi stage filters are very common in inverters and power supplies since EMI filters are practically always used when grid connection is desired and much of the time even if it isn’t.

If we were to add another filter in series to form a LCLC filter we would simply write the equation of the additional LC filters after the first one. In this case the input voltage of the second inductor is the voltage of the first capacitor and the load current of the first capacitor is the current of the additional inductor. This is shown below

Further LC sections can be added in a similar fashion with little effort. As previously stated, this set of equations can be used directly in simulation. We simply need to numerically integrate the equations and plot the results.

Numerical integration

Integrator is a function which when calculated for a duration of one second with input of 1 produces an output of 1. Consequently, put input = 2 and after a second it outputs 2 and 0.5 gets 0.5.

When we numerically integrate, we simply run the function 1/ts times per second. If calculation interval ts is 1/10 of a second and we set input = 1 the output obtained is the sequence 1/10, 2/10, 3/10…and 1 is obtained after 10 rounds. Hence, integration is achieved. The simplest integrator is thus

Integrator = integrator + ts×input

With the simplest integrator, also known as forward euler integrator, the LC filter can be simulated by substituting the state equations into the input in the above integrator. This yields the following algorithm

I = I + ts*1/L*(uin - U - r1*I);
U = U + ts*1/C×(I-I_load);

A small trick is used here by just first integrating the current and using the integrated current with the capacitor equation. The current is therefore a one step ahead of the voltage equation. Although this might not seem to be all that significant, the difference in behavior is literally between stabilitity and instablility when r1 is zero or very close to it! Such is the fantastical world of numerical integration.

Since calculation time step ts and inductor gain 1/L are constants together they are referred as “gain”. Rewriting the code so that a separate integration step is used, the algorithm for the simulator is written as

r_i1 = i1*r1;
D_I = I_gain × (uin - U - r_i1);
I = I + D_I;
D_U = U_gain × (I - I_load)
U = U + D_U;

This equation is then coded with VHDL

Simulation of the LC filter with VHDL

The simulation model is designed using the multiplier that was designed previously and using the sequential_multiply procedure for the multiplication. With the sequential_multiply the model can be written in VHDL below with the lines corresponding to the algorithm above highlighted. Since the multiplier is synthesizable this could be compiled to FPGA if desired.

				
					clocked_reset_generator : process(simulator_clock, rstn)
begin
    if rising_edge(simulator_clock) then
        create_multiplier(hw_multiplier); 
        simulation_counter <= simulation_counter + 1;
        simulation_trigger_counter <= simulation_trigger_counter + 1;
        if simulation_trigger_counter = 19 then
            simulation_trigger_counter <= 0;
            process_counter <= 0;
        end if;
        if simulation_counter  mod 6725 = 0 then
            load_current <= -load_current;
        end if;
        CASE process_counter is
            WHEN 0 => 
                sequential_multiply(hw_multiplier, inductor_series_resistance, inductor_current);
                if multiplier_is_ready(hw_multiplier) then
                    inductor_current_delta <= get_multiplier_result(hw_multiplier, 15);
                    process_counter <= process_counter + 1;
                end if;
            WHEN 1 => 
                sequential_multiply(hw_multiplier, inductor_integrator_gain, input_voltage - capacitor_voltage); 
                if multiplier_is_ready(hw_multiplier) then
                    inductor_current <= get_multiplier_result(hw_multiplier, 15) + inductor_current - inductor_current_delta;
                    process_counter <= process_counter + 1;
                end if;
            WHEN 2 => 
                sequential_multiply(hw_multiplier, load_resistance, capacitor_voltage); 
                if multiplier_is_ready(hw_multiplier) then
                    capacitor_delta <= get_multiplier_result(hw_multiplier, 17);
                    process_counter <= process_counter + 1;
                end if;
            WHEN 3 =>
                sequential_multiply(hw_multiplier, capacitor_integrator_gain, inductor_current - load_current);
                if multiplier_is_ready(hw_multiplier) then
                    capacitor_voltage <= capacitor_voltage + get_multiplier_result(hw_multiplier, 15) - capacitor_delta;
                    process_counter <= process_counter + 1;
                end if;
            WHEN others => -- do nothing
        end CASE; 
    end if; -- rstn
end process clocked_reset_generator;    
				
			

To limit the required amount of messy code needed for a simulation model, the code is refactored into a state_variable type with a method for the integration.

State_variable

State variable is a record that has a methods for integration and initializing integrator gain. This allows more state equations to be added to a model by just declaring additional states with the init function and calling the integration method. State equation record type is declared as follows

				
					------------------------------------------------------------------------
package state_variable_pkg is
    type state_variable_record is record
        state           : int18;
        integrator_gain : int18;
    end record;
    function init_state_variable_gain ( integrator_gain : int18)
        return state_variable_record;
    constant init_state_variable : state_variable_record := (0, 0);
    procedure create_state_variable (
        signal state_variable : inout state_variable_record;
        integrator_gain : int18);
    procedure integrate_state (
        signal state_variable : inout state_variable_record;
        signal multiplier : inout multiplier_record;
        constant radix : in natural;
        state_equation : in int18);
end package state_variable_pkg;
------------------------------------------------------------------------
package body state_variable_pkg is
    function init_state_variable_gain
    (
        integrator_gain : int18
    )
    return state_variable_record
    is
        variable state_variable : state_variable_record;
    begin
        state_variable := (state => 0, integrator_gain => integrator_gain);
        return state_variable;
    end init_state_variable_gain;
    procedure create_state_variable
    (
        signal state_variable : inout state_variable_record;
        integrator_gain : int18
    ) is
    begin
        state_variable.integrator_gain <= integrator_gain;
    end create_state_variable;
--------------------------------------------------
    procedure integrate_state
    (
        signal state_variable : inout state_variable_record;
        signal multiplier : inout multiplier_record;
        constant radix : in natural;
        state_equation : in int18
    ) is
        alias integrator_gain is state_variable.integrator_gain;
    begin
        sequential_multiply(multiplier, integrator_gain, state_equation); 
        if multiplier_is_ready(multiplier) then
            state_variable.state <= get_multiplier_result(multiplier, radix) + state_variable.state;
        end if;
         
    end integrate_state;
------------------------------------------------------------------------
end package body state_variable_pkg;
				
			

Multiplier is also given as argument to the integrate state procedure, which allows sharing the multiplier with multiple states thus allowing the possibility for reusing a multiplier and reducing the resource usage. More multipliers can also be added if we wish to simultaneously evaluate multiple state equations.

We are not yet quite done though and there is still a bit more expressiveness that can be wringed out of VHDL by using an impure function for the multiplication in the resistance calculation stage.

impure function overload

The word impure in a function is the VHDL way of saying “aah, I’ll let ‘er pass, ye knows what ye doin”, as it literally allows any function to be used as a call for any function, procedure or set of operations as long a correct type is returned. In this case the impure overloading of the “*” operator is used.

With the impure “*” operator and use of the state_variable type, the LCR filter simulation model can be written as

				
					process(simulator_clock)
    impure function "*" ( left, right : int18)
    return int18
    is
    begin
        sequential_multiply(hw_multiplier, left, right);
        return get_multiplier_result(hw_multiplier, 15);
    end "*";
begin
    if rising_edge(simulator_clock) then
        create_multiplier(hw_multiplier); 
        create_multiplier(hw_multiplier2); 
        simulation_counter <= simulation_counter + 1;
        simulation_trigger_counter <= simulation_trigger_counter + 1;
        if simulation_trigger_counter = 40 then
            simulation_trigger_counter <= 0;
            process_counter <= 0;
        end if;
        input_voltage <= 32e2;
        if simulation_counter = 12000  then
            load_resistance <= 65e3;
        end if;
        CASE process_counter is
            WHEN 0 => 
                inductor_current_delta <= inductor_series_resistance * inductor_current.state;
                increment_counter_when_ready(hw_multiplier, process_counter);
            WHEN 1 => 
                integrate_state(inductor_current, hw_multiplier, input_voltage - capacitor_voltage.state - inductor_current_delta);
                increment_counter_when_ready(hw_multiplier, process_counter);
            WHEN 2 => 
                capacitor_delta <= load_resistance * capacitor_voltage.state;
                increment_counter_when_ready(hw_multiplier, process_counter);
            WHEN 3 =>
                integrate_state(capacitor_voltage, hw_multiplier, inductor_current.state - load_current - capacitor_delta);
                increment_counter_when_ready(hw_multiplier, process_counter);
            WHEN others => -- do nothing
        end CASE; 
    end if; -- rstn
end process;    
				
			

Usually impure function is not a reasonable use of the VHDL language as it is extremely error prone and easily makes up for unintuitive behaviour. For example here the returned value is the result of a previous multiplication until the called multiplication is done, however numeric simulation is error prone all by it self and in this case the increased readability of the code might be a reasonable trade-off.

With the state variable handling the integration, the code is already quite simple, but we can do even better. Similarly to what was done with the bandpass filter, the two states of the LC filter are wrapped into a new record to form a LCR filter object. This also allows limiting the scope of the wildly behaving “*” operator to inside a custom objects method.

LCR filter object

The LCR filter record has two states, namely the inductor current and capacitor voltage as well as the intermediate signals required for calculating the series resistance of the inductor and the process counter for the model calculation. Calculation of the LCR filter is triggered by just setting the process counter of the filter calculation to zero. The LCR filter is declared in its package as follows

				
					package lcr_filter_model_pkg is
------------------------------------------------------------------------
    type lcr_model_record is record
        inductor_current  : state_variable_record;
        capacitor_voltage : state_variable_record;
        process_counter   : natural range 0 to 7;
        inductor_current_delta     : int18;
        inductor_series_resistance : int18;
    end record;
    constant init_lcr_filter : lcr_model_record := 
            (inductor_current          => (0, 0) ,
            capacitor_voltage          => (0, 0) ,
            process_counter            => 4      ,
            inductor_current_delta     => 0      ,
            inductor_series_resistance => 950);
------------------------------------------------------------------------
    procedure create_lcr_filter (
        signal lcr_filter : inout lcr_model_record;
        signal multiplier : inout multiplier_record;
        inductor_current_state_equation : int18;
        capacitor_voltage_state_equation : int18 );
------------------------------------------------------------------------
    procedure calculate_lcr_filter (
        signal lcr_filter : inout lcr_model_record);
------------------------------------------------------------------------
    function init_lcr_model_integrator_gains (
        inductor_integrator_gain : int18;
        capacitor_integrator_gain : int18)
        return lcr_model_record;
------------------------------------------------------------------------
end package lcr_filter_model_pkg;
package body lcr_filter_model_pkg is
------------------------------------------------------------------------
    procedure create_lcr_filter
    (
        signal lcr_filter : inout lcr_model_record;
        signal multiplier : inout multiplier_record;
        inductor_current_state_equation : int18;
        capacitor_voltage_state_equation : int18
    ) is
        alias hw_multiplier is multiplier;
        alias process_counter is lcr_filter.process_counter;
        alias inductor_current_delta is lcr_filter.inductor_current_delta;
        alias inductor_series_resistance is lcr_filter.inductor_series_resistance;
        alias inductor_current is lcr_filter.inductor_current;
        alias capacitor_voltage is lcr_filter.capacitor_voltage;
    --------------------------------------------------
        impure function "*" ( left, right : int18)
        return int18
        is
        begin
            sequential_multiply(hw_multiplier, left, right);
            return get_multiplier_result(hw_multiplier, 15);
        end "*";
    --------------------------------------------------
    begin
            CASE process_counter is
                WHEN 0 => 
                    inductor_current_delta <= inductor_series_resistance * inductor_current.state; 
                    increment_counter_when_ready(hw_multiplier, process_counter);
                WHEN 1 => 
                    integrate_state(inductor_current, hw_multiplier, 15, inductor_current_state_equation - inductor_current_delta); -- input_voltage - capacitor_voltage.state - inductor_current_delta);
                    increment_counter_when_ready(hw_multiplier, process_counter);
                WHEN 2 =>
                    integrate_state(capacitor_voltage, hw_multiplier, 15, capacitor_voltage_state_equation);
                    increment_counter_when_ready(hw_multiplier, process_counter);
                WHEN others => -- do nothing
            end CASE; 
    end create_lcr_filter;
------------------------------------------------------------------------
    procedure calculate_lcr_filter
    (
        signal lcr_filter : inout lcr_model_record
    ) is
    begin
        lcr_filter.process_counter <= 0; 
    end calculate_lcr_filter;
------------------------------------------------------------------------
				
			

Now an LC filter can be created by instantiating a single signal and creating the LCR filter inside a clocked process. Now that the accidental complexity is hidden under the state variable type and aggregated LC filter type, the code is then synthesized and run with FPGA.

				
					 signal hw_multiplier1             : multiplier_record := multiplier_init_values;
    signal lcr_filter1 : lcr_model_record := init_lcr_model_integrator_gains(int18_inductor_integrator_gain, int18_capacitor_integrator_gain);
begin
---- inside process 
    process(clock)
    if rising_edge(clock) then
    -- create multiplier and lcr filter
    create_multiplier(hw_multiplier1); 
    create_lcr_filter(lcr_filter1 , hw_multiplier1 , input_voltage - lcr_filter1.capacitor_voltage.state , lcr_filter1.inductor_current.state - load_current);
    if calculation_is_triggered then
            calculate_lcr_filter(lcr_filter1);
    end if;
----
				
			

Test with FPGA

The previous filter test code developed while designing the band-pass filter is amended with the LC filter. The states of the LC filter are added as options 16 and 17 for signals that are streamed out with uart. When number 16 is transmitted to FPGA, the uart outputs the filter current and with 17 capacitor voltage is transmitted.

See the page navigation for link for steps needed for building the uart console software for receiving the input voltage and load current responses via uart.

The test code alternates between reversing the input voltage and load current every 32768 calculation cycles. The resulting response is captured with uart from the FPGA and is shown in Figures 2 and 3, which show classical second order oscillating response.

				
					signal hw_multiplier1             : multiplier_record := multiplier_init_values;
    signal load_current               : int18             := 3000;
    signal input_voltage              : int18             := 2e3;
    signal lcr_filter1 : lcr_model_record := init_lcr_model_integrator_gains(25e3, 2e3);
--------------------------------------------------
begin   
test_with_uart : process(clock)
    --------------------------------------------------
         
    begin
        if rising_edge(clock) then
            create_bandpass_filter(bandpass_filter);
            init_mdio_driver(mdio_driver_data_in);
            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
                calculate_lcr_filter(lcr_filter1); 
                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 15 => transmit_16_bit_word_with_uart(uart_data_in, uart_rx_data);
                    WHEN 16 => transmit_16_bit_word_with_uart(uart_data_in, lcr_filter1.inductor_current.state + 32768);
                    WHEN 17 => transmit_16_bit_word_with_uart(uart_data_in, lcr_filter1.capacitor_voltage.state+ 32768);
                    WHEN others => -- get data from MDIO
                end CASE; 
                if test_counter = 65535 then
                    test_counter <= 0;
                end if;
                if test_counter = 65536/2 then
                    input_voltage <= -input_voltage;
                end if;
                if test_counter = 65535 then
                    load_current <= -load_current;
                end if;
            end if;
            -------------------------------------------------- 
            create_multiplier(hw_multiplier1); 
            create_lcr_filter(lcr_filter1 , hw_multiplier1 , input_voltage - lcr_filter1.capacitor_voltage.state , lcr_filter1.inductor_current.state - load_current);
        end if; --rising_edge
    end process test_with_uart; 
				
			

Figure 2. LC filter inductor current response during current step simulated with FPGA

Figure 3.Capacitor voltage response of a LC filter during voltage step

A multistage filter is also tested by cascading 5 of the LC filter sections in the FPGA test code. Since the state equations are given as arguments to the create_lcr_filter procedure, this is made quite straightforward by use of the LC filter object. In a cascaded configuration a LC filter has its inductor current state equation as the difference of the previous LC filters capacitor voltage and it’s own capacitor voltage and the capacitor voltage equation is the difference of it’s own inductor current and the succeeding LC sections inductor current.

With the resulting 10th order LC filter, the voltage and current step responces of the last LC section show oscillation with several frequencies which are seen in Figures 4 and 5.

				
					signal hw_multiplier1             : multiplier_record := multiplier_init_values;
    signal hw_multiplier2             : multiplier_record := multiplier_init_values;
    signal hw_multiplier3             : multiplier_record := multiplier_init_values;
    signal hw_multiplier4             : multiplier_record := multiplier_init_values;
    signal hw_multiplier5             : multiplier_record := multiplier_init_values;
    signal load_current               : int18             := 3000;
    signal input_voltage              : int18             := 2e3;
    signal lcr_filter1 : lcr_model_record := init_lcr_model_integrator_gains(25e3, 2e3);
    signal lcr_filter2 : lcr_model_record := init_lcr_model_integrator_gains(25e3, 2e3);
    signal lcr_filter3 : lcr_model_record := init_lcr_model_integrator_gains(25e3, 2e3);
    signal lcr_filter4 : lcr_model_record := init_lcr_model_integrator_gains(25e3, 2e3);
    signal lcr_filter5 : lcr_model_record := init_lcr_model_integrator_gains(25e3, 2e3);
--------------------------------------------------
begin
test_with_uart : process(clock)
--------------------------------------------------
begin
    if rising_edge(clock) then
        create_bandpass_filter(bandpass_filter);
        init_mdio_driver(mdio_driver_data_in);
        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
            calculate_lcr_filter(lcr_filter1);
            calculate_lcr_filter(lcr_filter2); 
            calculate_lcr_filter(lcr_filter3); 
            calculate_lcr_filter(lcr_filter4); 
            calculate_lcr_filter(lcr_filter5);  
            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 15 => transmit_16_bit_word_with_uart(uart_data_in, uart_rx_data);
                WHEN 16 => transmit_16_bit_word_with_uart(uart_data_in, lcr_filter5.inductor_current.state + 32768);
                WHEN 17 => transmit_16_bit_word_with_uart(uart_data_in, lcr_filter5.capacitor_voltage.state+ 32768);
                WHEN others => -- get data from MDIO
            end CASE; 
            if test_counter = 65535 then
                test_counter <= 0;
            end if;
            if test_counter = 65536/2 then
                input_voltage <= -input_voltage;
            end if;
            if test_counter = 65535 then
                load_current <= -load_current;
            end if;
        end if;
                   -------------------------------------------------- 
            create_multiplier(hw_multiplier1); 
            create_multiplier(hw_multiplier2); 
            create_multiplier(hw_multiplier3); 
            create_multiplier(hw_multiplier4); 
            create_multiplier(hw_multiplier5); 
            create_lcr_filter(lcr_filter1 , hw_multiplier1 , input_voltage                       - lcr_filter1.capacitor_voltage.state , lcr_filter1.inductor_current.state - lcr_filter2.inductor_current.state);
            create_lcr_filter(lcr_filter2 , hw_multiplier2 , lcr_filter1.capacitor_voltage.state - lcr_filter2.capacitor_voltage.state , lcr_filter2.inductor_current.state - lcr_filter3.inductor_current.state);
            create_lcr_filter(lcr_filter3 , hw_multiplier3 , lcr_filter2.capacitor_voltage.state - lcr_filter3.capacitor_voltage.state , lcr_filter3.inductor_current.state - lcr_filter4.inductor_current.state);
            create_lcr_filter(lcr_filter4 , hw_multiplier4 , lcr_filter3.capacitor_voltage.state - lcr_filter4.capacitor_voltage.state , lcr_filter4.inductor_current.state - lcr_filter5.inductor_current.state);
            create_lcr_filter(lcr_filter5 , hw_multiplier5 , lcr_filter4.capacitor_voltage.state - lcr_filter5.capacitor_voltage.state , lcr_filter5.inductor_current.state - load_current);
    end if; --rising_edge
end process test_with_uart;    
				
			

Figure 4. Current of 5th inductor during a load current step with 5 cascaded LC filters

Figure 5. Voltage of 5th capacitor with 5 cascaded LC filter sections during input voltage step

Considering that what is happening here is synthesizable logic gate level simulation model creation of a power electronic device with high order EMI filter which VHDL is making pretty darn snappy!

With the 5 cascaded LC filters, the whole system consumes slightly over 2500 logic gates and 6 out of the 66 18×18 bit multipliers available in the FPGA. Note that the since the size of the ethernet build was ~1600 luts before the filter simulator, the 10th order LC filter simulator is taking less than 1k logic units and 5 multipliers.

Figure 6. Resource use of the whole system with 5 LC filter sections

Remarkable thing here is also that a single LC filter is takes exactly the same amount of clock cycles to  calculate as 5 cascaded LC filters and since the calculation takes 20 clock cycles, the 10th order filter could be calculated at the rate of 6MHz with the presented implementation. This could be significantly improved by pipelining the multiplications inside each of the LC filters as that would drop the calculation time to only 6 clock cycles resulting in possible calculation frequency of 20MHz and it would probably even consume less resources from the FPGA!

Fixed point interpretation

So far we haven’t bothered ourselves with how the numbers are changed from the used integer arithmetic to real world values of currents, voltages, Henries and Farads.

The inductor integrator gain is in the test code 25000, which equals 25e3/2^15 or 0.78125 in real numbers due to used radix15. With the test code running at 100kHz, the gain is 0.78125 * 100 000 or 78125. This sets the inductance at 1/78125 Henries or 12.8uH similarly the capacitance gain is 2000 which results in 164uF capacitance. The conversion between inductance or capacitance and the integrator gain is simply 2^radix*ts/gain.

Interestingly the actual values of the voltages and currents cannot be calculated in a similar straightforward fashion. Since the LC filter model is linear, the dynamics are the same regardless of the actual amplitudes of voltages and current. It would not make any difference if the ranges were megavolts and amperes or millivolts and amperes. In an actual device the voltage and current ranges are set by the ranges of the measurement devices.

Next in line

Now with the state variable and LC filter modules tested with FPGA, next thing to do is to create a FPGA simulation model for the entire bidirectional power supply hardware consisting of grid inverter and output inverters with correct values for inductors and capacitors and a dual active bridge which connects their dc links together.

1 thought on “Real-Time dynamic simulation with FPGA vol 1 : the space of states”

  1. Pingback: Real-Time dynamic simulation with FPGA vol 2 : Differential equations on a chip - Hardware Descriptions

Leave a Comment

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

Scroll to Top