blob: fee9d509de8773f8437db41e49b91c00da3924e6 [file] [log] [blame]
Willy Tude54f482021-01-26 15:59:09 -08001/*
2// Copyright (c) 2017 2018 Intel Corporation
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#include "dbus-sdr/sensorutils.hpp"
18
Haicheng Zhang041b3752025-07-14 17:13:45 +080019#include <phosphor-logging/lg2.hpp>
20
Willy Tude54f482021-01-26 15:59:09 -080021#include <algorithm>
22#include <cmath>
Willy Tude54f482021-01-26 15:59:09 -080023
24namespace ipmi
25{
26
27// Helper function to avoid repeated complicated expression
28static bool baseInRange(double base)
29{
30 auto min10 = static_cast<double>(minInt10);
31 auto max10 = static_cast<double>(maxInt10);
32
33 return ((base >= min10) && (base <= max10));
34}
35
36// Helper function for internal use by getSensorAttributes()
37// Ensures floating-point "base" is within bounds,
38// and adjusts integer exponent "expShift" accordingly.
39// To minimize data loss when later truncating to integer,
40// the floating-point "base" will be as large as possible,
41// but still within the bounds (minInt10,maxInt10).
42// The bounds of "expShift" are (minInt4,maxInt4).
43// Consider this equation: n = base * (10.0 ** expShift)
44// This function will try to maximize "base",
45// adjusting "expShift" to keep the value "n" unchanged,
46// while keeping base and expShift within bounds.
47// Returns true if successful, modifies values in-place
48static bool scaleFloatExp(double& base, int8_t& expShift)
49{
50 // Comparing with zero should be OK, zero is special in floating-point
51 // If base is exactly zero, no adjustment of the exponent is necessary
52 if (base == 0.0)
53 {
54 return true;
55 }
56
57 // As long as base value is within allowed range, expand precision
58 // This will help to avoid loss when later rounding to integer
59 while (baseInRange(base))
60 {
61 if (expShift <= minInt4)
62 {
63 // Already at the minimum expShift, can not decrement it more
64 break;
65 }
66
67 // Multiply by 10, but shift decimal point to the left, no net change
68 base *= 10.0;
69 --expShift;
70 }
71
72 // As long as base value is *not* within range, shrink precision
73 // This will pull base value closer to zero, thus within range
74 while (!(baseInRange(base)))
75 {
76 if (expShift >= maxInt4)
77 {
78 // Already at the maximum expShift, can not increment it more
79 break;
80 }
81
82 // Divide by 10, but shift decimal point to the right, no net change
83 base /= 10.0;
84 ++expShift;
85 }
86
87 // If the above loop was not able to pull it back within range,
88 // the base value is beyond what expShift can represent, return false.
89 return baseInRange(base);
90}
91
92// Helper function for internal use by getSensorAttributes()
93// Ensures integer "ibase" is no larger than necessary,
94// by normalizing it so that the decimal point shift is in the exponent,
95// whenever possible.
96// This provides more consistent results,
97// as many equivalent solutions are collapsed into one consistent solution.
98// If integer "ibase" is a clean multiple of 10,
99// divide it by 10 (this is lossless), so it is closer to zero.
100// Also modify floating-point "dbase" at the same time,
101// as both integer and floating-point base share the same expShift.
102// Example: (ibase=300, expShift=2) becomes (ibase=3, expShift=4)
103// because the underlying value is the same: 200*(10**2) == 2*(10**4)
104// Always successful, modifies values in-place
105static void normalizeIntExp(int16_t& ibase, int8_t& expShift, double& dbase)
106{
107 for (;;)
108 {
109 // If zero, already normalized, ensure exponent also zero
110 if (ibase == 0)
111 {
112 expShift = 0;
113 break;
114 }
115
116 // If not cleanly divisible by 10, already normalized
117 if ((ibase % 10) != 0)
118 {
119 break;
120 }
121
122 // If exponent already at max, already normalized
123 if (expShift >= maxInt4)
124 {
125 break;
126 }
127
128 // Bring values closer to zero, correspondingly shift exponent,
129 // without changing the underlying number that this all represents,
130 // similar to what is done by scaleFloatExp().
131 // The floating-point base must be kept in sync with the integer base,
132 // as both floating-point and integer share the same exponent.
133 ibase /= 10;
134 dbase /= 10.0;
135 ++expShift;
136 }
137}
138
139// The IPMI equation:
140// y = (Mx + (B * 10^(bExp))) * 10^(rExp)
141// Section 36.3 of this document:
142// https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf
143//
144// The goal is to exactly match the math done by the ipmitool command,
145// at the other side of the interface:
146// https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360
147//
148// To use with Wolfram Alpha, make all variables single letters
149// bExp becomes E, rExp becomes R
150// https://www.wolframalpha.com/input/?i=y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29
151bool getSensorAttributes(const double max, const double min, int16_t& mValue,
152 int8_t& rExp, int16_t& bValue, int8_t& bExp,
153 bool& bSigned)
154{
155 if (!(std::isfinite(min)))
156 {
Haicheng Zhang041b3752025-07-14 17:13:45 +0800157 lg2::error("getSensorAttributes: Min value is unusable");
Willy Tude54f482021-01-26 15:59:09 -0800158 return false;
159 }
160 if (!(std::isfinite(max)))
161 {
Haicheng Zhang041b3752025-07-14 17:13:45 +0800162 lg2::error("getSensorAttributes: Max value is unusable");
Willy Tude54f482021-01-26 15:59:09 -0800163 return false;
164 }
165
166 // Because NAN has already been tested for, this comparison works
167 if (max <= min)
168 {
Haicheng Zhang041b3752025-07-14 17:13:45 +0800169 lg2::error("getSensorAttributes: Max must be greater than min");
Willy Tude54f482021-01-26 15:59:09 -0800170 return false;
171 }
172
173 // Given min and max, we must solve for M, B, bExp, rExp
174 // y comes in from D-Bus (the actual sensor reading)
175 // x is calculated from y by scaleIPMIValueFromDouble() below
176 // If y is min, x should equal = 0 (or -128 if signed)
177 // If y is max, x should equal 255 (or 127 if signed)
178 double fullRange = max - min;
179 double lowestX;
180
181 rExp = 0;
182 bExp = 0;
183
184 // TODO(): The IPMI document is ambiguous, as to whether
185 // the resulting byte should be signed or unsigned,
186 // essentially leaving it up to the caller.
187 // The document just refers to it as "raw reading",
188 // or "byte of reading", without giving further details.
189 // Previous code set it signed if min was less than zero,
190 // so I'm sticking with that, until I learn otherwise.
191 if (min < 0.0)
192 {
193 // TODO(): It would be worth experimenting with the range (-127,127),
194 // instead of the range (-128,127), because this
195 // would give good symmetry around zero, and make results look better.
196 // Divide by 254 instead of 255, and change -128 to -127 elsewhere.
197 bSigned = true;
198 lowestX = -128.0;
199 }
200 else
201 {
202 bSigned = false;
203 lowestX = 0.0;
204 }
205
206 // Step 1: Set y to (max - min), set x to 255, set B to 0, solve for M
207 // This works, regardless of signed or unsigned,
208 // because total range is the same.
209 double dM = fullRange / 255.0;
210
211 // Step 2: Constrain M, and set rExp accordingly
212 if (!(scaleFloatExp(dM, rExp)))
213 {
Haicheng Zhang041b3752025-07-14 17:13:45 +0800214 lg2::error(
215 "getSensorAttributes: Multiplier range exceeds scale (M={DM}, "
216 "rExp={REXP})",
217 "DM", dM, "REXP", rExp);
Willy Tude54f482021-01-26 15:59:09 -0800218 return false;
219 }
220
221 mValue = static_cast<int16_t>(std::round(dM));
222
223 normalizeIntExp(mValue, rExp, dM);
224
225 // The multiplier can not be zero, for obvious reasons
226 if (mValue == 0)
227 {
Haicheng Zhang041b3752025-07-14 17:13:45 +0800228 lg2::error("getSensorAttributes: Multiplier range below scale");
Willy Tude54f482021-01-26 15:59:09 -0800229 return false;
230 }
231
232 // Step 3: set y to min, set x to min, keep M and rExp, solve for B
233 // If negative, x will be -128 (the most negative possible byte), not 0
234
235 // Solve the IPMI equation for B, instead of y
236 // https://www.wolframalpha.com/input/?i=solve+y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29+for+B
237 // B = 10^(-rExp - bExp) (y - M 10^rExp x)
238 // TODO(): Compare with this alternative solution from SageMathCell
239 // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasjoKTJgDAECTH&lang=sage&interacts=eJyLjgUAARUAuQ==
240 double dB = std::pow(10.0, ((-rExp) - bExp)) *
241 (min - ((dM * std::pow(10.0, rExp) * lowestX)));
242
243 // Step 4: Constrain B, and set bExp accordingly
244 if (!(scaleFloatExp(dB, bExp)))
245 {
Haicheng Zhang041b3752025-07-14 17:13:45 +0800246 lg2::error(
247 "getSensorAttributes: Offset range exceeds scale (B={DB}, "
248 "bExp={BEXP}) exceeds multiplier scale (M={DM}, rExp={REXP})",
249 "DB", dB, "BEXP", bExp, "DM", dM, "REXP", rExp);
Willy Tude54f482021-01-26 15:59:09 -0800250 return false;
251 }
252
253 bValue = static_cast<int16_t>(std::round(dB));
254
255 normalizeIntExp(bValue, bExp, dB);
256
257 // Unlike the multiplier, it is perfectly OK for bValue to be zero
258 return true;
259}
260
261uint8_t scaleIPMIValueFromDouble(const double value, const int16_t mValue,
262 const int8_t rExp, const int16_t bValue,
263 const int8_t bExp, const bool bSigned)
264{
265 // Avoid division by zero below
266 if (mValue == 0)
267 {
268 throw std::out_of_range("Scaling multiplier is uninitialized");
269 }
270
271 auto dM = static_cast<double>(mValue);
272 auto dB = static_cast<double>(bValue);
273
274 // Solve the IPMI equation for x, instead of y
275 // https://www.wolframalpha.com/input/?i=solve+y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29+for+x
276 // x = (10^(-rExp) (y - B 10^(rExp + bExp)))/M and M 10^rExp!=0
277 // TODO(): Compare with this alternative solution from SageMathCell
278 // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasDlAlAMB8JP0=&lang=sage&interacts=eJyLjgUAARUAuQ==
279 double dX =
280 (std::pow(10.0, -rExp) * (value - (dB * std::pow(10.0, rExp + bExp)))) /
281 dM;
282
283 auto scaledValue = static_cast<int32_t>(std::round(dX));
284
285 int32_t minClamp;
286 int32_t maxClamp;
287
288 // Because of rounding and integer truncation of scaling factors,
289 // sometimes the resulting byte is slightly out of range.
290 // Still allow this, but clamp the values to range.
291 if (bSigned)
292 {
293 minClamp = std::numeric_limits<int8_t>::lowest();
294 maxClamp = std::numeric_limits<int8_t>::max();
295 }
296 else
297 {
298 minClamp = std::numeric_limits<uint8_t>::lowest();
299 maxClamp = std::numeric_limits<uint8_t>::max();
300 }
301
302 auto clampedValue = std::clamp(scaledValue, minClamp, maxClamp);
303
304 // This works for both signed and unsigned,
305 // because it is the same underlying byte storage.
306 return static_cast<uint8_t>(clampedValue);
307}
308
309uint8_t getScaledIPMIValue(const double value, const double max,
310 const double min)
311{
312 int16_t mValue = 0;
313 int8_t rExp = 0;
314 int16_t bValue = 0;
315 int8_t bExp = 0;
316 bool bSigned = false;
317
Patrick Williams1318a5e2024-08-16 15:19:54 -0400318 bool result =
319 getSensorAttributes(max, min, mValue, rExp, bValue, bExp, bSigned);
Willy Tude54f482021-01-26 15:59:09 -0800320 if (!result)
321 {
322 throw std::runtime_error("Illegal sensor attributes");
323 }
324
325 return scaleIPMIValueFromDouble(value, mValue, rExp, bValue, bExp, bSigned);
326}
327
328} // namespace ipmi