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