From 1eed1adfa0314bb53b3716ea59187b49aa40a036 Mon Sep 17 00:00:00 2001 From: Thomas SAMTER <7680607+P4uLT@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:38:45 +0100 Subject: [PATCH] [pid] Replace std::deque with FixedRingBuffer (#14733) Co-authored-by: J. Nick Koston --- esphome/components/pid/climate.py | 24 ++++++++++------- esphome/components/pid/pid_climate.h | 10 ++++++- esphome/components/pid/pid_controller.cpp | 32 +++++++++++------------ esphome/components/pid/pid_controller.h | 24 ++++++++++------- 4 files changed, 53 insertions(+), 37 deletions(-) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 0e66b67637..18e33b8039 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_, cv.Optional( CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1 - ): cv.int_, + ): cv.positive_not_null_int, } ), cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema( @@ -68,8 +68,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_STARTING_INTEGRAL_TERM, default=0.0): cv.float_, cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_, cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_, - cv.Optional(CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1): cv.int_, - cv.Optional(CONF_OUTPUT_AVERAGING_SAMPLES, default=1): cv.int_, + cv.Optional( + CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1 + ): cv.positive_not_null_int, + cv.Optional( + CONF_OUTPUT_AVERAGING_SAMPLES, default=1 + ): cv.positive_not_null_int, } ), } @@ -102,13 +106,15 @@ async def to_code(config): cg.add(var.set_starting_integral_term(params[CONF_STARTING_INTEGRAL_TERM])) cg.add(var.set_derivative_samples(params[CONF_DERIVATIVE_AVERAGING_SAMPLES])) - cg.add(var.set_output_samples(params[CONF_OUTPUT_AVERAGING_SAMPLES])) + output_samples = params[CONF_OUTPUT_AVERAGING_SAMPLES] + cg.add(var.set_output_samples(output_samples)) if CONF_MIN_INTEGRAL in params: cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL])) if CONF_MAX_INTEGRAL in params: cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL])) + deadband_output_samples = 1 if CONF_DEADBAND_PARAMETERS in config: params = config[CONF_DEADBAND_PARAMETERS] cg.add(var.set_threshold_low(params[CONF_THRESHOLD_LOW])) @@ -116,11 +122,11 @@ async def to_code(config): cg.add(var.set_kp_multiplier(params[CONF_KP_MULTIPLIER])) cg.add(var.set_ki_multiplier(params[CONF_KI_MULTIPLIER])) cg.add(var.set_kd_multiplier(params[CONF_KD_MULTIPLIER])) - cg.add( - var.set_deadband_output_samples( - params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES] - ) - ) + deadband_output_samples = params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES] + cg.add(var.set_deadband_output_samples(deadband_output_samples)) + + # Single shared output buffer sized to max of both modes + cg.add(var.init_output_buffer(max(output_samples, deadband_output_samples))) cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE])) diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index dc0a92efed..3708c29ff1 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -28,7 +28,11 @@ class PIDClimate : public climate::Climate, public Component { void set_min_integral(float min_integral) { controller_.min_integral_ = min_integral; } void set_max_integral(float max_integral) { controller_.max_integral_ = max_integral; } void set_output_samples(int in) { controller_.output_samples_ = in; } - void set_derivative_samples(int in) { controller_.derivative_samples_ = in; } + void set_derivative_samples(int in) { + controller_.derivative_samples_ = in; + if (in > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits) + controller_.derivative_window_.init(in); + } void set_threshold_low(float in) { controller_.threshold_low_ = in; } void set_threshold_high(float in) { controller_.threshold_high_ = in; } @@ -38,6 +42,10 @@ class PIDClimate : public climate::Climate, public Component { void set_starting_integral_term(float in) { controller_.set_starting_integral_term(in); } void set_deadband_output_samples(int in) { controller_.deadband_output_samples_ = in; } + void init_output_buffer(int size) { + if (size > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits) + controller_.output_window_.init(size); + } float get_output_value() const { return output_value_; } float get_error_value() const { return controller_.error_; } diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 5d7aecdb05..cab15331cd 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -21,9 +21,9 @@ float PIDController::update(float setpoint, float process_value) { // u(t) := p(t) + i(t) + d(t) float output = proportional_term_ + integral_term_ + derivative_term_; - // smooth/sample the output + // smooth/sample the output using shared buffer with mode-appropriate sample count int samples = in_deadband() ? deadband_output_samples_ : output_samples_; - return weighted_average_(output_list_, output, samples); + return ring_buffer_average_(output_window_, output, samples); } bool PIDController::in_deadband() { @@ -83,7 +83,7 @@ void PIDController::calculate_derivative_term_(float setpoint) { previous_setpoint_ = setpoint; // smooth the derivative samples - derivative = weighted_average_(derivative_list_, derivative, derivative_samples_); + derivative = ring_buffer_average_(derivative_window_, derivative, derivative_samples_); derivative_term_ = kd_ * derivative; @@ -93,25 +93,23 @@ void PIDController::calculate_derivative_term_(float setpoint) { } } -float PIDController::weighted_average_(std::deque &list, float new_value, int samples) { - // if only 1 sample needed, clear the list and return - if (samples == 1) { - list.clear(); +float PIDController::ring_buffer_average_(FixedRingBuffer &buf, float new_value, int max_samples) { + // if only 1 sample needed (or invalid), clear the buffer and return + if (max_samples <= 1) { + buf.clear(); return new_value; } - // add the new item to the list - list.push_front(new_value); + // Trim oldest entries to make room (handles mode-switching where buffer + // may have more entries than the current mode needs) + while (buf.size() >= static_cast(max_samples)) + buf.pop(); + buf.push(new_value); - // keep only 'samples' readings, by popping off the back of the list - while (samples > 0 && list.size() > static_cast(samples)) - list.pop_back(); - - // calculate and return the average of all values in the list float sum = 0; - for (auto &elem : list) - sum += elem; - return sum / list.size(); + for (auto val : buf) + sum += val; + return sum / buf.size(); } float PIDController::calculate_relative_time_() { diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index e2a7030b57..6848a23965 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -1,6 +1,7 @@ #pragma once + #include "esphome/core/hal.h" -#include +#include "esphome/core/helpers.h" #include namespace esphome { @@ -24,10 +25,10 @@ struct PIDController { /// Differential gain K_d. float kd_ = 0; - // smooth the derivative value using a weighted average over X samples - int derivative_samples_ = 8; + // smooth the derivative value using an average over X samples + int derivative_samples_ = 1; - /// smooth the output value using a weighted average over X values + /// smooth the output value using an average over X values int output_samples_ = 1; float threshold_low_ = 0.0f; @@ -50,7 +51,10 @@ struct PIDController { void calculate_proportional_term_(); void calculate_integral_term_(); void calculate_derivative_term_(float setpoint); - float weighted_average_(std::deque &list, float new_value, int samples); + + /// Ring buffer smoothing using FixedRingBuffer (single allocation at setup) + float ring_buffer_average_(FixedRingBuffer &buf, float new_value, int max_samples); + float calculate_relative_time_(); /// Error from previous update used for derivative term @@ -60,12 +64,12 @@ struct PIDController { float accumulated_integral_ = 0; uint32_t last_time_ = 0; - // this is a list of derivative values for smoothing. - std::deque derivative_list_; + // Ring buffer for derivative smoothing + FixedRingBuffer derivative_window_; - // this is a list of output values for smoothing. - std::deque output_list_; + // Ring buffer for output smoothing (shared between normal and deadband modes) + FixedRingBuffer output_window_; -}; // Struct PID Controller +}; // Struct PIDController } // namespace pid } // namespace esphome