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