1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * Platform driver for OneXPlayer, AOK ZOE, and Aya Neo Handhelds that expose |
4 | * fan reading and control via hwmon sysfs. |
5 | * |
6 | * Old OXP boards have the same DMI strings and they are told apart by |
7 | * the boot cpu vendor (Intel/AMD). Currently only AMD boards are |
8 | * supported but the code is made to be simple to add other handheld |
9 | * boards in the future. |
10 | * Fan control is provided via pwm interface in the range [0-255]. |
11 | * Old AMD boards use [0-100] as range in the EC, the written value is |
12 | * scaled to accommodate for that. Newer boards like the mini PRO and |
13 | * AOK ZOE are not scaled but have the same EC layout. |
14 | * |
15 | * Copyright (C) 2022 Joaquín I. Aramendía <samsagax@gmail.com> |
16 | */ |
17 | |
18 | #include <linux/acpi.h> |
19 | #include <linux/dmi.h> |
20 | #include <linux/hwmon.h> |
21 | #include <linux/init.h> |
22 | #include <linux/kernel.h> |
23 | #include <linux/module.h> |
24 | #include <linux/platform_device.h> |
25 | #include <linux/processor.h> |
26 | |
27 | /* Handle ACPI lock mechanism */ |
28 | static u32 oxp_mutex; |
29 | |
30 | #define ACPI_LOCK_DELAY_MS 500 |
31 | |
32 | static bool lock_global_acpi_lock(void) |
33 | { |
34 | return ACPI_SUCCESS(acpi_acquire_global_lock(ACPI_LOCK_DELAY_MS, &oxp_mutex)); |
35 | } |
36 | |
37 | static bool unlock_global_acpi_lock(void) |
38 | { |
39 | return ACPI_SUCCESS(acpi_release_global_lock(oxp_mutex)); |
40 | } |
41 | |
42 | enum oxp_board { |
43 | aok_zoe_a1 = 1, |
44 | aya_neo_2, |
45 | aya_neo_air, |
46 | aya_neo_air_plus_mendo, |
47 | aya_neo_air_pro, |
48 | aya_neo_geek, |
49 | oxp_mini_amd, |
50 | oxp_mini_amd_a07, |
51 | oxp_mini_amd_pro, |
52 | }; |
53 | |
54 | static enum oxp_board board; |
55 | |
56 | /* Fan reading and PWM */ |
57 | #define OXP_SENSOR_FAN_REG 0x76 /* Fan reading is 2 registers long */ |
58 | #define OXP_SENSOR_PWM_ENABLE_REG 0x4A /* PWM enable is 1 register long */ |
59 | #define OXP_SENSOR_PWM_REG 0x4B /* PWM reading is 1 register long */ |
60 | |
61 | /* Turbo button takeover function |
62 | * Older boards have different values and EC registers |
63 | * for the same function |
64 | */ |
65 | #define OXP_OLD_TURBO_SWITCH_REG 0x1E |
66 | #define OXP_OLD_TURBO_TAKE_VAL 0x01 |
67 | #define OXP_OLD_TURBO_RETURN_VAL 0x00 |
68 | |
69 | #define OXP_TURBO_SWITCH_REG 0xF1 |
70 | #define OXP_TURBO_TAKE_VAL 0x40 |
71 | #define OXP_TURBO_RETURN_VAL 0x00 |
72 | |
73 | static const struct dmi_system_id dmi_table[] = { |
74 | { |
75 | .matches = { |
76 | DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE" ), |
77 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07" ), |
78 | }, |
79 | .driver_data = (void *)aok_zoe_a1, |
80 | }, |
81 | { |
82 | .matches = { |
83 | DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE" ), |
84 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro" ), |
85 | }, |
86 | .driver_data = (void *)aok_zoe_a1, |
87 | }, |
88 | { |
89 | .matches = { |
90 | DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO" ), |
91 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 2" ), |
92 | }, |
93 | .driver_data = (void *)aya_neo_2, |
94 | }, |
95 | { |
96 | .matches = { |
97 | DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO" ), |
98 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR" ), |
99 | }, |
100 | .driver_data = (void *)aya_neo_air, |
101 | }, |
102 | { |
103 | .matches = { |
104 | DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO" ), |
105 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "AB05-Mendocino" ), |
106 | }, |
107 | .driver_data = (void *)aya_neo_air_plus_mendo, |
108 | }, |
109 | { |
110 | .matches = { |
111 | DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO" ), |
112 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro" ), |
113 | }, |
114 | .driver_data = (void *)aya_neo_air_pro, |
115 | }, |
116 | { |
117 | .matches = { |
118 | DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO" ), |
119 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "GEEK" ), |
120 | }, |
121 | .driver_data = (void *)aya_neo_geek, |
122 | }, |
123 | { |
124 | .matches = { |
125 | DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK" ), |
126 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONE XPLAYER" ), |
127 | }, |
128 | .driver_data = (void *)oxp_mini_amd, |
129 | }, |
130 | { |
131 | .matches = { |
132 | DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK" ), |
133 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER mini A07" ), |
134 | }, |
135 | .driver_data = (void *)oxp_mini_amd_a07, |
136 | }, |
137 | { |
138 | .matches = { |
139 | DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK" ), |
140 | DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER Mini Pro" ), |
141 | }, |
142 | .driver_data = (void *)oxp_mini_amd_pro, |
143 | }, |
144 | {}, |
145 | }; |
146 | |
147 | /* Helper functions to handle EC read/write */ |
148 | static int read_from_ec(u8 reg, int size, long *val) |
149 | { |
150 | int i; |
151 | int ret; |
152 | u8 buffer; |
153 | |
154 | if (!lock_global_acpi_lock()) |
155 | return -EBUSY; |
156 | |
157 | *val = 0; |
158 | for (i = 0; i < size; i++) { |
159 | ret = ec_read(addr: reg + i, val: &buffer); |
160 | if (ret) |
161 | return ret; |
162 | *val <<= i * 8; |
163 | *val += buffer; |
164 | } |
165 | |
166 | if (!unlock_global_acpi_lock()) |
167 | return -EBUSY; |
168 | |
169 | return 0; |
170 | } |
171 | |
172 | static int write_to_ec(u8 reg, u8 value) |
173 | { |
174 | int ret; |
175 | |
176 | if (!lock_global_acpi_lock()) |
177 | return -EBUSY; |
178 | |
179 | ret = ec_write(addr: reg, val: value); |
180 | |
181 | if (!unlock_global_acpi_lock()) |
182 | return -EBUSY; |
183 | |
184 | return ret; |
185 | } |
186 | |
187 | /* Turbo button toggle functions */ |
188 | static int tt_toggle_enable(void) |
189 | { |
190 | u8 reg; |
191 | u8 val; |
192 | |
193 | switch (board) { |
194 | case oxp_mini_amd_a07: |
195 | reg = OXP_OLD_TURBO_SWITCH_REG; |
196 | val = OXP_OLD_TURBO_TAKE_VAL; |
197 | break; |
198 | case oxp_mini_amd_pro: |
199 | case aok_zoe_a1: |
200 | reg = OXP_TURBO_SWITCH_REG; |
201 | val = OXP_TURBO_TAKE_VAL; |
202 | break; |
203 | default: |
204 | return -EINVAL; |
205 | } |
206 | return write_to_ec(reg, value: val); |
207 | } |
208 | |
209 | static int tt_toggle_disable(void) |
210 | { |
211 | u8 reg; |
212 | u8 val; |
213 | |
214 | switch (board) { |
215 | case oxp_mini_amd_a07: |
216 | reg = OXP_OLD_TURBO_SWITCH_REG; |
217 | val = OXP_OLD_TURBO_RETURN_VAL; |
218 | break; |
219 | case oxp_mini_amd_pro: |
220 | case aok_zoe_a1: |
221 | reg = OXP_TURBO_SWITCH_REG; |
222 | val = OXP_TURBO_RETURN_VAL; |
223 | break; |
224 | default: |
225 | return -EINVAL; |
226 | } |
227 | return write_to_ec(reg, value: val); |
228 | } |
229 | |
230 | /* Callbacks for turbo toggle attribute */ |
231 | static umode_t tt_toggle_is_visible(struct kobject *kobj, |
232 | struct attribute *attr, int n) |
233 | { |
234 | switch (board) { |
235 | case aok_zoe_a1: |
236 | case oxp_mini_amd_a07: |
237 | case oxp_mini_amd_pro: |
238 | return attr->mode; |
239 | default: |
240 | break; |
241 | } |
242 | return 0; |
243 | } |
244 | |
245 | static ssize_t tt_toggle_store(struct device *dev, |
246 | struct device_attribute *attr, const char *buf, |
247 | size_t count) |
248 | { |
249 | int rval; |
250 | bool value; |
251 | |
252 | rval = kstrtobool(s: buf, res: &value); |
253 | if (rval) |
254 | return rval; |
255 | |
256 | if (value) { |
257 | rval = tt_toggle_enable(); |
258 | } else { |
259 | rval = tt_toggle_disable(); |
260 | } |
261 | if (rval) |
262 | return rval; |
263 | |
264 | return count; |
265 | } |
266 | |
267 | static ssize_t tt_toggle_show(struct device *dev, |
268 | struct device_attribute *attr, char *buf) |
269 | { |
270 | int retval; |
271 | u8 reg; |
272 | long val; |
273 | |
274 | switch (board) { |
275 | case oxp_mini_amd_a07: |
276 | reg = OXP_OLD_TURBO_SWITCH_REG; |
277 | break; |
278 | case oxp_mini_amd_pro: |
279 | case aok_zoe_a1: |
280 | reg = OXP_TURBO_SWITCH_REG; |
281 | break; |
282 | default: |
283 | return -EINVAL; |
284 | } |
285 | |
286 | retval = read_from_ec(reg, size: 1, val: &val); |
287 | if (retval) |
288 | return retval; |
289 | |
290 | return sysfs_emit(buf, fmt: "%d\n" , !!val); |
291 | } |
292 | |
293 | static DEVICE_ATTR_RW(tt_toggle); |
294 | |
295 | /* PWM enable/disable functions */ |
296 | static int oxp_pwm_enable(void) |
297 | { |
298 | return write_to_ec(OXP_SENSOR_PWM_ENABLE_REG, value: 0x01); |
299 | } |
300 | |
301 | static int oxp_pwm_disable(void) |
302 | { |
303 | return write_to_ec(OXP_SENSOR_PWM_ENABLE_REG, value: 0x00); |
304 | } |
305 | |
306 | /* Callbacks for hwmon interface */ |
307 | static umode_t oxp_ec_hwmon_is_visible(const void *drvdata, |
308 | enum hwmon_sensor_types type, u32 attr, int channel) |
309 | { |
310 | switch (type) { |
311 | case hwmon_fan: |
312 | return 0444; |
313 | case hwmon_pwm: |
314 | return 0644; |
315 | default: |
316 | return 0; |
317 | } |
318 | } |
319 | |
320 | static int oxp_platform_read(struct device *dev, enum hwmon_sensor_types type, |
321 | u32 attr, int channel, long *val) |
322 | { |
323 | int ret; |
324 | |
325 | switch (type) { |
326 | case hwmon_fan: |
327 | switch (attr) { |
328 | case hwmon_fan_input: |
329 | return read_from_ec(OXP_SENSOR_FAN_REG, size: 2, val); |
330 | default: |
331 | break; |
332 | } |
333 | break; |
334 | case hwmon_pwm: |
335 | switch (attr) { |
336 | case hwmon_pwm_input: |
337 | ret = read_from_ec(OXP_SENSOR_PWM_REG, size: 1, val); |
338 | if (ret) |
339 | return ret; |
340 | switch (board) { |
341 | case aya_neo_2: |
342 | case aya_neo_air: |
343 | case aya_neo_air_plus_mendo: |
344 | case aya_neo_air_pro: |
345 | case aya_neo_geek: |
346 | case oxp_mini_amd: |
347 | case oxp_mini_amd_a07: |
348 | *val = (*val * 255) / 100; |
349 | break; |
350 | case oxp_mini_amd_pro: |
351 | case aok_zoe_a1: |
352 | default: |
353 | break; |
354 | } |
355 | return 0; |
356 | case hwmon_pwm_enable: |
357 | return read_from_ec(OXP_SENSOR_PWM_ENABLE_REG, size: 1, val); |
358 | default: |
359 | break; |
360 | } |
361 | break; |
362 | default: |
363 | break; |
364 | } |
365 | return -EOPNOTSUPP; |
366 | } |
367 | |
368 | static int oxp_platform_write(struct device *dev, enum hwmon_sensor_types type, |
369 | u32 attr, int channel, long val) |
370 | { |
371 | switch (type) { |
372 | case hwmon_pwm: |
373 | switch (attr) { |
374 | case hwmon_pwm_enable: |
375 | if (val == 1) |
376 | return oxp_pwm_enable(); |
377 | else if (val == 0) |
378 | return oxp_pwm_disable(); |
379 | return -EINVAL; |
380 | case hwmon_pwm_input: |
381 | if (val < 0 || val > 255) |
382 | return -EINVAL; |
383 | switch (board) { |
384 | case aya_neo_2: |
385 | case aya_neo_air: |
386 | case aya_neo_air_plus_mendo: |
387 | case aya_neo_air_pro: |
388 | case aya_neo_geek: |
389 | case oxp_mini_amd: |
390 | case oxp_mini_amd_a07: |
391 | val = (val * 100) / 255; |
392 | break; |
393 | case aok_zoe_a1: |
394 | case oxp_mini_amd_pro: |
395 | default: |
396 | break; |
397 | } |
398 | return write_to_ec(OXP_SENSOR_PWM_REG, value: val); |
399 | default: |
400 | break; |
401 | } |
402 | break; |
403 | default: |
404 | break; |
405 | } |
406 | return -EOPNOTSUPP; |
407 | } |
408 | |
409 | /* Known sensors in the OXP EC controllers */ |
410 | static const struct hwmon_channel_info * const oxp_platform_sensors[] = { |
411 | HWMON_CHANNEL_INFO(fan, |
412 | HWMON_F_INPUT), |
413 | HWMON_CHANNEL_INFO(pwm, |
414 | HWMON_PWM_INPUT | HWMON_PWM_ENABLE), |
415 | NULL, |
416 | }; |
417 | |
418 | static struct attribute *oxp_ec_attrs[] = { |
419 | &dev_attr_tt_toggle.attr, |
420 | NULL |
421 | }; |
422 | |
423 | static struct attribute_group oxp_ec_attribute_group = { |
424 | .is_visible = tt_toggle_is_visible, |
425 | .attrs = oxp_ec_attrs, |
426 | }; |
427 | |
428 | static const struct attribute_group *oxp_ec_groups[] = { |
429 | &oxp_ec_attribute_group, |
430 | NULL |
431 | }; |
432 | |
433 | static const struct hwmon_ops oxp_ec_hwmon_ops = { |
434 | .is_visible = oxp_ec_hwmon_is_visible, |
435 | .read = oxp_platform_read, |
436 | .write = oxp_platform_write, |
437 | }; |
438 | |
439 | static const struct hwmon_chip_info oxp_ec_chip_info = { |
440 | .ops = &oxp_ec_hwmon_ops, |
441 | .info = oxp_platform_sensors, |
442 | }; |
443 | |
444 | /* Initialization logic */ |
445 | static int oxp_platform_probe(struct platform_device *pdev) |
446 | { |
447 | struct device *dev = &pdev->dev; |
448 | struct device *hwdev; |
449 | |
450 | hwdev = devm_hwmon_device_register_with_info(dev, name: "oxpec" , NULL, |
451 | info: &oxp_ec_chip_info, NULL); |
452 | |
453 | return PTR_ERR_OR_ZERO(ptr: hwdev); |
454 | } |
455 | |
456 | static struct platform_driver oxp_platform_driver = { |
457 | .driver = { |
458 | .name = "oxp-platform" , |
459 | .dev_groups = oxp_ec_groups, |
460 | }, |
461 | .probe = oxp_platform_probe, |
462 | }; |
463 | |
464 | static struct platform_device *oxp_platform_device; |
465 | |
466 | static int __init oxp_platform_init(void) |
467 | { |
468 | const struct dmi_system_id *dmi_entry; |
469 | |
470 | /* |
471 | * Have to check for AMD processor here because DMI strings are the |
472 | * same between Intel and AMD boards, the only way to tell them apart |
473 | * is the CPU. |
474 | * Intel boards seem to have different EC registers and values to |
475 | * read/write. |
476 | */ |
477 | dmi_entry = dmi_first_match(list: dmi_table); |
478 | if (!dmi_entry || boot_cpu_data.x86_vendor != X86_VENDOR_AMD) |
479 | return -ENODEV; |
480 | |
481 | board = (enum oxp_board)(unsigned long)dmi_entry->driver_data; |
482 | |
483 | oxp_platform_device = |
484 | platform_create_bundle(&oxp_platform_driver, |
485 | oxp_platform_probe, NULL, 0, NULL, 0); |
486 | |
487 | return PTR_ERR_OR_ZERO(ptr: oxp_platform_device); |
488 | } |
489 | |
490 | static void __exit oxp_platform_exit(void) |
491 | { |
492 | platform_device_unregister(oxp_platform_device); |
493 | platform_driver_unregister(&oxp_platform_driver); |
494 | } |
495 | |
496 | MODULE_DEVICE_TABLE(dmi, dmi_table); |
497 | |
498 | module_init(oxp_platform_init); |
499 | module_exit(oxp_platform_exit); |
500 | |
501 | MODULE_AUTHOR("Joaquín Ignacio Aramendía <samsagax@gmail.com>" ); |
502 | MODULE_DESCRIPTION("Platform driver that handles EC sensors of OneXPlayer devices" ); |
503 | MODULE_LICENSE("GPL" ); |
504 | |