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