xref: /openbmc/phosphor-pid-control/pid/zone.cpp (revision f8b6e55147148c3cfb42327ff267197a460b411c)
1 /**
2  * Copyright 2017 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 /* Configuration. */
18 #include "zone.hpp"
19 
20 #include "conf.hpp"
21 #include "failsafeloggers/failsafe_logger_utility.hpp"
22 #include "interfaces.hpp"
23 #include "pid/controller.hpp"
24 #include "pid/tuning.hpp"
25 
26 #include <sdbusplus/bus.hpp>
27 
28 #include <algorithm>
29 #include <chrono>
30 #include <cstdint>
31 #include <cstring>
32 #include <exception>
33 #include <fstream>
34 #include <iostream>
35 #include <limits>
36 #include <memory>
37 #include <sstream>
38 #include <string>
39 #include <string_view>
40 #include <utility>
41 #include <vector>
42 
43 using tstamp = std::chrono::high_resolution_clock::time_point;
44 using namespace std::literals::chrono_literals;
45 
46 // Enforces minimum duration between events
47 // Rreturns true if event should be allowed, false if disallowed
allowThrottle(const tstamp & now,const std::chrono::seconds & pace)48 bool allowThrottle(const tstamp& now, const std::chrono::seconds& pace)
49 {
50     static tstamp then;
51     static bool first = true;
52 
53     if (first)
54     {
55         // Special case initialization
56         then = now;
57         first = false;
58 
59         // Initialization, always allow
60         return true;
61     }
62 
63     auto elapsed = now - then;
64     if (elapsed < pace)
65     {
66         // Too soon since last time, disallow
67         return false;
68     }
69 
70     // It has been long enough, allow
71     then = now;
72     return true;
73 }
74 
75 namespace pid_control
76 {
77 
getMaxSetPointRequest(void) const78 double DbusPidZone::getMaxSetPointRequest(void) const
79 {
80     return _maximumSetPoint;
81 }
82 
getManualMode(void) const83 bool DbusPidZone::getManualMode(void) const
84 {
85     return _manualMode;
86 }
87 
setManualMode(bool mode)88 void DbusPidZone::setManualMode(bool mode)
89 {
90     _manualMode = mode;
91 
92     // If returning to automatic mode, need to restore PWM from PID loop
93     if (!mode)
94     {
95         _redundantWrite = true;
96     }
97 }
98 
getFailSafeMode(void) const99 bool DbusPidZone::getFailSafeMode(void) const
100 {
101     // If any keys are present at least one sensor is in fail safe mode.
102     return !_failSafeSensors.empty();
103 }
104 
getFailSafeSensors(void) const105 FailSafeSensorsMap DbusPidZone::getFailSafeSensors(void) const
106 {
107     return _failSafeSensors;
108 }
109 
markSensorMissing(const std::string & name,const std::string & failReason)110 void DbusPidZone::markSensorMissing(const std::string& name,
111                                     const std::string& failReason)
112 {
113     if (_missingAcceptable.find(name) != _missingAcceptable.end())
114     {
115         // Disallow sensors in MissingIsAcceptable list from causing failsafe
116         outputFailsafeLogWithZone(_zoneId, this->getFailSafeMode(), name,
117                                   "The sensor is missing but is acceptable.");
118         return;
119     }
120 
121     if (_sensorFailSafePercent[name] == 0)
122     {
123         _failSafeSensors[name] = std::pair(failReason, _zoneFailSafePercent);
124     }
125     else
126     {
127         _failSafeSensors[name] =
128             std::pair(failReason, _sensorFailSafePercent[name]);
129     }
130 
131     if (debugEnabled)
132     {
133         std::cerr << "Sensor " << name << " marked missing\n";
134     }
135 }
136 
getZoneID(void) const137 int64_t DbusPidZone::getZoneID(void) const
138 {
139     return _zoneId;
140 }
141 
addSetPoint(double setPoint,const std::string & name)142 void DbusPidZone::addSetPoint(double setPoint, const std::string& name)
143 {
144     /* exclude disabled pidloop from _maximumSetPoint calculation*/
145     if (!isPidProcessEnabled(name))
146     {
147         return;
148     }
149 
150     auto profileName = name;
151     if (getAccSetPoint())
152     {
153         /*
154          * If the name of controller is Linear_Temp_CPU0.
155          * The profile name will be Temp_CPU0.
156          */
157         profileName = name.substr(name.find('_') + 1);
158         setPoints[profileName] += setPoint;
159     }
160     else
161     {
162         if (setPoints[profileName] < setPoint)
163         {
164             setPoints[profileName] = setPoint;
165         }
166     }
167 
168     /*
169      * if there are multiple thermal controllers with the same
170      * value, pick the first one in the iterator
171      */
172     if (_maximumSetPoint < setPoints[profileName])
173     {
174         _maximumSetPoint = setPoints[profileName];
175         _maximumSetPointName = profileName;
176     }
177 }
178 
addRPMCeiling(double ceiling)179 void DbusPidZone::addRPMCeiling(double ceiling)
180 {
181     rpmCeilings.push_back(ceiling);
182 }
183 
clearRPMCeilings(void)184 void DbusPidZone::clearRPMCeilings(void)
185 {
186     rpmCeilings.clear();
187 }
188 
clearSetPoints(void)189 void DbusPidZone::clearSetPoints(void)
190 {
191     setPoints.clear();
192     _maximumSetPoint = 0;
193     _maximumSetPointName.clear();
194 }
195 
getFailSafePercent(void)196 double DbusPidZone::getFailSafePercent(void)
197 {
198     if (_failSafeSensors.empty())
199     {
200         return _zoneFailSafePercent;
201     }
202 
203     FailSafeSensorsMap::iterator maxData = std::max_element(
204         _failSafeSensors.begin(), _failSafeSensors.end(),
205         [](const FailSafeSensorPair& firstData,
206            const FailSafeSensorPair& secondData) {
207             return firstData.second.second < secondData.second.second;
208         });
209 
210     // In dbus/dbusconfiguration.cpp, the default sensor failsafepercent is 0 if
211     // there is no setting in json.
212     // Therfore, if the max failsafe duty in _failSafeSensors is 0, set final
213     // failsafe duty to _zoneFailSafePercent.
214     if ((*maxData).second.second == 0)
215     {
216         return _zoneFailSafePercent;
217     }
218 
219     return (*maxData).second.second;
220 }
221 
getMinThermalSetPoint(void) const222 double DbusPidZone::getMinThermalSetPoint(void) const
223 {
224     return _minThermalOutputSetPt;
225 }
226 
getCycleIntervalTime(void) const227 uint64_t DbusPidZone::getCycleIntervalTime(void) const
228 {
229     return _cycleTime.cycleIntervalTimeMS;
230 }
231 
getUpdateThermalsCycle(void) const232 uint64_t DbusPidZone::getUpdateThermalsCycle(void) const
233 {
234     return _cycleTime.updateThermalsTimeMS;
235 }
236 
addFanPID(std::unique_ptr<Controller> pid)237 void DbusPidZone::addFanPID(std::unique_ptr<Controller> pid)
238 {
239     _fans.push_back(std::move(pid));
240 }
241 
addThermalPID(std::unique_ptr<Controller> pid)242 void DbusPidZone::addThermalPID(std::unique_ptr<Controller> pid)
243 {
244     _thermals.push_back(std::move(pid));
245 }
246 
getCachedValue(const std::string & name)247 double DbusPidZone::getCachedValue(const std::string& name)
248 {
249     return _cachedValuesByName.at(name).scaled;
250 }
251 
getCachedValues(const std::string & name)252 ValueCacheEntry DbusPidZone::getCachedValues(const std::string& name)
253 {
254     return _cachedValuesByName.at(name);
255 }
256 
setOutputCache(std::string_view name,const ValueCacheEntry & values)257 void DbusPidZone::setOutputCache(std::string_view name,
258                                  const ValueCacheEntry& values)
259 {
260     _cachedFanOutputs[std::string{name}] = values;
261 }
262 
addFanInput(const std::string & fan,bool missingAcceptable)263 void DbusPidZone::addFanInput(const std::string& fan, bool missingAcceptable)
264 {
265     _fanInputs.push_back(fan);
266 
267     if (missingAcceptable)
268     {
269         _missingAcceptable.emplace(fan);
270     }
271 }
272 
addThermalInput(const std::string & therm,bool missingAcceptable)273 void DbusPidZone::addThermalInput(const std::string& therm,
274                                   bool missingAcceptable)
275 {
276     /*
277      * One sensor may have stepwise and PID at the same time.
278      * Searching the sensor name before inserting it to avoid duplicated sensor
279      * names.
280      */
281     if (std::find(_thermalInputs.begin(), _thermalInputs.end(), therm) ==
282         _thermalInputs.end())
283     {
284         _thermalInputs.push_back(therm);
285     }
286 
287     if (missingAcceptable)
288     {
289         _missingAcceptable.emplace(therm);
290     }
291 }
292 
293 // Updates desired RPM setpoint from optional text file
294 // Returns true if rpmValue updated, false if left unchanged
fileParseRpm(const std::string & fileName,double & rpmValue)295 static bool fileParseRpm(const std::string& fileName, double& rpmValue)
296 {
297     static constexpr std::chrono::seconds throttlePace{3};
298 
299     std::string errText;
300 
301     try
302     {
303         std::ifstream ifs;
304         ifs.open(fileName);
305         if (ifs)
306         {
307             int value;
308             ifs >> value;
309 
310             if (value <= 0)
311             {
312                 errText = "File content could not be parsed to a number";
313             }
314             else if (value <= 100)
315             {
316                 errText = "File must contain RPM value, not PWM value";
317             }
318             else
319             {
320                 rpmValue = static_cast<double>(value);
321                 return true;
322             }
323         }
324     }
325     catch (const std::exception& e)
326     {
327         errText = "Exception: ";
328         errText += e.what();
329     }
330 
331     // The file is optional, intentionally not an error if file not found
332     if (!(errText.empty()))
333     {
334         tstamp now = std::chrono::high_resolution_clock::now();
335         if (allowThrottle(now, throttlePace))
336         {
337             std::cerr << "Unable to read from '" << fileName << "': " << errText
338                       << "\n";
339         }
340     }
341 
342     return false;
343 }
344 
determineMaxSetPointRequest(void)345 void DbusPidZone::determineMaxSetPointRequest(void)
346 {
347     std::vector<double>::iterator result;
348     double minThermalThreshold = getMinThermalSetPoint();
349 
350     if (rpmCeilings.size() > 0)
351     {
352         result = std::min_element(rpmCeilings.begin(), rpmCeilings.end());
353         // if Max set point is larger than the lowest ceiling, reset to lowest
354         // ceiling.
355         if (*result < _maximumSetPoint)
356         {
357             _maximumSetPoint = *result;
358             // When using lowest ceiling, controller name is ceiling.
359             _maximumSetPointName = "Ceiling";
360         }
361     }
362 
363     /*
364      * Combine the maximum SetPoint Name if the controllers have same profile
365      * name. e.g., PID_BB_INLET_TEMP_C + Stepwise_BB_INLET_TEMP_C.
366      */
367     if (getAccSetPoint())
368     {
369         auto profileName = _maximumSetPointName;
370         _maximumSetPointName = "";
371 
372         for (auto& p : _thermals)
373         {
374             auto controllerID = p->getID();
375             auto found = controllerID.find(profileName);
376             if (found != std::string::npos)
377             {
378                 if (_maximumSetPointName.empty())
379                 {
380                     _maximumSetPointName = controllerID;
381                 }
382                 else
383                 {
384                     _maximumSetPointName += " + " + controllerID;
385                 }
386             }
387         }
388     }
389 
390     /*
391      * If the maximum RPM setpoint output is below the minimum RPM
392      * setpoint, set it to the minimum.
393      */
394     if (minThermalThreshold >= _maximumSetPoint)
395     {
396         _maximumSetPoint = minThermalThreshold;
397         _maximumSetPointName = "Minimum";
398     }
399     else if (_maximumSetPointName.compare(_maximumSetPointNamePrev))
400     {
401         std::cerr << "PID Zone " << _zoneId << " max SetPoint "
402                   << _maximumSetPoint << " requested by "
403                   << _maximumSetPointName;
404         for (const auto& sensor : _failSafeSensors)
405         {
406             if (sensor.first.find("Fan") == std::string::npos)
407             {
408                 std::cerr << " " << sensor.first;
409             }
410         }
411         std::cerr << "\n";
412         _maximumSetPointNamePrev.assign(_maximumSetPointName);
413     }
414     if (tuningEnabled)
415     {
416         /*
417          * We received no setpoints from thermal sensors.
418          * This is a case experienced during tuning where they only specify
419          * fan sensors and one large fan PID for all the fans.
420          */
421         static constexpr auto setpointpath = "/etc/thermal.d/setpoint";
422 
423         fileParseRpm(setpointpath, _maximumSetPoint);
424 
425         // Allow per-zone setpoint files to override overall setpoint file
426         std::ostringstream zoneSuffix;
427         zoneSuffix << ".zone" << _zoneId;
428         std::string zoneSetpointPath = setpointpath + zoneSuffix.str();
429 
430         fileParseRpm(zoneSetpointPath, _maximumSetPoint);
431     }
432     return;
433 }
434 
initializeLog(void)435 void DbusPidZone::initializeLog(void)
436 {
437     /* Print header for log file:
438      * epoch_ms,setpt,fan1,fan1_raw,fan1_pwm,fan1_pwm_raw,fan2,fan2_raw,fan2_pwm,fan2_pwm_raw,fanN,fanN_raw,fanN_pwm,fanN_pwm_raw,sensor1,sensor1_raw,sensor2,sensor2_raw,sensorN,sensorN_raw,failsafe
439      */
440 
441     _log << "epoch_ms,setpt,requester";
442 
443     for (const auto& f : _fanInputs)
444     {
445         _log << "," << f << "," << f << "_raw";
446         _log << "," << f << "_pwm," << f << "_pwm_raw";
447     }
448     for (const auto& t : _thermalInputs)
449     {
450         _log << "," << t << "," << t << "_raw";
451     }
452 
453     _log << ",failsafe";
454     _log << std::endl;
455 }
456 
writeLog(const std::string & value)457 void DbusPidZone::writeLog(const std::string& value)
458 {
459     _log << value;
460 }
461 
462 /*
463  * TODO(venture) This is effectively updating the cache and should check if the
464  * values they're using to update it are new or old, or whatnot.  For instance,
465  * if we haven't heard from the host in X time we need to detect this failure.
466  *
467  * I haven't decided if the Sensor should have a lastUpdated method or whether
468  * that should be for the ReadInterface or etc...
469  */
470 
471 /**
472  * We want the PID loop to run with values cached, so this will get all the
473  * fan tachs for the loop.
474  */
updateFanTelemetry(void)475 void DbusPidZone::updateFanTelemetry(void)
476 {
477     /* TODO(venture): Should I just make _log point to /dev/null when logging
478      * is disabled?  I think it's a waste to try and log things even if the
479      * data is just being dropped though.
480      */
481     const auto now = std::chrono::high_resolution_clock::now();
482     if (loggingEnabled)
483     {
484         _log << std::chrono::duration_cast<std::chrono::milliseconds>(
485                     now.time_since_epoch())
486                     .count();
487         _log << "," << _maximumSetPoint;
488         _log << "," << _maximumSetPointName;
489     }
490 
491     processSensorInputs</* fanSensorLogging */ true>(_fanInputs, now);
492 
493     if (loggingEnabled)
494     {
495         for (const auto& t : _thermalInputs)
496         {
497             const auto& v = _cachedValuesByName[t];
498             _log << "," << v.scaled << "," << v.unscaled;
499         }
500     }
501 
502     return;
503 }
504 
updateSensors(void)505 void DbusPidZone::updateSensors(void)
506 {
507     processSensorInputs</* fanSensorLogging */ false>(
508         _thermalInputs, std::chrono::high_resolution_clock::now());
509 
510     return;
511 }
512 
initializeCache(void)513 void DbusPidZone::initializeCache(void)
514 {
515     auto nan = std::numeric_limits<double>::quiet_NaN();
516 
517     for (const auto& f : _fanInputs)
518     {
519         _cachedValuesByName[f] = {nan, nan};
520         _cachedFanOutputs[f] = {nan, nan};
521 
522         // Start all fans in fail-safe mode.
523         markSensorMissing(f, "");
524     }
525 
526     for (const auto& t : _thermalInputs)
527     {
528         _cachedValuesByName[t] = {nan, nan};
529 
530         // Start all sensors in fail-safe mode.
531         markSensorMissing(t, "");
532     }
533 }
534 
dumpCache(void)535 void DbusPidZone::dumpCache(void)
536 {
537     std::cerr << "Cache values now: \n";
538     for (const auto& [name, value] : _cachedValuesByName)
539     {
540         std::cerr << name << ": " << value.scaled << " " << value.unscaled
541                   << "\n";
542     }
543 
544     std::cerr << "Fan outputs now: \n";
545     for (const auto& [name, value] : _cachedFanOutputs)
546     {
547         std::cerr << name << ": " << value.scaled << " " << value.unscaled
548                   << "\n";
549     }
550 }
551 
processFans(void)552 void DbusPidZone::processFans(void)
553 {
554     for (auto& p : _fans)
555     {
556         p->process();
557     }
558 
559     if (_redundantWrite)
560     {
561         // This is only needed once
562         _redundantWrite = false;
563     }
564 }
565 
processThermals(void)566 void DbusPidZone::processThermals(void)
567 {
568     for (auto& p : _thermals)
569     {
570         p->process();
571     }
572 }
573 
getSensor(const std::string & name)574 Sensor* DbusPidZone::getSensor(const std::string& name)
575 {
576     return _mgr.getSensor(name);
577 }
578 
getSensorNames(void)579 std::vector<std::string> DbusPidZone::getSensorNames(void)
580 {
581     return _thermalInputs;
582 }
583 
getRedundantWrite(void) const584 bool DbusPidZone::getRedundantWrite(void) const
585 {
586     return _redundantWrite;
587 }
588 
manual(bool value)589 bool DbusPidZone::manual(bool value)
590 {
591     std::cerr << "manual: " << value << std::endl;
592     setManualMode(value);
593     return ModeObject::manual(value);
594 }
595 
failSafe() const596 bool DbusPidZone::failSafe() const
597 {
598     return getFailSafeMode();
599 }
600 
addPidControlProcess(const std::string & name,const std::string & type,double setpoint,sdbusplus::bus_t & bus,const std::string & objPath,bool defer)601 void DbusPidZone::addPidControlProcess(
602     const std::string& name, const std::string& type, double setpoint,
603     sdbusplus::bus_t& bus, const std::string& objPath, bool defer)
604 {
605     _pidsControlProcess[name] = std::make_unique<ProcessObject>(
606         bus, objPath.c_str(),
607         defer ? ProcessObject::action::defer_emit
608               : ProcessObject::action::emit_object_added);
609     // Default enable setting = true
610     _pidsControlProcess[name]->enabled(true);
611     _pidsControlProcess[name]->setpoint(setpoint);
612 
613     if (type == "temp")
614     {
615         _pidsControlProcess[name]->classType("Temperature");
616     }
617     else if (type == "margin")
618     {
619         _pidsControlProcess[name]->classType("Margin");
620     }
621     else if (type == "power")
622     {
623         _pidsControlProcess[name]->classType("Power");
624     }
625     else if (type == "powersum")
626     {
627         _pidsControlProcess[name]->classType("PowerSum");
628     }
629 }
630 
isPidProcessEnabled(const std::string & name)631 bool DbusPidZone::isPidProcessEnabled(const std::string& name)
632 {
633     return _pidsControlProcess[name]->enabled();
634 }
635 
addPidFailSafePercent(const std::vector<std::string> & inputs,double percent)636 void DbusPidZone::addPidFailSafePercent(const std::vector<std::string>& inputs,
637                                         double percent)
638 {
639     for (const auto& sensorName : inputs)
640     {
641         if (_sensorFailSafePercent.find(sensorName) !=
642             _sensorFailSafePercent.end())
643         {
644             _sensorFailSafePercent[sensorName] =
645                 std::max(_sensorFailSafePercent[sensorName], percent);
646             if (debugEnabled)
647             {
648                 std::cerr << "Sensor " << sensorName
649                           << " failsafe percent updated to "
650                           << _sensorFailSafePercent[sensorName] << "\n";
651             }
652         }
653         else
654         {
655             _sensorFailSafePercent[sensorName] = percent;
656             if (debugEnabled)
657             {
658                 std::cerr << "Sensor " << sensorName
659                           << " failsafe percent set to " << percent << "\n";
660             }
661         }
662     }
663 }
664 
leader() const665 std::string DbusPidZone::leader() const
666 {
667     return _maximumSetPointName;
668 }
669 
updateThermalPowerDebugInterface(std::string pidName,std::string leader,double input,double output)670 void DbusPidZone::updateThermalPowerDebugInterface(
671     std::string pidName, std::string leader, double input, double output)
672 {
673     if (leader.empty())
674     {
675         _pidsControlProcess[pidName]->output(output);
676     }
677     else
678     {
679         _pidsControlProcess[pidName]->leader(leader);
680         _pidsControlProcess[pidName]->input(input);
681     }
682 }
683 
getAccSetPoint(void) const684 bool DbusPidZone::getAccSetPoint(void) const
685 {
686     return _accumulateSetPoint;
687 }
688 
689 } // namespace pid_control
690