| #ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_THERMAL_CONTROLLER_PID_H_ |
| #define THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_THERMAL_CONTROLLER_PID_H_ |
| |
| #include <cmath> |
| #include <limits> |
| |
| #include "thermal_config.pb.h" |
| |
| namespace milotic_tlbmc { |
| namespace thermal { |
| |
| /* |
| * `PidThermalLoop` is a commonly used thermal control algorithm based on |
| * proportional, integral, and derivative terms. The algorithm implemented here |
| * is a feedforward PID: |
| * https://blog.incatools.com/benefits-feedforward-pid-controllers |
| * |
| * This class is not thread-safe. |
| * |
| * This class is created based on the PID structs at |
| * https://github.com/openbmc/phosphor-pid-control/blob/master/pid/ec/pid.hpp |
| * |
| * More resources can be found at: |
| * https://www.mathworks.com/help/control/ug/proportional-integral-derivative-pid-controllers.html |
| * https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller |
| */ |
| class PidThermalLoop { |
| public: |
| PidThermalLoop() = default; |
| |
| explicit PidThermalLoop(const PidLoopConfig& pid_params) |
| : pid_params_(pid_params) { |
| if (pid_params.sample_time_sec() <= 0) { |
| pid_params_.set_sample_time_sec(1.0); |
| } |
| } |
| |
| /* |
| * If the hysteresis check passes, `ExecutePidLoop` performs exactly one |
| * iteration of a PID loop with the current input; otherwise, the last input |
| * will be used for the calculation. |
| * |
| * A hysteresis check is performed by comparing the current input with the |
| * last input. If the difference between the current input and the last |
| * input is no less than the hysteresis value, the check will pass. |
| * |
| * If `pid_params_.check_hysteresis_with_setpoint()` is true, the hysteresis |
| * check will be performed by comparing the current input with the setpoint, |
| * instead of comparing it with the last input. |
| * |
| * Hysteresis check will be skipped if |
| * `pid_params_.check_hysteresis_with_setpoint()` is false and none of the |
| * hysteresis values is significant. |
| * |
| * A hysteresis value needs to be at least 0.5 to be significant. |
| */ |
| double ExecutePidLoop(double input, double setpoint, |
| bool enable_calculation_log = false); |
| |
| /* |
| * `CalculatePidOutput` performs exactly one iteration of a PID loop. |
| * |
| * The states from the last iterations (e.g., `integral_`, `last_output_`, and |
| * `last_error_`) will be used here, so we need to maintain the states, making |
| * this function non-const. |
| */ |
| double CalculatePidOutput(double input, double setpoint, |
| bool enable_calculation_log = false); |
| |
| void SetInitialized(bool initialized) { initialized_ = initialized; } |
| bool GetInitialized() const { return initialized_; } |
| |
| void SetCheckHysteresisWithSetpoint(bool check_hysteresis_with_setpoint) { |
| pid_params_.set_check_hysteresis_with_setpoint( |
| check_hysteresis_with_setpoint); |
| } |
| bool GetCheckHysteresisWithSetpoint() const { |
| return pid_params_.check_hysteresis_with_setpoint(); |
| } |
| |
| void SetIntegral(double integral) { integral_ = integral; } |
| double GetIntegral() const { return integral_; } |
| |
| void SetLastInput(double last_input) { last_input_ = last_input; } |
| double GetLastInput() const { return last_input_; } |
| |
| void SetLastOutput(double last_output) { last_output_ = last_output; } |
| double GetLastOutput() const { return last_output_; } |
| |
| void SetLastError(double last_error) { last_error_ = last_error; } |
| double GetLastError() const { return last_error_; } |
| |
| void SetSampleTime(double sample_time) { |
| pid_params_.set_sample_time_sec(sample_time); |
| } |
| double GetSampleTime() const { return pid_params_.sample_time_sec(); } |
| |
| void SetCoeffProportional(double coeff_proportional) { |
| pid_params_.set_coeff_proportional(coeff_proportional); |
| } |
| double GetCoeffProportional() const { |
| return pid_params_.coeff_proportional(); |
| } |
| |
| void SetCoeffIntegral(double coeff_integral) { |
| pid_params_.set_coeff_integral(coeff_integral); |
| } |
| double GetCoeffIntegral() const { return pid_params_.coeff_integral(); } |
| |
| void SetCoeffDerivative(double coeff_derivative) { |
| pid_params_.set_coeff_derivative(coeff_derivative); |
| } |
| double GetCoeffDerivative() const { return pid_params_.coeff_derivative(); } |
| |
| void SetFeedForwardOffset(double feed_forward_offset) { |
| pid_params_.set_feed_forward_offset(feed_forward_offset); |
| } |
| double GetFeedForwardOffset() const { |
| return pid_params_.feed_forward_offset(); |
| } |
| |
| void SetFeedForwardGain(double feed_forward_gain) { |
| pid_params_.set_feed_forward_gain(feed_forward_gain); |
| } |
| double GetFeedForwardGain() const { return pid_params_.feed_forward_gain(); } |
| |
| void SetIntegralLimitMax(double integral_limit_max) { |
| pid_params_.set_integral_limit_max(integral_limit_max); |
| } |
| double GetIntegralLimitMax() const { |
| return pid_params_.integral_limit_max(); |
| } |
| |
| void SetIntegralLimitMin(double integral_limit_min) { |
| pid_params_.set_integral_limit_min(integral_limit_min); |
| } |
| double GetIntegralLimitMin() const { |
| return pid_params_.integral_limit_min(); |
| } |
| |
| void SetOutputLimitMax(double output_limit_max) { |
| pid_params_.set_output_limit_max(output_limit_max); |
| } |
| double GetOutputLimitMax() const { return pid_params_.output_limit_max(); } |
| |
| void SetOutputLimitMin(double output_limit_min) { |
| pid_params_.set_output_limit_min(output_limit_min); |
| } |
| double GetOutputLimitMin() const { return pid_params_.output_limit_min(); } |
| |
| void SetSlewNeg(double slew_neg) { pid_params_.set_slew_neg(slew_neg); } |
| double GetSlewNeg() const { return pid_params_.slew_neg(); } |
| |
| void SetSlewPos(double slew_pos) { pid_params_.set_slew_pos(slew_pos); } |
| double GetSlewPos() const { return pid_params_.slew_pos(); } |
| |
| void SetHysteresisNeg(double hysteresis_neg) { |
| pid_params_.set_hysteresis_neg(hysteresis_neg); |
| } |
| double GetHysteresisNeg() const { return pid_params_.hysteresis_neg(); } |
| |
| void SetHysteresisPos(double hysteresis_pos) { |
| pid_params_.set_hysteresis_pos(hysteresis_pos); |
| } |
| double GetHysteresisPos() const { return pid_params_.hysteresis_pos(); } |
| |
| protected: |
| // Calculate proportional gain based on Kp. |
| double CalculateProportionalGain(double error_value) const; |
| // Calculate integral gain based on Ki. |
| double CalculateIntegralGain(double error_value) const; |
| // Calculate derivative gain based on Kd. |
| double CalculateDerivativeGain(double error_value) const; |
| // Calculate feed-forward term based on feed-forward gain and offset. |
| double CalculateFeedForward(double setpoint) const; |
| // Clamp the output based on slew rates. |
| double ClampOutputWithSlewRates(double pid_output) const; |
| |
| // Hysteresis values need to be at least 0.5 to be significant. |
| bool IsHysteresisCheckEnabled() const { |
| return pid_params_.check_hysteresis_with_setpoint() || |
| GetHysteresisPos() >= 0.5 || GetHysteresisNeg() >= 0.5; |
| } |
| // Perform input hysteresis check if significant hysteresis params are set: |
| // 1. Is `last_input_` invalid (e.g., NaN, INF, or -INF)? |
| // 2. Is the delta between `input` and `last_input_` outside of the |
| // hysteresis bounds? |
| // If any condition is satisfied, perform one PID iteration using `input`; |
| // otherwise, use `last_input_`. |
| bool IsWithinInputHysteresisBounds(double input) const { |
| return IsHysteresisCheckEnabled() && std::isfinite(GetLastInput()) && |
| input - GetLastInput() <= GetHysteresisPos() && |
| GetLastInput() - input <= GetHysteresisNeg(); |
| } |
| |
| private: |
| bool initialized_ = false; // Is this PID finished with the first iteration? |
| |
| double integral_ = 0.0; // Integral of error |
| |
| double last_input_ = std::numeric_limits<double>::quiet_NaN(); |
| double last_output_ = 0.0; |
| double last_error_ = 0.0; |
| |
| PidLoopConfig pid_params_; // The parameters for this PID thermal loop. |
| }; |
| |
| } // namespace thermal |
| } // namespace milotic_tlbmc |
| |
| #endif // THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_THERMAL_CONTROLLER_PID_H_ |