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