Electronic – How to tune this PID loop

dc motorfpgapid controllerrotary encodervhdl

I have a DC motor that is controlled by an FPGA and a L298N motor driver board. The FPGA generates a PWM signal based on feedback from a dual channel quadrature rotary encoder (1000ppr per channel).

I have the code at the bottom of this post showing the PID code used to produce the error (PWM) signal for the motor driver. The PID code takes the setpoint and actual speeds in RPM and calculates the error based on the PID constants. The PID scan rate is 16ms which I think is to slow for this application, but as fast as I can currently get and I would like the tune the PID loop for this code run time.

I have some information about the running with constant changes (kd and ki not used yet):

Setpoint = 3500rpm and kp = 4, motor runs at constant 2750rpm and 80% duty cycle.

Setpoint = 3500rpm and kp = 5, motor fluctuates between 2830rpm and 2920rpm and between 79% and 83% duty cycle.

Setpoint = 3500rpm and kp = 6, motor fluctuates between 2920rpm and 3050rpm and between 78% and 85% duty cycle.

Setpoint = 3500rpm and kp = 7, motor fluctuates between 2970rpm and 3130rpm and between 77% and 86% duty cycle.

Setpoint = 3500rpm and kp = 8, motor fluctuates between 3000rpm and 3280rpm and between 68% and 90% duty cycle.

Setpoint = 3500rpm and kp = 9, motor fluctuates between 3020rpm and 3330rpm and between 71% and 91% duty cycle.

Setpoint = 3500rpm and kp = 10, motor fluctuates between 3080rpm and 3390rpm and between 70% and 94% duty cycle.

Setpoint = 3500rpm and kp = 13, motor fluctuates between 3100rpm and 3600rpm and between 25% and 91% duty cycle changing rapidly.

You can see with kp = 9, the duty cycle is starting to oscillate with the rpm value also starting to oscillate with some steady error.

And with kp = 13, the oscillation is very bad.

How should I go about tuning this system from here?


--This code contains the logic for generating the PWM output signal for the motor, based
--on the PID control loop.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
use work.Data_Sizes_Package.ALL;

entity PWM_Counter_and_Comparator is

 Generic (Max_Counter_Value : integer := 5000);
    
 Port (PWM_Comparison_Value : in std_logic_vector(27 downto 0);
       Clock : in std_logic;
       PID_Run_Command : in std_logic; 
       Run_Reset : in std_logic;
       PWM_Output : out std_logic
       );
end PWM_Counter_and_Comparator;

architecture Behavioral of PWM_Counter_and_Comparator is

signal Setpoint_RPM : integer range 0 to 5000 := 0;
signal Actual_RPM : integer range 0 to 5000 := 0;
signal Count_Value : integer range 0 to 5000 := 0;
signal PID_Comparison_Value : integer range 0 to 5000 := 0;
signal PID_Comparison_Value_Buffer : integer range 0 to 5000 := 0;
            
constant Kp : integer := 5;
constant Ki : integer := 0;
constant Kd : integer := 0;

begin

Setpoint_RPM <= conv_integer(unsigned(PWM_Comparison_Value(13 downto 0)));  --Convert setpoint RPM from a vector to an integer
Actual_RPM <= conv_integer(unsigned(PWM_Comparison_Value(27 downto 14)));   --Convert actual RPM from a vector to an integer

Process(Clock , Run_Reset)  --This process generates a counter ranging from 0 to 5000 with a 100MHz clock.
                            --Used to create a 20kHz PWM output frequency.
begin

    if(Run_Reset = '0') then    --When run command is low, reset counter.
      Count_Value <= 0;
          
    elsif(rising_edge(Clock)) then
    
         if(Count_Value = Max_Counter_Value) then   
            Count_Value <= 0;                                       --Resets counter back to 0 once it reaches 5000.
            PID_Comparison_Value <= PID_Comparison_Value_Buffer;    --Updates the new counter comparison value from PID calculation only when counter = 0
              
         elsif(Run_Reset = '1') then
               Count_Value <= Count_Value + 1;      --Increments counter on clock rising edge.
    
         end if;
         
     end if;
     
end Process;

Process(Clock)    

variable Error_RPM :  integer range -5000 to 5000 := 0;
variable Last_Error_RPM : integer range -50000000 to 50000000 := 0;
variable Error_Sum:  integer range -50000000 to 5000000 := 0;
variable Error_Change:  integer range -50000000 to 50000000 := 0;
variable PID_Temp:  integer range -50000000 to 50000000 := 0;
variable PID_Output:  integer range 0 to 5000 := 0;

begin

    if(Run_Reset = '0') then
      Error_RPM := 0;
      Last_Error_RPM := 0;
      Error_Sum := 0;
      Error_Change := 0;
      PID_Output := 0;
          
    elsif((falling_edge(Clock)) and Run_Reset = '1' and PID_Run_Command = '1') then  --PID clock is 41.67Hz, perfoming this process every 16ms.
    
      Error_RPM := Setpoint_RPM - Actual_RPM;   --Calculate error in rpm
      
      Error_Sum := Error_Sum + Error_RPM;       --Integral not used yet
      
      if(Error_Sum > 20000) then                --Setting max integral limit
         Error_Sum := 20000;                    
      
      elsif(Error_Sum < -20000) then
         Error_Sum := -20000;                   --Setting min integral limit
      end if;
      
      Error_Change := Error_RPM - Last_Error_RPM;   --Derivative not used yet
      
      PID_Temp := ((Kp * Error_RPM) + (Ki * Error_Sum) + (Kd * Error_Change) / 100); --PID output signal calculation
                                                                                      
      Last_Error_RPM := Error_RPM;
      
        if(PID_Temp > Max_Counter_Value) then --Max_Counter_Value = 5000 = 100% duty cycle
            PID_Temp := Max_Counter_Value;
              
         elsif(PID_Temp < -5000) then  -- -5000 = 0% duty cycle
           PID_Temp := -5000;        
    
          end if;
          
      end if;
      
      PID_Output := (PID_Temp + 5000) / 2;     --This scales the PID_Temp calculation result from -5000 to 5000, into a  
                                               --value between 0 and 5000 to match the counter value for duty cycle.
                                                     
      PID_Comparison_Value_Buffer <= PID_Output;    --Buffer used to hold the PID_Output until counter is equal to zero.
      
end Process;

Process(Clock , Run_Reset)

begin
    
    if((Count_Value < PID_Comparison_Value) and (Run_Reset = '1')) then
      PWM_Output <= '1';
                                                --This process sets the output pin high and low at a frequency of 20kHz.
    else                                        --Compares the counter value with the PID calculation output.
      PWM_Output <= '0';                        --The ratio between count value and comparison value is the duty cycle.
                                                --i.e. count value = 5000, comparison value = 2500, duty cycle = 5000/2500 = 50%
    end if;
    
end Process;

end Behavioral;
```

Best Answer

I remember to have gone through your code some days ago. Finaly thinking that you had fixed your issue I did not comment.

Frankly speaking it is really dirty what you coded from an FPGA engineering point of view. It would not pass a code review in the industry.

Here are my remarks:

1- PWM_Comparison_Value port: Just a cosmetic remarks, it does not improve readability to concatenate 2 signals in one

2- Reset: All signals are not reset

3- Reset: Is the reset synchronously deasserted somewhere?

4- Integers: Use integers for signals if you are totally aware of what is synthesized by the tool inside the FPGA. Otherwise make the effort to use unsigned or signed types

4b- Avoid to use variables / pipeline your design so operations are done in several clock pulses

5- Math operations scalling: I recommend you to check that all your Maths operation line of codes do not overflow. That your calculations fit inside signals ranges.

6- No if falling edge: There is a common FPGA coding rule to use only rising or falling edge on a clock but not both. Functionally it looks like you gain half a clock cycle of latency. Behind the scene you double the constraint frequency.

7- Testbench: Create a testbench of your module in closed loop, tune it, make it working and then continue on target. You can create a simple model in the top testbench file.

Point 7 is the most important to me.

I hope this helps