[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_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]))
+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_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_; }
+15 -17
View File
@@ -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<float> &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<float> &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<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;
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_() {
+14 -10
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/hal.h"
#include <deque>
#include "esphome/core/helpers.h"
#include <cmath>
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<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_();
/// 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<float> derivative_list_;
// Ring buffer for derivative smoothing
FixedRingBuffer<float> derivative_window_;
// this is a list of output values for smoothing.
std::deque<float> output_list_;
// Ring buffer for output smoothing (shared between normal and deadband modes)
FixedRingBuffer<float> output_window_;
}; // Struct PID Controller
}; // Struct PIDController
} // namespace pid
} // namespace esphome