Skip to content

Commit

Permalink
feat(thermocycler-gen2): add filtering to peltier output (#435)
Browse files Browse the repository at this point in the history
* Added a PeltierFilter class to constrain peltier outputs during closed loop control
  • Loading branch information
fsinapi authored Mar 9, 2023
1 parent 0420835 commit cff6451
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @file peltier_filter.hpp
* @brief Implements a simple filter on the output power of a peltier to
* enforce a maximum ∆power/sec limit.
*/

#pragma once

#include "systemwide.h"

namespace peltier_filter {

using PowerPerSec = double;

/** Number of seconds in 100ms */
static constexpr double ONE_HUNDRED_MS = 0.1;
/**
* Maximum rate of change is -100% to 100% in one hundred milliseconds.
* This effectively means that changing from max cooling to max heating
* will take 100ms.
*/
static constexpr PowerPerSec MAX_DELTA = (2.0 / ONE_HUNDRED_MS);

/**
* Provides a simple filter on Peltier power values to ease the stress on
* on the peltiers over their lifetime.
*/
class PeltierFilter {
public:
/**
* @brief Reset the filter. This should be called whenever a peltier is
* disabled.
*/
auto reset() -> void;

/**
* @brief Set a new peltier power value and filter it based on the
* last value that was set.
*
* @param setting The desired power, in the range [-1.0, 1.0]
* @param delta_sec The number of seconds that have elapsed since the
* last setting.
* @return The power that should be set on the peltier.
*/
[[nodiscard]] auto set_filtered(double setting, double delta_sec) -> double;

/**
* @brief Get the last filtered setting for this peltier.
*
* @return The most recent filtered setting
*/
[[nodiscard]] auto get_last() const -> double;

private:
/** The last setting for this peltier.*/
double _last = 0.0;
};

} // namespace peltier_filter
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "core/thermistor_conversion.hpp"
#include "systemwide.h"
#include "thermocycler-gen2/errors.hpp"
#include "thermocycler-gen2/peltier_filter.hpp"

namespace thermal_general {

Expand Down Expand Up @@ -60,6 +61,8 @@ struct Peltier {
ThermistorPair thermistors; // Links to the front & back thermistors
// NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
PID pid; // Current PID loop
// NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
peltier_filter::PeltierFilter filter = peltier_filter::PeltierFilter();

/** Get the current temperature of this peltier.*/
[[nodiscard]] auto current_temp() const -> double {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ class ThermalPlateTask {
// We entered an error state. Disable power output.
_state.system_status = State::ERROR;
policy.set_enabled(false);
reset_peltier_filters();
} else {
// We went from an error state to no error state... so go idle
_state.system_status = State::IDLE;
Expand Down Expand Up @@ -349,6 +350,7 @@ class ThermalPlateTask {
// Not an `else` so we can immediately resolve any issue setting outputs
if (_state.system_status == State::ERROR) {
policy.set_enabled(false);
reset_peltier_filters();
}

// Cache the timestamp from this message so the time difference for
Expand Down Expand Up @@ -454,6 +456,7 @@ class ThermalPlateTask {
enabled = false;
}
policy.set_enabled(enabled);
reset_peltier_filters();
_state.system_status = (enabled) ? State::PWM_TEST : State::IDLE;

if (!ok) {
Expand Down Expand Up @@ -534,6 +537,7 @@ class ThermalPlateTask {
ret = policy.set_peltier(_peltier_center.id, 0.0F,
PeltierDirection::PELTIER_HEATING);
}
reset_peltier_filters();
if (!ret) {
policy.set_enabled(false);
response.with_error = errors::ErrorCode::THERMAL_PELTIER_ERROR;
Expand All @@ -551,6 +555,7 @@ class ThermalPlateTask {
if (msg.setpoint <= 0.0F) {
_state.system_status = State::IDLE;
policy.set_enabled(false);
reset_peltier_filters();
} else {
if (_plate_control.set_new_target(msg.setpoint, volume_ul,
msg.hold_time)) {
Expand Down Expand Up @@ -578,6 +583,7 @@ class ThermalPlateTask {
}

policy.set_enabled(false);
reset_peltier_filters();
_state.system_status = State::IDLE;

if (msg.from_system) {
Expand All @@ -596,6 +602,7 @@ class ThermalPlateTask {
messages::DeactivateAllResponse{.responding_to_id = msg.id};

policy.set_enabled(false);
reset_peltier_filters();
if (_state.system_status != State::ERROR) {
_state.system_status = State::IDLE;
}
Expand Down Expand Up @@ -851,15 +858,16 @@ class ThermalPlateTask {
auto ret = values.has_value();
if (ret) {
ret = set_peltier_power(_peltier_left, values.value().left_power,
policy);
elapsed_time, policy);
}
if (ret) {
ret = set_peltier_power(_peltier_right, values.value().right_power,
policy);
elapsed_time, policy);
}
if (ret) {
ret = set_peltier_power(_peltier_center,
values.value().center_power, policy);
ret =
set_peltier_power(_peltier_center, values.value().center_power,
elapsed_time, policy);
}
if (!ret) {
policy.set_enabled(false);
Expand All @@ -881,19 +889,24 @@ class ThermalPlateTask {
}

/**
* @brief Updates the power of a peltier. Calculates a new PID value and
* updates the power output. The \c temp_current field in the peltier
* must be updated before invoking this function.
* @brief Updates the power of a peltier, and intended to be called for
* closed-loop control. Accepts a power setting, applies a small filter,
* and updates the PWM to the peltier.
*
* @tparam Policy Provides platform-specific control mechanisms
* @param[in] peltier The peltier to update
* @param[in] power The power to set this peltier to. This power will
* be filtered and may not reflect the actual setting sent to the PWM!
* @param[in] elapsed_time The time (in seconds) that has passed since
* the last control update
* @param[in] policy Instance of the platform policy
* @return True on success, false if an error occurs
*/
template <ThermalPlateExecutionPolicy Policy>
auto set_peltier_power(Peltier& peltier, double power, Policy& policy)
-> bool {
auto set_peltier_power(Peltier& peltier, double power, Seconds elapsed_time,
Policy& policy) -> bool {
auto direction = PeltierDirection::PELTIER_HEATING;
power = peltier.filter.set_filtered(power, elapsed_time.count());
if (power < 0.0F) {
// The set_peltier function takes a *positive* percentage and a
// direction
Expand Down Expand Up @@ -969,6 +982,12 @@ class ThermalPlateTask {
return (const_a * heatsink_temp) + ((1.0F + const_b) * temp) + const_c;
}

auto reset_peltier_filters() {
_peltier_left.filter.reset();
_peltier_right.filter.reset();
_peltier_center.filter.reset();
}

Queue& _message_queue;
tasks::Tasks<QueueImpl>* _task_registry;
std::array<Thermistor, PLATE_THERM_COUNT> _thermistors;
Expand Down
1 change: 1 addition & 0 deletions stm32-modules/thermocycler-gen2/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ endif()
set(CORE_LINTABLE_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/errors.cpp
${CMAKE_CURRENT_SOURCE_DIR}/plate_control.cpp
${CMAKE_CURRENT_SOURCE_DIR}/peltier_filter.cpp
${CMAKE_CURRENT_SOURCE_DIR}/board_revision.cpp
${CMAKE_CURRENT_SOURCE_DIR}/colors.cpp
${CMAKE_CURRENT_SOURCE_DIR}/motor_utils.cpp)
Expand Down
23 changes: 23 additions & 0 deletions stm32-modules/thermocycler-gen2/src/peltier_filter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include "thermocycler-gen2/peltier_filter.hpp"

#include <algorithm>
#include <cstdlib>

using namespace peltier_filter;

auto PeltierFilter::reset() -> void { _last = 0.0F; }

[[nodiscard]] auto PeltierFilter::set_filtered(double setting, double delta_sec)
-> double {
setting = std::clamp(setting, -1.0, 1.0);
const auto max_change = delta_sec * MAX_DELTA;
if (std::abs(setting - _last) > max_change) {
// Just change by the max change for this tick
auto change = (setting > _last) ? max_change : -max_change;
setting = _last + change;
}
_last = setting;
return _last;
}

[[nodiscard]] auto PeltierFilter::get_last() const -> double { return _last; }
1 change: 1 addition & 0 deletions stm32-modules/thermocycler-gen2/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ add_executable(${TARGET_MODULE_NAME}
test_system_pulse.cpp
test_thermal_plate_task.cpp
test_plate_control.cpp
test_peltier_filter.cpp
test_tmc2130.cpp
test_board_revision_hardware.cpp
test_board_revision.cpp
Expand Down
50 changes: 50 additions & 0 deletions stm32-modules/thermocycler-gen2/tests/test_peltier_filter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

#include <vector>

#include "catch2/catch.hpp"
#include "thermocycler-gen2/peltier_filter.hpp"

TEST_CASE("peltier filter functionality") {
using namespace peltier_filter;
auto subject = PeltierFilter();
REQUIRE(subject.get_last() == 0.0F);
WHEN("setting power outside of the filter limits") {
const auto TIME_DELTA = GENERATE(0.01, 0.02);
const auto SETTING = GENERATE(1.0, -1.0);

auto result = subject.set_filtered(SETTING, TIME_DELTA);
THEN("the result is filtered") {
auto expected = MAX_DELTA * TIME_DELTA * (SETTING > 0 ? 1 : -1);
REQUIRE_THAT(result, Catch::Matchers::WithinAbs(expected, 0.01));
AND_WHEN("doing it again") {
result = subject.set_filtered(SETTING, TIME_DELTA);
THEN("the result is doubled") {
REQUIRE_THAT(
result, Catch::Matchers::WithinAbs(expected * 2, 0.01));
}
}
}
THEN("getting the last result matches expected") {
REQUIRE(subject.get_last() == result);
}
}
WHEN("setting power within filter limits") {
const auto TIME_DELTA = GENERATE(0.1, 0.5, 1.0);
const auto SETTING = GENERATE(1.0, -1.0, -0.245, 0.64);
auto result = subject.set_filtered(SETTING, TIME_DELTA);
THEN("the result is not filtered at all") {
REQUIRE_THAT(result, Catch::Matchers::WithinAbs(SETTING, 0.01));
}
}
WHEN("setting power at 10ms intervals") {
const auto TIME_DELTA = 0.01;
THEN("it increments as expected") {
std::vector<double> expected = {0.2, 0.4, 0.6, 0.8, 1.0, 1.0};
std::vector<double> result;
for (auto i = 0; i < 6; ++i) {
result.push_back(subject.set_filtered(1.0, TIME_DELTA));
}
REQUIRE_THAT(result, Catch::Matchers::Approx(expected));
}
}
}

0 comments on commit cff6451

Please sign in to comment.