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>
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);
+}