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