[pid] Replace std::deque with FixedRingBuffer (#14733)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Thomas SAMTER
2026-03-13 22:38:45 +01:00
committed by GitHub
parent a6c08576be
commit 1eed1adfa0
4 changed files with 53 additions and 37 deletions
+15 -9
View File
@@ -57,7 +57,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_, cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_,
cv.Optional( cv.Optional(
CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1 CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1
): cv.int_, ): cv.positive_not_null_int,
} }
), ),
cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema( 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_STARTING_INTEGRAL_TERM, default=0.0): cv.float_,
cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_, cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_,
cv.Optional(CONF_MAX_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(
cv.Optional(CONF_OUTPUT_AVERAGING_SAMPLES, default=1): cv.int_, 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_starting_integral_term(params[CONF_STARTING_INTEGRAL_TERM]))
cg.add(var.set_derivative_samples(params[CONF_DERIVATIVE_AVERAGING_SAMPLES])) 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: if CONF_MIN_INTEGRAL in params:
cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL])) cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL]))
if CONF_MAX_INTEGRAL in params: if CONF_MAX_INTEGRAL in params:
cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL])) cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL]))
deadband_output_samples = 1
if CONF_DEADBAND_PARAMETERS in config: if CONF_DEADBAND_PARAMETERS in config:
params = config[CONF_DEADBAND_PARAMETERS] params = config[CONF_DEADBAND_PARAMETERS]
cg.add(var.set_threshold_low(params[CONF_THRESHOLD_LOW])) 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_kp_multiplier(params[CONF_KP_MULTIPLIER]))
cg.add(var.set_ki_multiplier(params[CONF_KI_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_kd_multiplier(params[CONF_KD_MULTIPLIER]))
cg.add( deadband_output_samples = params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES]
var.set_deadband_output_samples( cg.add(var.set_deadband_output_samples(deadband_output_samples))
params[CONF_DEADBAND_OUTPUT_AVERAGING_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])) cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE]))
+9 -1
View File
@@ -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_min_integral(float min_integral) { controller_.min_integral_ = min_integral; }
void set_max_integral(float max_integral) { controller_.max_integral_ = max_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_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_low(float in) { controller_.threshold_low_ = in; }
void set_threshold_high(float in) { controller_.threshold_high_ = 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_starting_integral_term(float in) { controller_.set_starting_integral_term(in); }
void set_deadband_output_samples(int in) { controller_.deadband_output_samples_ = 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_output_value() const { return output_value_; }
float get_error_value() const { return controller_.error_; } float get_error_value() const { return controller_.error_; }
+15 -17
View File
@@ -21,9 +21,9 @@ float PIDController::update(float setpoint, float process_value) {
// u(t) := p(t) + i(t) + d(t) // u(t) := p(t) + i(t) + d(t)
float output = proportional_term_ + integral_term_ + derivative_term_; 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_; 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() { bool PIDController::in_deadband() {
@@ -83,7 +83,7 @@ void PIDController::calculate_derivative_term_(float setpoint) {
previous_setpoint_ = setpoint; previous_setpoint_ = setpoint;
// smooth the derivative samples // 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; derivative_term_ = kd_ * derivative;
@@ -93,25 +93,23 @@ void PIDController::calculate_derivative_term_(float setpoint) {
} }
} }
float PIDController::weighted_average_(std::deque<float> &list, float new_value, int samples) { float PIDController::ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples) {
// if only 1 sample needed, clear the list and return // if only 1 sample needed (or invalid), clear the buffer and return
if (samples == 1) { if (max_samples <= 1) {
list.clear(); buf.clear();
return new_value; return new_value;
} }
// add the new item to the list // Trim oldest entries to make room (handles mode-switching where buffer
list.push_front(new_value); // may have more entries than the current mode needs)
while (buf.size() >= static_cast<size_t>(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<size_t>(samples))
list.pop_back();
// calculate and return the average of all values in the list
float sum = 0; float sum = 0;
for (auto &elem : list) for (auto val : buf)
sum += elem; sum += val;
return sum / list.size(); return sum / buf.size();
} }
float PIDController::calculate_relative_time_() { float PIDController::calculate_relative_time_() {
+14 -10
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include <deque> #include "esphome/core/helpers.h"
#include <cmath> #include <cmath>
namespace esphome { namespace esphome {
@@ -24,10 +25,10 @@ struct PIDController {
/// Differential gain K_d. /// Differential gain K_d.
float kd_ = 0; float kd_ = 0;
// smooth the derivative value using a weighted average over X samples // smooth the derivative value using an average over X samples
int derivative_samples_ = 8; 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; int output_samples_ = 1;
float threshold_low_ = 0.0f; float threshold_low_ = 0.0f;
@@ -50,7 +51,10 @@ struct PIDController {
void calculate_proportional_term_(); void calculate_proportional_term_();
void calculate_integral_term_(); void calculate_integral_term_();
void calculate_derivative_term_(float setpoint); void calculate_derivative_term_(float setpoint);
float weighted_average_(std::deque<float> &list, float new_value, int samples);
/// Ring buffer smoothing using FixedRingBuffer (single allocation at setup)
float ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples);
float calculate_relative_time_(); float calculate_relative_time_();
/// Error from previous update used for derivative term /// Error from previous update used for derivative term
@@ -60,12 +64,12 @@ struct PIDController {
float accumulated_integral_ = 0; float accumulated_integral_ = 0;
uint32_t last_time_ = 0; uint32_t last_time_ = 0;
// this is a list of derivative values for smoothing. // Ring buffer for derivative smoothing
std::deque<float> derivative_list_; FixedRingBuffer<float> derivative_window_;
// this is a list of output values for smoothing. // Ring buffer for output smoothing (shared between normal and deadband modes)
std::deque<float> output_list_; FixedRingBuffer<float> output_window_;
}; // Struct PID Controller }; // Struct PIDController
} // namespace pid } // namespace pid
} // namespace esphome } // namespace esphome