1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * power_supply_hwmon.c - power supply hwmon support. |
4 | */ |
5 | |
6 | #include <linux/err.h> |
7 | #include <linux/hwmon.h> |
8 | #include <linux/power_supply.h> |
9 | #include <linux/slab.h> |
10 | |
11 | struct power_supply_hwmon { |
12 | struct power_supply *psy; |
13 | unsigned long *props; |
14 | }; |
15 | |
16 | static const char *const ps_temp_label[] = { |
17 | "temp" , |
18 | "ambient temp" , |
19 | }; |
20 | |
21 | static int power_supply_hwmon_in_to_property(u32 attr) |
22 | { |
23 | switch (attr) { |
24 | case hwmon_in_average: |
25 | return POWER_SUPPLY_PROP_VOLTAGE_AVG; |
26 | case hwmon_in_min: |
27 | return POWER_SUPPLY_PROP_VOLTAGE_MIN; |
28 | case hwmon_in_max: |
29 | return POWER_SUPPLY_PROP_VOLTAGE_MAX; |
30 | case hwmon_in_input: |
31 | return POWER_SUPPLY_PROP_VOLTAGE_NOW; |
32 | default: |
33 | return -EINVAL; |
34 | } |
35 | } |
36 | |
37 | static int power_supply_hwmon_curr_to_property(u32 attr) |
38 | { |
39 | switch (attr) { |
40 | case hwmon_curr_average: |
41 | return POWER_SUPPLY_PROP_CURRENT_AVG; |
42 | case hwmon_curr_max: |
43 | return POWER_SUPPLY_PROP_CURRENT_MAX; |
44 | case hwmon_curr_input: |
45 | return POWER_SUPPLY_PROP_CURRENT_NOW; |
46 | default: |
47 | return -EINVAL; |
48 | } |
49 | } |
50 | |
51 | static int power_supply_hwmon_temp_to_property(u32 attr, int channel) |
52 | { |
53 | if (channel) { |
54 | switch (attr) { |
55 | case hwmon_temp_input: |
56 | return POWER_SUPPLY_PROP_TEMP_AMBIENT; |
57 | case hwmon_temp_min_alarm: |
58 | return POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MIN; |
59 | case hwmon_temp_max_alarm: |
60 | return POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MAX; |
61 | default: |
62 | break; |
63 | } |
64 | } else { |
65 | switch (attr) { |
66 | case hwmon_temp_input: |
67 | return POWER_SUPPLY_PROP_TEMP; |
68 | case hwmon_temp_max: |
69 | return POWER_SUPPLY_PROP_TEMP_MAX; |
70 | case hwmon_temp_min: |
71 | return POWER_SUPPLY_PROP_TEMP_MIN; |
72 | case hwmon_temp_min_alarm: |
73 | return POWER_SUPPLY_PROP_TEMP_ALERT_MIN; |
74 | case hwmon_temp_max_alarm: |
75 | return POWER_SUPPLY_PROP_TEMP_ALERT_MAX; |
76 | default: |
77 | break; |
78 | } |
79 | } |
80 | |
81 | return -EINVAL; |
82 | } |
83 | |
84 | static int |
85 | power_supply_hwmon_to_property(enum hwmon_sensor_types type, |
86 | u32 attr, int channel) |
87 | { |
88 | switch (type) { |
89 | case hwmon_in: |
90 | return power_supply_hwmon_in_to_property(attr); |
91 | case hwmon_curr: |
92 | return power_supply_hwmon_curr_to_property(attr); |
93 | case hwmon_temp: |
94 | return power_supply_hwmon_temp_to_property(attr, channel); |
95 | default: |
96 | return -EINVAL; |
97 | } |
98 | } |
99 | |
100 | static bool power_supply_hwmon_is_a_label(enum hwmon_sensor_types type, |
101 | u32 attr) |
102 | { |
103 | return type == hwmon_temp && attr == hwmon_temp_label; |
104 | } |
105 | |
106 | struct hwmon_type_attr_list { |
107 | const u32 *attrs; |
108 | size_t n_attrs; |
109 | }; |
110 | |
111 | static const u32 ps_temp_attrs[] = { |
112 | hwmon_temp_input, |
113 | hwmon_temp_min, hwmon_temp_max, |
114 | hwmon_temp_min_alarm, hwmon_temp_max_alarm, |
115 | }; |
116 | |
117 | static const struct hwmon_type_attr_list ps_type_attrs[hwmon_max] = { |
118 | [hwmon_temp] = { ps_temp_attrs, ARRAY_SIZE(ps_temp_attrs) }, |
119 | }; |
120 | |
121 | static bool power_supply_hwmon_has_input( |
122 | const struct power_supply_hwmon *psyhw, |
123 | enum hwmon_sensor_types type, int channel) |
124 | { |
125 | const struct hwmon_type_attr_list *attr_list = &ps_type_attrs[type]; |
126 | size_t i; |
127 | |
128 | for (i = 0; i < attr_list->n_attrs; ++i) { |
129 | int prop = power_supply_hwmon_to_property(type, |
130 | attr: attr_list->attrs[i], channel); |
131 | |
132 | if (prop >= 0 && test_bit(prop, psyhw->props)) |
133 | return true; |
134 | } |
135 | |
136 | return false; |
137 | } |
138 | |
139 | static bool power_supply_hwmon_is_writable(enum hwmon_sensor_types type, |
140 | u32 attr) |
141 | { |
142 | switch (type) { |
143 | case hwmon_in: |
144 | return attr == hwmon_in_min || |
145 | attr == hwmon_in_max; |
146 | case hwmon_curr: |
147 | return attr == hwmon_curr_max; |
148 | case hwmon_temp: |
149 | return attr == hwmon_temp_max || |
150 | attr == hwmon_temp_min || |
151 | attr == hwmon_temp_min_alarm || |
152 | attr == hwmon_temp_max_alarm; |
153 | default: |
154 | return false; |
155 | } |
156 | } |
157 | |
158 | static umode_t power_supply_hwmon_is_visible(const void *data, |
159 | enum hwmon_sensor_types type, |
160 | u32 attr, int channel) |
161 | { |
162 | const struct power_supply_hwmon *psyhw = data; |
163 | int prop; |
164 | |
165 | if (power_supply_hwmon_is_a_label(type, attr)) { |
166 | if (power_supply_hwmon_has_input(psyhw, type, channel)) |
167 | return 0444; |
168 | else |
169 | return 0; |
170 | } |
171 | |
172 | prop = power_supply_hwmon_to_property(type, attr, channel); |
173 | if (prop < 0 || !test_bit(prop, psyhw->props)) |
174 | return 0; |
175 | |
176 | if (power_supply_property_is_writeable(psy: psyhw->psy, psp: prop) > 0 && |
177 | power_supply_hwmon_is_writable(type, attr)) |
178 | return 0644; |
179 | |
180 | return 0444; |
181 | } |
182 | |
183 | static int power_supply_hwmon_read_string(struct device *dev, |
184 | enum hwmon_sensor_types type, |
185 | u32 attr, int channel, |
186 | const char **str) |
187 | { |
188 | switch (type) { |
189 | case hwmon_temp: |
190 | *str = ps_temp_label[channel]; |
191 | break; |
192 | default: |
193 | /* unreachable, but see: |
194 | * gcc bug #51513 [1] and clang bug #978 [2] |
195 | * |
196 | * [1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51513 |
197 | * [2] https://github.com/ClangBuiltLinux/linux/issues/978 |
198 | */ |
199 | break; |
200 | } |
201 | |
202 | return 0; |
203 | } |
204 | |
205 | static int |
206 | power_supply_hwmon_read(struct device *dev, enum hwmon_sensor_types type, |
207 | u32 attr, int channel, long *val) |
208 | { |
209 | struct power_supply_hwmon *psyhw = dev_get_drvdata(dev); |
210 | struct power_supply *psy = psyhw->psy; |
211 | union power_supply_propval pspval; |
212 | int ret, prop; |
213 | |
214 | prop = power_supply_hwmon_to_property(type, attr, channel); |
215 | if (prop < 0) |
216 | return prop; |
217 | |
218 | ret = power_supply_get_property(psy, psp: prop, val: &pspval); |
219 | if (ret) |
220 | return ret; |
221 | |
222 | switch (type) { |
223 | /* |
224 | * Both voltage and current is reported in units of |
225 | * microvolts/microamps, so we need to adjust it to |
226 | * milliamps(volts) |
227 | */ |
228 | case hwmon_curr: |
229 | case hwmon_in: |
230 | pspval.intval = DIV_ROUND_CLOSEST(pspval.intval, 1000); |
231 | break; |
232 | /* |
233 | * Temp needs to be converted from 1/10 C to milli-C |
234 | */ |
235 | case hwmon_temp: |
236 | if (check_mul_overflow(pspval.intval, 100, |
237 | &pspval.intval)) |
238 | return -EOVERFLOW; |
239 | break; |
240 | default: |
241 | return -EINVAL; |
242 | } |
243 | |
244 | *val = pspval.intval; |
245 | |
246 | return 0; |
247 | } |
248 | |
249 | static int |
250 | power_supply_hwmon_write(struct device *dev, enum hwmon_sensor_types type, |
251 | u32 attr, int channel, long val) |
252 | { |
253 | struct power_supply_hwmon *psyhw = dev_get_drvdata(dev); |
254 | struct power_supply *psy = psyhw->psy; |
255 | union power_supply_propval pspval; |
256 | int prop; |
257 | |
258 | prop = power_supply_hwmon_to_property(type, attr, channel); |
259 | if (prop < 0) |
260 | return prop; |
261 | |
262 | pspval.intval = val; |
263 | |
264 | switch (type) { |
265 | /* |
266 | * Both voltage and current is reported in units of |
267 | * microvolts/microamps, so we need to adjust it to |
268 | * milliamps(volts) |
269 | */ |
270 | case hwmon_curr: |
271 | case hwmon_in: |
272 | if (check_mul_overflow(pspval.intval, 1000, |
273 | &pspval.intval)) |
274 | return -EOVERFLOW; |
275 | break; |
276 | /* |
277 | * Temp needs to be converted from 1/10 C to milli-C |
278 | */ |
279 | case hwmon_temp: |
280 | pspval.intval = DIV_ROUND_CLOSEST(pspval.intval, 100); |
281 | break; |
282 | default: |
283 | return -EINVAL; |
284 | } |
285 | |
286 | return power_supply_set_property(psy, psp: prop, val: &pspval); |
287 | } |
288 | |
289 | static const struct hwmon_ops power_supply_hwmon_ops = { |
290 | .is_visible = power_supply_hwmon_is_visible, |
291 | .read = power_supply_hwmon_read, |
292 | .write = power_supply_hwmon_write, |
293 | .read_string = power_supply_hwmon_read_string, |
294 | }; |
295 | |
296 | static const struct hwmon_channel_info * const power_supply_hwmon_info[] = { |
297 | HWMON_CHANNEL_INFO(temp, |
298 | HWMON_T_LABEL | |
299 | HWMON_T_INPUT | |
300 | HWMON_T_MAX | |
301 | HWMON_T_MIN | |
302 | HWMON_T_MIN_ALARM, |
303 | |
304 | HWMON_T_LABEL | |
305 | HWMON_T_INPUT | |
306 | HWMON_T_MIN_ALARM | |
307 | HWMON_T_MAX_ALARM), |
308 | |
309 | HWMON_CHANNEL_INFO(curr, |
310 | HWMON_C_AVERAGE | |
311 | HWMON_C_MAX | |
312 | HWMON_C_INPUT), |
313 | |
314 | HWMON_CHANNEL_INFO(in, |
315 | HWMON_I_AVERAGE | |
316 | HWMON_I_MIN | |
317 | HWMON_I_MAX | |
318 | HWMON_I_INPUT), |
319 | NULL |
320 | }; |
321 | |
322 | static const struct hwmon_chip_info power_supply_hwmon_chip_info = { |
323 | .ops = &power_supply_hwmon_ops, |
324 | .info = power_supply_hwmon_info, |
325 | }; |
326 | |
327 | int power_supply_add_hwmon_sysfs(struct power_supply *psy) |
328 | { |
329 | const struct power_supply_desc *desc = psy->desc; |
330 | struct power_supply_hwmon *psyhw; |
331 | struct device *dev = &psy->dev; |
332 | struct device *hwmon; |
333 | int ret, i; |
334 | const char *name; |
335 | |
336 | if (!devres_open_group(dev, id: power_supply_add_hwmon_sysfs, |
337 | GFP_KERNEL)) |
338 | return -ENOMEM; |
339 | |
340 | psyhw = devm_kzalloc(dev, size: sizeof(*psyhw), GFP_KERNEL); |
341 | if (!psyhw) { |
342 | ret = -ENOMEM; |
343 | goto error; |
344 | } |
345 | |
346 | psyhw->psy = psy; |
347 | psyhw->props = devm_bitmap_zalloc(dev, |
348 | nbits: POWER_SUPPLY_PROP_TIME_TO_FULL_AVG + 1, |
349 | GFP_KERNEL); |
350 | if (!psyhw->props) { |
351 | ret = -ENOMEM; |
352 | goto error; |
353 | } |
354 | |
355 | for (i = 0; i < desc->num_properties; i++) { |
356 | const enum power_supply_property prop = desc->properties[i]; |
357 | |
358 | switch (prop) { |
359 | case POWER_SUPPLY_PROP_CURRENT_AVG: |
360 | case POWER_SUPPLY_PROP_CURRENT_MAX: |
361 | case POWER_SUPPLY_PROP_CURRENT_NOW: |
362 | case POWER_SUPPLY_PROP_TEMP: |
363 | case POWER_SUPPLY_PROP_TEMP_MAX: |
364 | case POWER_SUPPLY_PROP_TEMP_MIN: |
365 | case POWER_SUPPLY_PROP_TEMP_ALERT_MIN: |
366 | case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: |
367 | case POWER_SUPPLY_PROP_TEMP_AMBIENT: |
368 | case POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MIN: |
369 | case POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MAX: |
370 | case POWER_SUPPLY_PROP_VOLTAGE_AVG: |
371 | case POWER_SUPPLY_PROP_VOLTAGE_MIN: |
372 | case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
373 | case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
374 | set_bit(nr: prop, addr: psyhw->props); |
375 | break; |
376 | default: |
377 | break; |
378 | } |
379 | } |
380 | |
381 | name = psy->desc->name; |
382 | if (strchr(name, '-')) { |
383 | char *new_name; |
384 | |
385 | new_name = devm_kstrdup(dev, s: name, GFP_KERNEL); |
386 | if (!new_name) { |
387 | ret = -ENOMEM; |
388 | goto error; |
389 | } |
390 | strreplace(str: new_name, old: '-', new: '_'); |
391 | name = new_name; |
392 | } |
393 | hwmon = devm_hwmon_device_register_with_info(dev, name, |
394 | drvdata: psyhw, |
395 | info: &power_supply_hwmon_chip_info, |
396 | NULL); |
397 | ret = PTR_ERR_OR_ZERO(ptr: hwmon); |
398 | if (ret) |
399 | goto error; |
400 | |
401 | devres_close_group(dev, id: power_supply_add_hwmon_sysfs); |
402 | return 0; |
403 | error: |
404 | devres_release_group(dev, NULL); |
405 | return ret; |
406 | } |
407 | |
408 | void power_supply_remove_hwmon_sysfs(struct power_supply *psy) |
409 | { |
410 | devres_release_group(dev: &psy->dev, id: power_supply_add_hwmon_sysfs); |
411 | } |
412 | |