diff options
| author | Patrick Venture <venture@google.com> | 2018-03-08 08:21:38 -0800 |
|---|---|---|
| committer | Patrick Venture <venture@google.com> | 2018-03-08 08:23:45 -0800 |
| commit | d80121817bb771b305f1db6aacc3ad71611a3e58 (patch) | |
| tree | 1cd48911f32fa665185cd3f6c32f0da565c514a7 | |
| parent | a788899e4747960914ed850fa7155981b9cb9de7 (diff) | |
| download | phosphor-pid-control-d80121817bb771b305f1db6aacc3ad71611a3e58.tar.gz phosphor-pid-control-d80121817bb771b305f1db6aacc3ad71611a3e58.zip | |
PID Objects & Algo
These are the PID controller implementations for fans,
and thermals. This also includes the PID algorithm used.
Change-Id: I30471fbf7a8a7ed65f78bf105970d62815fedc56
Signed-off-by: Patrick Venture <venture@google.com>
| -rw-r--r-- | pid/README | 11 | ||||
| -rw-r--r-- | pid/controller.cpp | 48 | ||||
| -rw-r--r-- | pid/controller.hpp | 58 | ||||
| -rw-r--r-- | pid/ec/pid.cpp | 118 | ||||
| -rw-r--r-- | pid/ec/pid.hpp | 53 | ||||
| -rw-r--r-- | pid/fan.hpp | 9 | ||||
| -rw-r--r-- | pid/fancontroller.cpp | 146 | ||||
| -rw-r--r-- | pid/fancontroller.hpp | 46 | ||||
| -rw-r--r-- | pid/pidthread.cpp | 107 | ||||
| -rw-r--r-- | pid/pidthread.hpp | 6 | ||||
| -rw-r--r-- | pid/thermalcontroller.cpp | 76 | ||||
| -rw-r--r-- | pid/thermalcontroller.hpp | 39 | ||||
| -rw-r--r-- | pid/util.cpp | 57 | ||||
| -rw-r--r-- | pid/util.hpp | 12 | ||||
| -rw-r--r-- | pid/zone.cpp | 553 | ||||
| -rw-r--r-- | pid/zone.hpp | 124 |
16 files changed, 1463 insertions, 0 deletions
diff --git a/pid/README b/pid/README new file mode 100644 index 0000000..93e48f3 --- /dev/null +++ b/pid/README @@ -0,0 +1,11 @@ +ThermalControllers and FanControllers are derived objects from a common PID +Controller object. The design implemented in this structure is a facsimile of +what was published in the Chrome OS source. + +One has any number of ThermalControllers that run through a PID step to +generate a set-point RPM to reach its thermal set-point. The maximum ouput +from the set of ThermalControllers is taken as the input to all the +FanController PID loops. + +Each group of these controllers is managed within a zone. A PIDZone object +helps manage them by providing a sensor value cache and overall execution. diff --git a/pid/controller.cpp b/pid/controller.cpp new file mode 100644 index 0000000..a5360fe --- /dev/null +++ b/pid/controller.cpp @@ -0,0 +1,48 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <algorithm> +#include <chrono> +#include <iostream> +#include <map> +#include <memory> +#include <thread> +#include <vector> + +#include "controller.hpp" +#include "ec/pid.hpp" + + +void PIDController::pid_process(void) +{ + float input; + float setpt; + float output; + + // Get setpt value + setpt = setpt_proc(); + + // Get input value + input = input_proc(); + + // Calculate new output + output = ec::pid(get_pid_info(), input, setpt); + + // Output new value + output_proc(output); + + return; +} diff --git a/pid/controller.hpp b/pid/controller.hpp new file mode 100644 index 0000000..0b1de3c --- /dev/null +++ b/pid/controller.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include <memory> +#include <vector> + +#include "fan.hpp" +#include "ec/pid.hpp" + +class PIDZone; + +/* + * Base class for PID controllers. Each PID that implements this needs to + * provide an input_proc, setpt_proc, and output_proc. + */ +class PIDController +{ + public: + PIDController(const std::string& id, std::shared_ptr<PIDZone> owner) + : _owner(owner), + _id(id) + { } + + virtual ~PIDController() { } + + virtual float input_proc(void) = 0; + virtual float setpt_proc(void) = 0; + virtual void output_proc(float value) = 0; + + void pid_process(void); + + std::string get_id(void) + { + return _id; + } + float get_setpoint(void) + { + return _setpoint; + } + void set_setpoint(float setpoint) + { + _setpoint = setpoint; + } + + ec::pid_info_t* get_pid_info(void) + { + return &_pid_info; + } + + protected: + std::shared_ptr<PIDZone> _owner; + + private: + // parameters + ec::pid_info_t _pid_info; + float _setpoint; + std::string _id; +}; + diff --git a/pid/ec/pid.cpp b/pid/ec/pid.cpp new file mode 100644 index 0000000..a1c4e41 --- /dev/null +++ b/pid/ec/pid.cpp @@ -0,0 +1,118 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pid.hpp" + +namespace ec +{ + +/******************************** + * clamp + * + */ +static float clamp(float x, float min, float max) +{ + if (x < min) + { + return min; + } + else if (x > max) + { + return max; + } + return x; +} + +/******************************** + * pid code + * Note: Codes assumes the ts field is non-zero + */ +float pid(pid_info_t* pidinfoptr, float input, float setpoint) +{ + float error; + + float p_term = 0.0f; + float i_term = 0.0f; + float ff_term = 0.0f; + + float output; + + // calculate P, I, D, FF + + // Pid + error = setpoint - input; + p_term = pidinfoptr->p_c * error; + + // pId + if (0.0f != pidinfoptr->i_c) + { + i_term = pidinfoptr->integral; + i_term += error * pidinfoptr->i_c * pidinfoptr->ts; + i_term = clamp(i_term, pidinfoptr->i_lim.min, pidinfoptr->i_lim.max); + } + + // FF + ff_term = (setpoint + pidinfoptr->ff_off) * pidinfoptr->ff_gain; + + output = p_term + i_term + ff_term; + output = clamp(output, pidinfoptr->out_lim.min, pidinfoptr->out_lim.max); + + // slew rate + // TODO(aarena) - Simplify logic as Andy suggested by creating dynamic + // out_lim_min/max that are affected by slew rate control and just clamping + // to those instead of effectively clamping twice. + if (pidinfoptr->initialized) + { + if (pidinfoptr->slew_neg != 0.0f) + { + // Don't decrease too fast + float min_out = pidinfoptr->last_output + pidinfoptr->slew_neg * + pidinfoptr->ts; + if (output < min_out) + { + output = min_out; + } + } + if (pidinfoptr->slew_pos != 0.0f) + { + // Don't increase too fast + float max_out = pidinfoptr->last_output + pidinfoptr->slew_pos * + pidinfoptr->ts; + if (output > max_out) + { + output = max_out; + } + } + + if (pidinfoptr->slew_neg != 0.0f || pidinfoptr->slew_pos != 0.0f) + { + // Back calculate integral term for the cases where we limited the + // output + i_term = output - p_term; + } + } + + // Clamp again because having limited the output may result in a + // larger integral term + i_term = clamp(i_term, pidinfoptr->i_lim.min, pidinfoptr->i_lim.max); + pidinfoptr->integral = i_term; + pidinfoptr->initialized = true; + pidinfoptr->last_output = output; + + return output; +} + +} diff --git a/pid/ec/pid.hpp b/pid/ec/pid.hpp new file mode 100644 index 0000000..e138933 --- /dev/null +++ b/pid/ec/pid.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include <cstdint> + +namespace ec +{ + +typedef struct +{ + float min; + float max; +} limits_t; + +/* Note: If you update these structs you need to update the copy code in + * pid/util.cpp. + */ +typedef struct +{ + bool initialized; // has pid been initialized + + float ts; // sample time in seconds + float integral; // intergal of error + float last_output; // value of last output + + float p_c; // coeff for P + float i_c; // coeff for I + float ff_off; // offset coeff for feed-forward term + float ff_gain; // gain for feed-forward term + + limits_t i_lim; // clamp of integral + limits_t out_lim; // clamp of output + float slew_neg; + float slew_pos; +} pid_info_t; + +float pid(pid_info_t* pidinfoptr, float input, float setpoint); + +/* Condensed version for use by the configuration. */ +struct pidinfo +{ + float ts; // sample time in seconds + float p_c; // coeff for P + float i_c; // coeff for I + float ff_off; // offset coeff for feed-forward term + float ff_gain; // gain for feed-forward term + ec::limits_t i_lim; // clamp of integral + ec::limits_t out_lim; // clamp of output + float slew_neg; + float slew_pos; +}; + + +} diff --git a/pid/fan.hpp b/pid/fan.hpp new file mode 100644 index 0000000..7792d04 --- /dev/null +++ b/pid/fan.hpp @@ -0,0 +1,9 @@ + +#pragma once + +enum class FanSpeedDirection +{ + DOWN, + UP, + NEUTRAL, /* not sure this will ever happen, but for completeness. */ +}; diff --git a/pid/fancontroller.cpp b/pid/fancontroller.cpp new file mode 100644 index 0000000..b5949a8 --- /dev/null +++ b/pid/fancontroller.cpp @@ -0,0 +1,146 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <algorithm> +#include <iostream> + +#include "fancontroller.hpp" +#include "util.hpp" +#include "zone.hpp" + +std::unique_ptr<PIDController> FanController::CreateFanPid( + std::shared_ptr<PIDZone> owner, + const std::string& id, + std::vector<std::string>& inputs, + ec::pidinfo initial) +{ + auto fan = std::make_unique<FanController>(id, inputs, owner); + ec::pid_info_t* info = fan->get_pid_info(); + + InitializePIDStruct(info, &initial); + + return fan; +} + +float FanController::input_proc(void) +{ + float sum = 0; + double value = 0; + std::vector<int64_t> values; + std::vector<int64_t>::iterator result; + + try + { + for (const auto& name : _inputs) + { + value = _owner->getCachedValue(name); + /* If we have a fan we can't read, its value will be 0 for at least + * some boards, while others... the fan will drop off dbus (if + * that's how it's being read and in that case its value will never + * be updated anymore, which is relatively harmless, except, when + * something tries to read its value through IPMI, and can't, they + * sort of have to guess -- all the other fans are reporting, why + * not this one? Maybe it's unable to be read, so it's "bad." + */ + if (value > 0) + { + values.push_back(value); + sum += value; + } + } + } + catch (const std::exception& e) + { + std::cerr << "exception on input_proc.\n"; + throw; + } + + if (values.size() > 0) + { + /* When this is a configuration option in a later update, this code + * will be updated. + */ + //value = sum / _inputs.size(); + + /* the fan PID algorithm was unstable with average, and seemed to work + * better with minimum. I had considered making this choice a variable + * in the configuration, and I will. + */ + result = std::min_element(values.begin(), values.end()); + value = *result; + } + + return static_cast<float>(value); +} + +float FanController::setpt_proc(void) +{ + float maxRPM = _owner->getMaxRPMRequest(); + + // store for reference, and check if more or less. + float prev = get_setpoint(); + + if (maxRPM > prev) + { + setFanDirection(FanSpeedDirection::UP); + } + else if (prev > maxRPM) + { + setFanDirection(FanSpeedDirection::DOWN); + } + else + { + setFanDirection(FanSpeedDirection::NEUTRAL); + } + + set_setpoint(maxRPM); + + return (maxRPM); +} + +void FanController::output_proc(float value) +{ + float percent = value; + + /* If doing tuning logging, don't go into failsafe mode. */ +#ifndef __TUNING_LOGGING__ + if (_owner->getFailSafeMode()) + { + /* In case it's being set to 100% */ + if (percent < _owner->getFailSafePercent()) + { + percent = _owner->getFailSafePercent(); + } + } +#endif + + // value and kFanFailSafeDutyCycle are 10 for 10% so let's fix that. + percent /= 100; + + // PidSensorMap for writing. + for (auto& it : _inputs) + { + auto& sensor = _owner->getSensor(it); + sensor->write(static_cast<double>(percent)); + } + +#ifdef __TUNING_LOGGING__ + _owner->getLogHandle() << "," << percent; +#endif + + return; +} + diff --git a/pid/fancontroller.hpp b/pid/fancontroller.hpp new file mode 100644 index 0000000..0816991 --- /dev/null +++ b/pid/fancontroller.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include <memory> +#include <string> +#include <vector> + +#include "controller.hpp" +#include "fan.hpp" +#include "ec/pid.hpp" + + +/* + * A FanController is a PID controller that reads a number of fans and given + * the output then tries to set them to the goal values set by the thermal + * controllers. + */ +class FanController : public PIDController +{ + public: + static std::unique_ptr<PIDController> CreateFanPid( + std::shared_ptr<PIDZone> owner, + const std::string& id, + std::vector<std::string>& inputs, + ec::pidinfo initial); + + FanController(const std::string& id, + std::vector<std::string>& inputs, + std::shared_ptr<PIDZone> owner) + : PIDController(id, owner), + _inputs(inputs), + _direction(FanSpeedDirection::NEUTRAL) + { } + + float input_proc(void) override; + float setpt_proc(void) override; + void output_proc(float value) override; + + void setFanDirection(FanSpeedDirection direction) + { + _direction = direction; + }; + + private: + std::vector<std::string> _inputs; + FanSpeedDirection _direction; +}; diff --git a/pid/pidthread.cpp b/pid/pidthread.cpp new file mode 100644 index 0000000..38b2516 --- /dev/null +++ b/pid/pidthread.cpp @@ -0,0 +1,107 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <chrono> +#include <map> +#include <memory> +#include <thread> +#include <vector> + +#include "pidthread.hpp" + +#include "pid/controller.hpp" +#include "sensors/sensor.hpp" + + +static void ProcessThermals(std::shared_ptr<PIDZone> zone) +{ + // Get the latest margins. + zone->updateSensors(); + // Zero out the RPM set point goals. + zone->clearRPMSetPoints(); + // Run the margin PIDs. + zone->process_thermals(); + // Get the maximum RPM set-point. + zone->determineMaxRPMRequest(); +} + + +void PIDControlThread(std::shared_ptr<PIDZone> zone) +{ + int ms100cnt = 0; + /* + * This should sleep on the conditional wait for the listen thread to tell + * us it's in sync. But then we also need a timeout option in case + * phosphor-hwmon is down, we can go into some weird failure more. + * + * Another approach would be to start all sensors in worst-case values, + * and fail-safe mode and then clear out of fail-safe mode once we start + * getting values. Which I think it is a solid approach. + * + * For now this runs before it necessarily has any sensor values. For the + * host sensors they start out in fail-safe mode. For the fans, they start + * out as 0 as input and then are adjusted once they have values. + * + * If a fan has failed, it's value will be whatever we're told or however + * we retrieve it. This program disregards fan values of 0, so any code + * providing a fan speed can set to 0 on failure and that fan value will be + * effectively ignored. The PID algorithm will be unhappy but nothing bad + * will happen. + * + * TODO(venture): If the fan value is 0 should that loop just be skipped? + * Right now, a 0 value is ignored in FanController::input_proc() + */ +#ifdef __TUNING_LOGGING__ + zone->initializeLog(); +#endif + zone->initializeCache(); + ProcessThermals(zone); + + while (true) + { + using namespace std::literals::chrono_literals; + std::this_thread::sleep_for(0.1s); + + // Check if we should just go back to sleep. + if (zone->getManualMode()) + { + continue; + } + + // Get the latest fan speeds. + zone->updateFanTelemetry(); + + if (10 <= ms100cnt) + { + ms100cnt = 0; + + ProcessThermals(zone); + } + + // Run the fan PIDs every iteration. + zone->process_fans(); + +#ifdef __TUNING_LOGGING__ + zone->getLogHandle() << std::endl; +#endif + + ms100cnt += 1; + } + + return; +} + + diff --git a/pid/pidthread.hpp b/pid/pidthread.hpp new file mode 100644 index 0000000..670d558 --- /dev/null +++ b/pid/pidthread.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include "pid/zone.hpp" + +/* Given a zone, run through the loops. */ +void PIDControlThread(std::shared_ptr<PIDZone> zone); diff --git a/pid/thermalcontroller.cpp b/pid/thermalcontroller.cpp new file mode 100644 index 0000000..c44503f --- /dev/null +++ b/pid/thermalcontroller.cpp @@ -0,0 +1,76 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "thermalcontroller.hpp" +#include "util.hpp" +#include "zone.hpp" + + +std::unique_ptr<PIDController> ThermalController::CreateThermalPid( + std::shared_ptr<PIDZone> owner, + const std::string& id, + std::vector<std::string>& inputs, + float setpoint, + ec::pidinfo initial) +{ + auto thermal = std::make_unique<ThermalController>(id, inputs, owner); + + ec::pid_info_t* info = thermal->get_pid_info(); + thermal->set_setpoint(setpoint); + + InitializePIDStruct(info, &initial); + + return thermal; +} + +//bmc_host_sensor_value_float +float ThermalController::input_proc(void) +{ + /* + * This only supports one thermal input because it doesn't yet know how to + * handle merging them, probably max? + */ + double value = _owner->getCachedValue(_inputs.at(0)); + return static_cast<float>(value); +} + +// bmc_get_setpt +float ThermalController::setpt_proc(void) +{ + float setpoint = get_setpoint(); + + /* TODO(venture): Thermal setpoint invalid? */ +#if 0 + if (-1 == setpoint) + { + return 0.0f; + } + else + { + return setpoint; + } +#endif + return setpoint; +} + +// bmc_set_pid_output +void ThermalController::output_proc(float value) +{ + _owner->addRPMSetPoint(value); + + return; +} + diff --git a/pid/thermalcontroller.hpp b/pid/thermalcontroller.hpp new file mode 100644 index 0000000..32616ac --- /dev/null +++ b/pid/thermalcontroller.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include <memory> +#include <string> +#include <vector> + +#include "controller.hpp" +#include "ec/pid.hpp" + + +/* + * A ThermalController is a PID controller that reads a number of sensors and + * provides the set-points for the fans. + */ +class ThermalController : public PIDController +{ + public: + static std::unique_ptr<PIDController> CreateThermalPid( + std::shared_ptr<PIDZone> owner, + const std::string& id, + std::vector<std::string>& inputs, + float setpoint, + ec::pidinfo initial); + + ThermalController(const std::string& id, + std::vector<std::string>& inputs, + std::shared_ptr<PIDZone> owner) + : PIDController(id, owner), + _inputs(inputs) + { } + + float input_proc(void) override; + float setpt_proc(void) override; + void output_proc(float value) override; + + private: + std::vector<std::string> _inputs; +}; + diff --git a/pid/util.cpp b/pid/util.cpp new file mode 100644 index 0000000..79da7e1 --- /dev/null +++ b/pid/util.cpp @@ -0,0 +1,57 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <cstring> +#include <iostream> + +#include "ec/pid.hpp" + +void InitializePIDStruct(ec::pid_info_t* info, ec::pidinfo* initial) +{ + std::memset(info, 0x00, sizeof(ec::pid_info_t)); + + info->ts = initial->ts; + info->p_c = initial->p_c; + info->i_c = initial->i_c; + info->ff_off = initial->ff_off; + info->ff_gain = initial->ff_gain; + info->i_lim.min = initial->i_lim.min; + info->i_lim.max = initial->i_lim.max; + info->out_lim.min = initial->out_lim.min; + info->out_lim.max = initial->out_lim.max; + info->slew_neg = initial->slew_neg; + info->slew_pos = initial->slew_pos; +} + +void DumpPIDStruct(ec::pid_info_t *info) +{ + std::cerr << " ts: " << info->ts + << " p_c: " << info->p_c + << " i_c: " << info->i_c + << " ff_off: " << info->ff_off + << " ff_gain: " << info->ff_gain + << " i_lim.min: " << info->i_lim.min + << " i_lim.max: " << info->i_lim.max + << " out_lim.min: " << info->out_lim.min + << " out_lim.max: " << info->out_lim.max + << " slew_neg: " << info->slew_neg + << " slew_pos: " << info->slew_pos + << " last_output: " << info->last_output + << " integral: " << info->integral + << std::endl; + + return; +} diff --git a/pid/util.hpp b/pid/util.hpp new file mode 100644 index 0000000..eb6e713 --- /dev/null +++ b/pid/util.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "ec/pid.hpp" + + +/* + * Given a configuration structure, fill out the information we use within the + * PID loop. + */ +void InitializePIDStruct(ec::pid_info_t* info, ec::pidinfo* initial); + +void DumpPIDStruct(ec::pid_info_t *info); diff --git a/pid/zone.cpp b/pid/zone.cpp new file mode 100644 index 0000000..04b3176 --- /dev/null +++ b/pid/zone.cpp @@ -0,0 +1,553 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Configuration. */ +#include "conf.hpp" + +#include "zone.hpp" + +#include <algorithm> +#include <chrono> +#include <cstring> +#include <fstream> +#include <iostream> +#include <libconfig.h++> +#include <memory> + +#include "pid/controller.hpp" +#include "pid/fancontroller.hpp" +#include "pid/thermalcontroller.hpp" +#include "pid/ec/pid.hpp" + + +using tstamp = std::chrono::high_resolution_clock::time_point; +using namespace std::literals::chrono_literals; + +static constexpr bool deferSignals = true; +static constexpr auto objectPath = "/xyz/openbmc_project/settings/fanctrl/zone"; + +float PIDZone::getMaxRPMRequest(void) const +{ + return _maximumRPMSetPt; +} + +bool PIDZone::getManualMode(void) const +{ + return _manualMode; +} + +void PIDZone::setManualMode(bool mode) +{ + _manualMode = mode; +} + +bool PIDZone::getFailSafeMode(void) const +{ + // If any keys are present at least one sensor is in fail safe mode. + return !_failSafeSensors.empty(); +} + +int64_t PIDZone::getZoneId(void) const +{ + return _zoneId; +} + +void PIDZone::addRPMSetPoint(float setpoint) +{ + _RPMSetPoints.push_back(setpoint); +} + +void PIDZone::clearRPMSetPoints(void) +{ + _RPMSetPoints.clear(); +} + +float PIDZone::getFailSafePercent(void) const +{ + return _failSafePercent; +} + +float PIDZone::getMinThermalRpmSetPt(void) const +{ + return _minThermalRpmSetPt; +} + +void PIDZone::addFanPID(std::unique_ptr<PIDController> pid) +{ + _fans.push_back(std::move(pid)); +} + +void PIDZone::addThermalPID(std::unique_ptr<PIDController> pid) +{ + _thermals.push_back(std::move(pid)); +} + +double PIDZone::getCachedValue(const std::string& name) +{ + return _cachedValuesByName.at(name); +} + +void PIDZone::addFanInput(std::string fan) +{ + _fanInputs.push_back(fan); +} + +void PIDZone::addThermalInput(std::string therm) +{ + _thermalInputs.push_back(therm); +} + +void PIDZone::determineMaxRPMRequest(void) +{ + float max = 0; + std::vector<float>::iterator result; + + if (_RPMSetPoints.size() > 0) + { + result = std::max_element(_RPMSetPoints.begin(), _RPMSetPoints.end()); + max = *result; + } + + /* + * If the maximum RPM set-point output is below the minimum RPM + * set-point, set it to the minimum. + */ + max = std::max(getMinThermalRpmSetPt(), max); + +#ifdef __TUNING_LOGGING__ + /* + * We received no set-points from thermal sensors. + * This is a case experienced during tuning where they only specify + * fan sensors and one large fan PID for all the fans. + */ + static constexpr auto setpointpath = "/etc/thermal.d/set-point"; + try + { + int value; + std::ifstream ifs; + ifs.open(setpointpath); + if (ifs.good()) { + ifs >> value; + max = value; // expecting RPM set-point, not pwm% + } + } + catch (const std::exception& e) + { + /* This exception is uninteresting. */ + std::cerr << "Unable to read from '" << setpointpath << "'\n"; + } +#endif + + _maximumRPMSetPt = max; + return; +} + +#ifdef __TUNING_LOGGING__ +void PIDZone::initializeLog(void) +{ + /* Print header for log file. */ + + _log << "epoch_ms,setpt"; + + for (auto& f : _fanInputs) + { + _log << "," << f; + } + _log << std::endl; + + return; +} + +std::ofstream& PIDZone::getLogHandle(void) +{ + return _log; +} +#endif + +/* + * TODO(venture) This is effectively updating the cache and should check if the + * values they're using to update it are new or old, or whatnot. For instance, + * if we haven't heard from the host in X time we need to detect this failure. + * + * I haven't decided if the Sensor should have a lastUpdated method or whether + * that should be for the ReadInterface or etc... + */ + +/** + * We want the PID loop to run with values cached, so this will get all the + * fan tachs for the loop. + */ +void PIDZone::updateFanTelemetry(void) +{ + /* TODO(venture): Should I just make _log point to /dev/null when logging + * is disabled? I think it's a waste to try and log things even if the + * data is just being dropped though. + */ +#ifdef __TUNING_LOGGING__ + tstamp now = std::chrono::high_resolution_clock::now(); + _log << std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count(); + _log << "," << _maximumRPMSetPt; +#endif + + for (auto& f : _fanInputs) + { + auto& sensor = _mgr->getSensor(f); + ReadReturn r = sensor->read(); + _cachedValuesByName[f] = r.value; + + /* + * TODO(venture): We should check when these were last read. + * However, these are the fans, so if I'm not getting updated values + * for them... what should I do? + */ +#ifdef __TUNING_LOGGING__ + _log << "," << r.value; +#endif + } + + return; +} + +void PIDZone::updateSensors(void) +{ + using namespace std::chrono; + /* margin and temp are stored as temp */ + tstamp now = high_resolution_clock::now(); + + for (auto& t : _thermalInputs) + { + auto& sensor = _mgr->getSensor(t); + ReadReturn r = sensor->read(); + int64_t timeout = sensor->GetTimeout(); + + _cachedValuesByName[t] = r.value; + tstamp then = r.updated; + + /* Only go into failsafe if the timeout is set for + * the sensor. + */ + if (timeout > 0) + { + auto duration = duration_cast<std::chrono::seconds> + (now - then).count(); + auto period = std::chrono::seconds(timeout).count(); + if (duration >= period) + { + //std::cerr << "Entering fail safe mode.\n"; + _failSafeSensors.insert(t); + } + else + { + // Check if it's in there: remove it. + auto kt = _failSafeSensors.find(t); + if (kt != _failSafeSensors.end()) + { + _failSafeSensors.erase(kt); + } + } + } + } + + return; +} + +void PIDZone::initializeCache(void) +{ + for (auto& f : _fanInputs) + { + _cachedValuesByName[f] = 0; + } + + for (auto& t : _thermalInputs) + { + _cachedValuesByName[t] = 0; + + // Start all sensors in fail-safe mode. + _failSafeSensors.insert(t); + } +} + +void PIDZone::dumpCache(void) +{ + std::cerr << "Cache values now: \n"; + for (auto& k : _cachedValuesByName) + { + std::cerr << k.first << ": " << k.second << "\n"; + } +} + +void PIDZone::process_fans(void) +{ + for (auto& p : _fans) + { + p->pid_process(); + } +} + +void PIDZone::process_thermals(void) +{ + for (auto& p : _thermals) + { + p->pid_process(); + } +} + +std::unique_ptr<Sensor>& PIDZone::getSensor(std::string name) +{ + return _mgr->getSensor(name); +} + +bool PIDZone::manual(bool value) +{ + std::cerr << "manual: " << value << std::endl; + setManualMode(value); + return ModeObject::manual(value); +} + +bool PIDZone::failSafe() const +{ + return getFailSafeMode(); +} + +static std::string GetControlPath(int64_t zone) +{ + return std::string(objectPath) + std::to_string(zone); +} + +std::map<int64_t, std::shared_ptr<PIDZone>> BuildZones( + std::map<int64_t, PIDConf>& ZonePids, + std::map<int64_t, struct zone>& ZoneConfigs, + std::shared_ptr<SensorManager> mgr, + sdbusplus::bus::bus& ModeControlBus) +{ + std::map<int64_t, std::shared_ptr<PIDZone>> zones; + + for (auto& zi : ZonePids) + { + auto zoneId = static_cast<int64_t>(zi.first); + /* The above shouldn't be necessary but is, and I am having trouble + * locating my notes on why. If I recall correctly it was casting it + * down to a byte in at least some cases causing weird behaviors. + */ + + auto zoneConf = ZoneConfigs.find(zoneId); + if (zoneConf == ZoneConfigs.end()) + { + /* The Zone doesn't have a configuration, bail. */ + static constexpr auto err = + "Bailing during load, missing Zone Configuration"; + std::cerr << err << std::endl; + throw std::runtime_error(err); + } + + PIDConf& PIDConfig = zi.second; + + auto zone = std::make_shared<PIDZone>( + zoneId, + zoneConf->second.minthermalrpm, + zoneConf->second.failsafepercent, + mgr, + ModeControlBus, + GetControlPath(zi.first).c_str(), + deferSignals); + + zones[zoneId] = zone; + + std::cerr << "Zone Id: " << zone->getZoneId() << "\n"; + + // For each PID create a Controller and a Sensor. + PIDConf::iterator pit = PIDConfig.begin(); + while (pit != PIDConfig.end()) + { + std::vector<std::string> inputs; + std::string name = pit->first; + struct controller_info* info = &pit->second; + + std::cerr << "PID name: " << name << "\n"; + + /* + * TODO(venture): Need to check if input is known to the + * SensorManager. + */ + if (info->type == "fan") + { + for (auto i : info->inputs) + { + inputs.push_back(i); + zone->addFanInput(i); + } + + auto pid = FanController::CreateFanPid( + zone, + name, + inputs, + info->info); + zone->addFanPID(std::move(pid)); + } + else if (info->type == "temp" || info->type == "margin") + { + for (auto i : info->inputs) + { + inputs.push_back(i); + zone->addThermalInput(i); + } + + auto pid = ThermalController::CreateThermalPid( + zone, + name, + inputs, + info->setpoint, + info->info); + zone->addThermalPID(std::move(pid)); + } + + std::cerr << "inputs: "; + for (auto& i : inputs) + { + std::cerr << i << ", "; + } + std::cerr << "\n"; + + ++pit; + } + + zone->emit_object_added(); + } + + return zones; +} + +std::map<int64_t, std::shared_ptr<PIDZone>> BuildZonesFromConfig( + std::string& path, + std::shared_ptr<SensorManager> mgr, + sdbusplus::bus::bus& ModeControlBus) +{ + using namespace libconfig; + // zone -> pids + std::map<int64_t, PIDConf> pidConfig; + // zone -> configs + std::map<int64_t, struct zone> zoneConfig; + + std::cerr << "entered BuildZonesFromConfig\n"; + + Config cfg; + + /* The load was modeled after the example source provided. */ + try + { + cfg.readFile(path.c_str()); + } + catch (const FileIOException& fioex) + { + std::cerr << "I/O error while reading file: " << fioex.what() << std::endl; + throw; + } + catch (const ParseException& pex) + { + std::cerr << "Parse error at " << pex.getFile() << ":" << pex.getLine() + << " - " << pex.getError() << std::endl; + throw; + } + + try + { + const Setting& root = cfg.getRoot(); + const Setting& zones = root["zones"]; + int count = zones.getLength(); + + /* For each zone. */ + for (int i = 0; i < count; ++i) + { + const Setting& zoneSettings = zones[i]; + + int id; + PIDConf thisZone; + struct zone thisZoneConfig; + + zoneSettings.lookupValue("id", id); + + thisZoneConfig.minthermalrpm = + zoneSettings.lookup("minthermalrpm"); + thisZoneConfig.failsafepercent = + zoneSettings.lookup("failsafepercent"); + + const Setting& pids = zoneSettings["pids"]; + int pidCount = pids.getLength(); + + for (int j = 0; j < pidCount; ++j) + { + const Setting& pid = pids[j]; + + std::string name; + controller_info info; + + /* + * Mysteriously if you use lookupValue on these, and the type + * is float. It won't work right. + * + * If the configuration file value doesn't look explicitly like + * a float it won't let you assign it to one. + */ + name = pid.lookup("name").c_str(); + info.type = pid.lookup("type").c_str(); + /* set-point is only required to be set for thermal. */ + /* TODO(venture): Verify this works optionally here. */ + info.setpoint = pid.lookup("set-point"); + info.info.ts = pid.lookup("pid.sampleperiod"); + info.info.p_c = pid.lookup("pid.p_coefficient"); + info.info.i_c = pid.lookup("pid.i_coefficient"); + info.info.ff_off = pid.lookup("pid.ff_off_coefficient"); + info.info.ff_gain = pid.lookup("pid.ff_gain_coefficient"); + info.info.i_lim.min = pid.lookup("pid.i_limit.min"); + info.info.i_lim.max = pid.lookup("pid.i_limit.max"); + info.info.out_lim.min = pid.lookup("pid.out_limit.min"); + info.info.out_lim.max = pid.lookup("pid.out_limit.max"); + info.info.slew_neg = pid.lookup("pid.slew_neg"); + info.info.slew_pos = pid.lookup("pid.slew_pos"); + + std::cerr << "out_lim.min: " << info.info.out_lim.min << "\n"; + std::cerr << "out_lim.max: " << info.info.out_lim.max << "\n"; + + const Setting& inputs = pid["inputs"]; + int icount = inputs.getLength(); + + for (int z = 0; z < icount; ++z) + { + std::string v; + v = pid["inputs"][z].c_str(); + info.inputs.push_back(v); + } + + thisZone[name] = info; + } + + pidConfig[static_cast<int64_t>(id)] = thisZone; + zoneConfig[static_cast<int64_t>(id)] = thisZoneConfig; + } + } + catch (const SettingTypeException &setex) + { + std::cerr << "Setting '" << setex.getPath() << "' type exception!" << std::endl; + throw; + } + catch (const SettingNotFoundException& snex) + { + std::cerr << "Setting '" << snex.getPath() << "' not found!" << std::endl; + throw; + } + + return BuildZones(pidConfig, zoneConfig, mgr, ModeControlBus); +} diff --git a/pid/zone.hpp b/pid/zone.hpp new file mode 100644 index 0000000..d75bf59 --- /dev/null +++ b/pid/zone.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include <fstream> +#include <map> +#include <memory> +#include <set> +#include <string> +#include <vector> + +#include "conf.hpp" +#include "controller.hpp" +#include "sensors/sensor.hpp" +#include "sensors/manager.hpp" + +#include "xyz/openbmc_project/Control/FanCtrl/Mode/server.hpp" +#include <sdbusplus/bus.hpp> +#include <sdbusplus/server.hpp> + + +template <typename... T> +using ServerObject = typename sdbusplus::server::object::object<T...>; +using ModeInterface = + sdbusplus::xyz::openbmc_project::Control::FanCtrl::server::Mode; +using ModeObject = ServerObject<ModeInterface>; + +/* + * The PIDZone inherits from the Mode object so that it can listen for control + * mode changes. It primarily holds all PID loops and holds the sensor value + * cache that's used per iteration of the PID loops. + */ +class PIDZone : public ModeObject +{ + public: + PIDZone(int64_t zone, + float minThermalRpm, + float failSafePercent, + std::shared_ptr<SensorManager> mgr, + sdbusplus::bus::bus& bus, + const char* objPath, + bool defer) + : ModeObject(bus, objPath, defer), + _zoneId(zone), + _maximumRPMSetPt(), + _minThermalRpmSetPt(minThermalRpm), + _failSafePercent(failSafePercent), + _mgr(mgr) + { +#ifdef __TUNING_LOGGING__ + _log.open("/tmp/swampd.log"); +#endif + } + + float getMaxRPMRequest(void) const; + bool getManualMode(void) const; + + /* Could put lock around this since it's accessed from two threads, but + * only one reader/one writer. + */ + void setManualMode(bool mode); + bool getFailSafeMode(void) const; + int64_t getZoneId(void) const; + void addRPMSetPoint(float setpoint); + void clearRPMSetPoints(void); + float getFailSafePercent(void) const; + float getMinThermalRpmSetPt(void) const; + + std::unique_ptr<Sensor>& getSensor(std::string name); + void determineMaxRPMRequest(void); + void updateFanTelemetry(void); + void updateSensors(void); + void initializeCache(void); + void dumpCache(void); + void process_fans(void); + void process_thermals(void); + + void addFanPID(std::unique_ptr<PIDController> pid); + void addThermalPID(std::unique_ptr<PIDController> pid); + double getCachedValue(const std::string& name); + void addFanInput(std::string fan); + void addThermalInput(std::string therm); + +#ifdef __TUNING_LOGGING__ + void initializeLog(void); + std::ofstream& getLogHandle(void); +#endif + + /* Method for setting the manual mode over dbus */ + bool manual(bool value) override; + /* Method for reading whether in fail-safe mode over dbus */ + bool failSafe() const override; + + private: +#ifdef __TUNING_LOGGING__ + std::ofstream _log; +#endif + + const int64_t _zoneId; + float _maximumRPMSetPt = 0; + bool _manualMode = false; + const float _minThermalRpmSetPt; + const float _failSafePercent; + + std::set<std::string> _failSafeSensors; + + std::vector<float> _RPMSetPoints; + std::vector<std::string> _fanInputs; + std::vector<std::string> _thermalInputs; + std::map<std::string, double> _cachedValuesByName; + std::shared_ptr<SensorManager> _mgr; + + std::vector<std::unique_ptr<PIDController>> _fans; + std::vector<std::unique_ptr<PIDController>> _thermals; +}; + +std::map<int64_t, std::shared_ptr<PIDZone>> BuildZones( + std::map<int64_t, PIDConf>& ZonePids, + std::map<int64_t, struct zone>& ZoneConfigs, + std::shared_ptr<SensorManager> mgmr, + sdbusplus::bus::bus& ModeControlBus); + +std::map<int64_t, std::shared_ptr<PIDZone>> BuildZonesFromConfig( + std::string& path, + std::shared_ptr<SensorManager> mgmr, + sdbusplus::bus::bus& ModeControlBus); |

