LCOV - code coverage report
Current view: top level - AC_PID - AC_PID.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 138 173 79.8 %
Date: 2026-09-09 11:02:08 Functions: 21 23 91.3 %

          Line data    Source code
       1             : /// @file       AC_PID.cpp
       2             : /// @brief      General-purpose PID controller with input, error, and derivative filtering, plus slew rate limiting and EEPROM gain storage.
       3             : 
       4             : #include <AP_Math/AP_Math.h>
       5             : #include "AC_PID.h"
       6             : 
       7             : #define AC_PID_DEFAULT_NOTCH_ATTENUATION 40
       8             : 
       9             : const AP_Param::GroupInfo AC_PID::var_info[] = {
      10             :     // @Param: P
      11             :     // @DisplayName: PID Proportional Gain
      12             :     // @Description: P Gain which produces an output value that is proportional to the current error value
      13             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("P", 0, AC_PID, _kp, default_kp),
      14             : 
      15             :     // @Param: I
      16             :     // @DisplayName: PID Integral Gain
      17             :     // @Description: I Gain which produces an output that is proportional to both the magnitude and the duration of the error
      18             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("I", 1, AC_PID, _ki, default_ki),
      19             : 
      20             :     // @Param: D
      21             :     // @DisplayName: PID Derivative Gain
      22             :     // @Description: D Gain which produces an output that is proportional to the rate of change of the error
      23             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("D", 2, AC_PID, _kd, default_kd),
      24             : 
      25             :     // 3 was for uint16 IMAX
      26             : 
      27             :     // @Param: FF
      28             :     // @DisplayName: FF FeedForward Gain
      29             :     // @Description: FF Gain which produces an output value that is proportional to the demanded input
      30             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("FF", 4, AC_PID, _kff, default_kff),
      31             : 
      32             :     // @Param: IMAX
      33             :     // @DisplayName: PID Integral Maximum
      34             :     // @Description: The maximum/minimum value that the I term can output
      35             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("IMAX", 5, AC_PID, _kimax, default_kimax),
      36             : 
      37             :     // 6 was for float FILT
      38             : 
      39             :     // 7 is for float ILMI and FF
      40             : 
      41             :     // index 8 was for AFF
      42             : 
      43             :     // @Param: FLTT
      44             :     // @DisplayName: PID Target filter frequency in Hz
      45             :     // @Description: Low-pass filter frequency applied to the target input (Hz)
      46             :     // @Units: Hz
      47             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("FLTT", 9, AC_PID, _filt_T_hz, default_filt_T_hz),
      48             : 
      49             :     // @Param: FLTE
      50             :     // @DisplayName: PID Error filter frequency in Hz
      51             :     // @Description: Low-pass filter frequency applied to the error (Hz)
      52             :     // @Units: Hz
      53             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("FLTE", 10, AC_PID, _filt_E_hz, default_filt_E_hz),
      54             : 
      55             :     // @Param: FLTD
      56             :     // @DisplayName: PID Derivative term filter frequency in Hz
      57             :     // @Description: Low-pass filter frequency applied to the derivative (Hz)
      58             :     // @Units: Hz
      59             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("FLTD", 11, AC_PID, _filt_D_hz, default_filt_D_hz),
      60             : 
      61             :     // @Param: SMAX
      62             :     // @DisplayName: Slew rate limit
      63             :     // @Description: Sets an upper limit on the slew rate produced by the combined P and D gains. If the amplitude of the control action produced by the rate feedback exceeds this value, then the D+P gain is reduced to respect the limit. This limits the amplitude of high frequency oscillations caused by an excessive gain. The limit should be set to no more than 25% of the actuators maximum slew rate to allow for load effects. Note: The gain will not be reduced to less than 10% of the nominal value. A value of zero will disable this feature.
      64             :     // @Range: 0 200
      65             :     // @Increment: 0.5
      66             :     // @User: Advanced
      67             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("SMAX", 12, AC_PID, _slew_rate_max, default_slew_rate_max),
      68             : 
      69             :     // @Param: PDMX
      70             :     // @DisplayName: PD sum maximum
      71             :     // @Description: The maximum/minimum value that the sum of the P and D term can output
      72             :     // @User: Advanced
      73             :     AP_GROUPINFO("PDMX", 13, AC_PID, _kpdmax, 0),
      74             : 
      75             :     // @Param: D_FF
      76             :     // @DisplayName: PID Derivative FeedForward Gain
      77             :     // @Description: FF D Gain which produces an output that is proportional to the rate of change of the target
      78             :     // @Range: 0 0.02
      79             :     // @Increment: 0.0001
      80             :     // @User: Advanced
      81             :     AP_GROUPINFO_FLAGS_DEFAULT_POINTER("D_FF", 14, AC_PID, _kdff, default_kdff),
      82             : 
      83             : #if AP_FILTER_ENABLED
      84             :     // @Param: NTF
      85             :     // @DisplayName: PID Target notch filter index
      86             :     // @Description: PID Target notch filter index, zero disables
      87             :     // @Range: 0 8
      88             :     // @User: Advanced
      89             :     AP_GROUPINFO("NTF", 15, AC_PID, _notch_T_filter, 0),
      90             : 
      91             :     // @Param: NEF
      92             :     // @DisplayName: PID Error notch filter index
      93             :     // @Description: PID Error notch filter index, zero disables
      94             :     // @Range: 0 8
      95             :     // @User: Advanced
      96             :     AP_GROUPINFO("NEF", 16, AC_PID, _notch_E_filter, 0),
      97             : #endif
      98             : 
      99             :     AP_GROUPEND
     100             : };
     101             : 
     102             : // Constructor
     103        9690 : AC_PID::AC_PID(float initial_p, float initial_i, float initial_d, float initial_ff, float initial_imax, float initial_filt_T_hz, float initial_filt_E_hz, float initial_filt_D_hz,
     104        9690 :                float initial_srmax, float initial_srtau, float initial_dff) :
     105        9690 :     default_kp(initial_p),
     106        9690 :     default_ki(initial_i),
     107        9690 :     default_kd(initial_d),
     108        9690 :     default_kff(initial_ff),
     109        9690 :     default_kdff(initial_dff),
     110        9690 :     default_kimax(initial_imax),
     111        9690 :     default_filt_T_hz(initial_filt_T_hz),
     112        9690 :     default_filt_E_hz(initial_filt_E_hz),
     113        9690 :     default_filt_D_hz(initial_filt_D_hz),
     114        9690 :     default_slew_rate_max(initial_srmax)
     115             : {
     116             :     // load parameter values from eeprom
     117        9690 :     AP_Param::setup_object_defaults(this, var_info);
     118             : 
     119             :     // this param is not in the table, so its default is no loaded in the call above
     120        9690 :     _slew_rate_tau.set(initial_srtau);
     121             : 
     122             :     // reset input filter to first value received
     123        9690 :     _flags._reset_filter = true;
     124             : 
     125        9690 :     memset(&_pid_info, 0, sizeof(_pid_info));
     126             : 
     127             :     // slew limit scaler allows for plane to use degrees/sec slew
     128             :     // limit
     129        9690 :     _slew_limit_scale = 1;
     130        9690 : }
     131             : 
     132             : // Sets the target low-pass filter frequency (Hz)
     133        7357 : void AC_PID::set_filt_T_hz(float hz)
     134             : {
     135        7357 :     _filt_T_hz.set(fabsf(hz));
     136        7357 : }
     137             : 
     138             : // Sets the error low-pass filter frequency (Hz)
     139        2970 : void AC_PID::set_filt_E_hz(float hz)
     140             : {
     141        2970 :     _filt_E_hz.set(fabsf(hz));
     142        2970 : }
     143             : 
     144             : // Sets the derivative low-pass filter frequency (Hz)
     145           0 : void AC_PID::set_filt_D_hz(float hz)
     146             : {
     147           0 :     _filt_D_hz.set(fabsf(hz));
     148           0 : }
     149             : 
     150             : // slew_limit - set slew limit
     151        7357 : void AC_PID::set_slew_limit(float smax)
     152             : {
     153        7357 :     _slew_rate_max.set(fabsf(smax));
     154        7357 : }
     155             : 
     156             : // Configures optional notch filters for target and error signals using the given sample rate.
     157             : // Filters are dynamically allocated and validated via the AP_Filter API.
     158      581067 : void AC_PID::set_notch_sample_rate(float sample_rate)
     159             : {
     160             : #if AP_FILTER_ENABLED
     161      581067 :     if (_notch_T_filter == 0 && _notch_E_filter == 0) {
     162      580344 :         return;
     163             :     }
     164             : 
     165         723 :     if (_notch_T_filter != 0) {
     166           0 :         if (_target_notch == nullptr) {
     167           0 :             _target_notch = NEW_NOTHROW NotchFilterFloat();
     168             :         }
     169             :         // Lookup filter definition and initialize if valid
     170           0 :         AP_Filter* filter = AP::filters().get_filter(_notch_T_filter);
     171           0 :         if (filter != nullptr && !filter->setup_notch_filter(*_target_notch, sample_rate)) {
     172           0 :             delete _target_notch;
     173           0 :             _target_notch = nullptr;
     174           0 :             _notch_T_filter.set(0);  // disable filter if setup fails
     175             :         }
     176             :     }
     177             : 
     178         723 :     if (_notch_E_filter != 0) {
     179         723 :         if (_error_notch == nullptr) {
     180          12 :             _error_notch = NEW_NOTHROW NotchFilterFloat();
     181             :         }
     182             :         // Lookup filter definition and initialize if valid
     183         723 :         AP_Filter* filter = AP::filters().get_filter(_notch_E_filter);
     184         723 :         if (filter != nullptr && !filter->setup_notch_filter(*_error_notch, sample_rate)) {
     185           0 :             delete _error_notch;
     186           0 :             _error_notch = nullptr;
     187           0 :             _notch_E_filter.set(0);  // disable filter if setup fails
     188             :         }
     189             :     }
     190             : #endif
     191             : }
     192             : 
     193             : // Computes the PID output using a target and measurement input.
     194             : // Applies filters to the target and error, calculates the derivative and updates the integrator.
     195             : // If `limit` is true, the integrator is allowed to shrink but not grow.
     196   144349060 : float AC_PID::update_all(float target, float measurement, float dt, bool limit, float pd_scale, float i_scale)
     197             : {
     198             :     // Return zero if input is invalid (NaN or infinite)
     199   144349060 :     if (!isfinite(target) || !isfinite(measurement)) {
     200           0 :         return 0.0f;
     201             :     }
     202             : 
     203             :     // Flag used for logging to indicate a filter reset occurred
     204   144349061 :     _pid_info.reset = _flags._reset_filter;
     205             :     // Reset filters to match the current input (first sample or after reset)
     206   144349061 :     if (_flags._reset_filter) {
     207             :         // Reset filters to match the current inputs
     208     4470320 :         _flags._reset_filter = false;
     209             : 
     210             :         // Reset target filter
     211     4470320 :         _target = target;
     212             : #if AP_FILTER_ENABLED
     213     4470320 :         if (_target_notch != nullptr) {
     214           0 :             _target_notch->reset();
     215           0 :             _target = _target_notch->apply(_target);
     216             :         }
     217             : #endif
     218             : 
     219             :         // Calculate error and reset error filter
     220     4470320 :         _error = _target - measurement;
     221             : #if AP_FILTER_ENABLED
     222     4470320 :         if (_error_notch != nullptr) {
     223           0 :             _error_notch->reset();
     224           0 :             _error = _error_notch->apply(_error);
     225             :         }
     226             : #endif
     227             :         // Clear derivative history to avoid spikes after reset
     228     4470320 :         _derivative = 0.0f;
     229     4470320 :         _target_derivative = 0.0f;
     230             : 
     231             :     } else {
     232             : 
     233             :         // Apply target filters
     234   139878741 :         const float target_last = _target;
     235             : #if AP_FILTER_ENABLED
     236   139878741 :         if (_target_notch != nullptr) {
     237             :             // Allocate and set up target notch filter
     238           0 :             target = _target_notch->apply(target);
     239             :         }
     240             : #endif
     241             :         // Apply first-order low-pass filter to target value
     242   139878741 :         _target += get_filt_T_alpha(dt) * (target - _target);
     243             : 
     244             :         // Calculate error and apply error filter
     245   139878739 :         const float error_last = _error;
     246   139878739 :         float error = _target - measurement;
     247             : #if AP_FILTER_ENABLED
     248   139878739 :         if (_error_notch != nullptr) {
     249             :             // Allocate and set up error notch filter
     250      289377 :             error = _error_notch->apply(error);
     251             :         }
     252             : #endif
     253             :         // apply notch filters before FTLD/FLTE to minimize shot noise
     254   139878739 :         _error += get_filt_E_alpha(dt) * (error - _error);
     255             : 
     256   139878740 :         if (is_positive(dt)) {
     257             :             // Compute and low-pass filter the error derivative (D term)
     258   139878740 :             float derivative = (_error - error_last) / dt;
     259   139878740 :             _derivative += get_filt_D_alpha(dt) * (derivative - _derivative);
     260             :             // Calculate target derivative for D_FF contribution
     261   139878739 :             _target_derivative = (_target - target_last) / dt;
     262             :         }
     263             :     }
     264             : 
     265             :     // Integrate error (with wind-up protection if limit is active)
     266             :     // If limit is active, allow I-term to shrink but not grow
     267   144349059 :     update_i(dt, limit, i_scale);
     268             : 
     269   144349061 :     float P_out = (_error * _kp);
     270   144349060 :     float D_out = (_derivative * _kd);
     271   144349060 :     float I_out = _integrator;
     272             : 
     273             :     // Calculate dynamic modifier to reduce P+D output based on slew rate limiter
     274   144349060 :     _pid_info.Dmod = _slew_limiter.modifier((_pid_info.P + _pid_info.D) * _slew_limit_scale, dt);
     275   144349057 :     _pid_info.slew_rate = _slew_limiter.get_slew_rate();
     276             : 
     277             :     // This modifier is used to reduce control effort under fast transients
     278   144349057 :     P_out *= _pid_info.Dmod;
     279   144349057 :     D_out *= _pid_info.Dmod;
     280             : 
     281             :     // scale pd output if required
     282   144349057 :     P_out *= pd_scale;
     283   144349057 :     D_out *= pd_scale;
     284             : 
     285   144349057 :     _pid_info.PD_limit = false;
     286             :     // Apply PD sum limit if enabled
     287   144349057 :     if (is_positive(_kpdmax)) {
     288           0 :         const float PD_sum_abs = fabsf(P_out + D_out);
     289           0 :         if (PD_sum_abs > _kpdmax) {
     290           0 :             const float pd_limit_scale = _kpdmax / PD_sum_abs;
     291           0 :             P_out *= pd_limit_scale;
     292           0 :             D_out *= pd_limit_scale;
     293           0 :             _pid_info.PD_limit = true;
     294             :         }
     295             :     }
     296             : 
     297   144349056 :     _pid_info.target = _target;
     298   144349056 :     _pid_info.actual = measurement;
     299   144349056 :     _pid_info.error = _error;
     300   144349056 :     _pid_info.P = P_out;
     301   144349056 :     _pid_info.D = D_out;
     302   144349056 :     _pid_info.I = I_out;
     303   144349056 :     _pid_info.limit = limit;
     304             :     // Set I set flag for logging and clear
     305   144349056 :     _pid_info.I_term_set = _flags._I_set;
     306   144349056 :     _flags._I_set = false;
     307   144349056 :     _pid_info.FF = _target * _kff;
     308   144349056 :     _pid_info.DFF = _target_derivative * _kdff;
     309             : 
     310   144349056 :     return P_out + D_out + I_out;
     311             : }
     312             : 
     313             : // Computes the PID output from an error input only (target assumed to be zero).
     314             : // Applies error filtering and updates the derivative and integrator.
     315             : // Target and measurement must be set separately for logging.
     316             : // todo: remove function when it is no longer used.
     317       17270 : float AC_PID::update_error(float error, float dt, bool limit)
     318             : {
     319             :     // don't process inf or NaN
     320       17270 :     if (!isfinite(error)) {
     321           0 :         return 0.0f;
     322             :     }
     323             : 
     324             :     // Reuse update all code path, zero target and pass negative error as measurement
     325             :     // Pass negative error as "measurement" so that error = target - measurement evaluates correctly
     326             :     // Bypasses target filtering for legacy compatibility
     327       17270 :     _target = 0.0;
     328       17270 :     const float output = update_all(0.0, -error, dt, limit);
     329             : 
     330             :     // Make sure logged target and actual are still 0 to maintain behaviour
     331       17270 :     _pid_info.target = 0.0;
     332       17270 :     _pid_info.actual = 0.0;
     333             : 
     334       17270 :     return output;
     335             : }
     336             : 
     337             : // Updates the integrator based on current error and dt.
     338             : // If `limit` is true, the integrator is only allowed to shrink to avoid wind-up.
     339             : // i_scale can be used to temporarily scale the updated I-term, by default is 1 - should not be set to 0
     340   144349059 : void AC_PID::update_i(float dt, bool limit, float i_scale)
     341             : {
     342   144349059 :     if (!is_zero(_ki) && is_positive(dt)) {
     343             :         // Allow integrator growth only if not limited, or if error opposes the integrator direction
     344   144331791 :         if (!limit || ((is_positive(_integrator) && is_negative(_error)) || (is_negative(_integrator) && is_positive(_error)))) {
     345   100910170 :             _integrator += ((float)_error * _ki) * i_scale * dt;
     346   100910169 :             _integrator = constrain_float(_integrator, -_kimax, _kimax);
     347             :         }
     348             :     } else {
     349       17270 :         _integrator = 0.0f;
     350             :     }
     351   144349061 : }
     352             : 
     353       17468 : float AC_PID::get_p() const
     354             : {
     355       17468 :     return _pid_info.P;
     356             : }
     357             : 
     358    19701140 : float AC_PID::get_i() const
     359             : {
     360    19701140 :     return _integrator;
     361             : }
     362             : 
     363       17468 : float AC_PID::get_d() const
     364             : {
     365       17468 :     return _pid_info.D;
     366             : }
     367             : 
     368   125770457 : float AC_PID::get_ff() const
     369             : {
     370   125770457 :     return  _pid_info.FF + _pid_info.DFF;
     371             : }
     372             : 
     373    15131989 : float AC_PID::get_ff_component() const
     374             : {
     375    15131989 :     return  _pid_info.FF;
     376             : }
     377             : 
     378    15131989 : float AC_PID::get_dff_component() const
     379             : {
     380    15131989 :     return  _pid_info.DFF;
     381             : }
     382             : 
     383             : // Used to fully zero the I term between mode changes or initialization
     384    49310475 : void AC_PID::reset_I()
     385             : {
     386    49310475 :     _flags._I_set = true;
     387    49310475 :     _integrator = 0.0;
     388    49310475 : }
     389             : 
     390             : // Loads controller configuration from EEPROM, including gains and filter frequencies. (not used)
     391           0 : void AC_PID::load_gains()
     392             : {
     393           0 :     _kp.load();
     394           0 :     _ki.load();
     395           0 :     _kd.load();
     396           0 :     _kff.load();
     397           0 :     _filt_T_hz.load();
     398           0 :     _filt_E_hz.load();
     399           0 :     _filt_D_hz.load();
     400           0 : }
     401             : 
     402             : // Saves controller configuration from EEPROM, including gains and filter frequencies. Used by autotune to save gains before tuning.
     403          17 : void AC_PID::save_gains()
     404             : {
     405          17 :     _kp.save();
     406          17 :     _ki.save();
     407          17 :     _kd.save();
     408          17 :     _kff.save();
     409          17 :     _filt_T_hz.save();
     410          17 :     _filt_E_hz.save();
     411          17 :     _filt_D_hz.save();
     412          17 : }
     413             : 
     414             : // Returns alpha value for the target low-pass filter (based on filter frequency and dt)
     415   139878740 : float AC_PID::get_filt_T_alpha(float dt) const
     416             : {
     417   139878740 :     return calc_lowpass_alpha_dt(dt, _filt_T_hz);
     418             : }
     419             : 
     420             : // Returns alpha value for the error low-pass filter (based on filter frequency and dt)
     421   139883103 : float AC_PID::get_filt_E_alpha(float dt) const
     422             : {
     423   139883103 :     return calc_lowpass_alpha_dt(dt, _filt_E_hz);
     424             : }
     425             : 
     426             : // Returns alpha value for the derivative low-pass filter (based on filter frequency and dt)
     427   139883104 : float AC_PID::get_filt_D_alpha(float dt) const
     428             : {
     429   139883104 :     return calc_lowpass_alpha_dt(dt, _filt_D_hz);
     430             : }
     431             : 
     432             : // Sets the integrator directly, clamped to the IMAX bounds. Also flags I-term as externally set.
     433    17747624 : void AC_PID::set_integrator(float integrator)
     434             : {
     435    17747624 :     _flags._I_set = true;
     436    17747624 :     _integrator = constrain_float(integrator, -_kimax, _kimax);
     437    17747624 : }
     438             : 
     439             : // Gradually adjust the integrator toward a desired value using a time constant.
     440             : // Typically used to "relax" the I-term in dynamic conditions.
     441    13525593 : void AC_PID::relax_integrator(float integrator, float dt, float time_constant)
     442             : {
     443    13525593 :     integrator = constrain_float(integrator, -_kimax, _kimax);
     444    13525593 :     if (is_positive(dt)) {
     445    11960006 :         _flags._I_set = true;
     446    11960006 :         _integrator = _integrator + (integrator - _integrator) * (dt / (dt + time_constant));
     447             :     }
     448    13525593 : }

Generated by: LCOV version 1.14