1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * hp_accel.c - Interface between LIS3LV02DL driver and HP ACPI BIOS |
4 | * |
5 | * Copyright (C) 2007-2008 Yan Burman |
6 | * Copyright (C) 2008 Eric Piel |
7 | * Copyright (C) 2008-2009 Pavel Machek |
8 | */ |
9 | |
10 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
11 | |
12 | #include <linux/kernel.h> |
13 | #include <linux/init.h> |
14 | #include <linux/dmi.h> |
15 | #include <linux/module.h> |
16 | #include <linux/types.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/interrupt.h> |
19 | #include <linux/delay.h> |
20 | #include <linux/wait.h> |
21 | #include <linux/poll.h> |
22 | #include <linux/freezer.h> |
23 | #include <linux/uaccess.h> |
24 | #include <linux/leds.h> |
25 | #include <linux/atomic.h> |
26 | #include <linux/acpi.h> |
27 | #include <linux/i8042.h> |
28 | #include <linux/serio.h> |
29 | #include "../../../misc/lis3lv02d/lis3lv02d.h" |
30 | |
31 | /* Delayed LEDs infrastructure ------------------------------------ */ |
32 | |
33 | /* Special LED class that can defer work */ |
34 | struct delayed_led_classdev { |
35 | struct led_classdev led_classdev; |
36 | struct work_struct work; |
37 | enum led_brightness new_brightness; |
38 | |
39 | unsigned int led; /* For driver */ |
40 | void (*set_brightness)(struct delayed_led_classdev *data, enum led_brightness value); |
41 | }; |
42 | |
43 | static inline void delayed_set_status_worker(struct work_struct *work) |
44 | { |
45 | struct delayed_led_classdev *data = |
46 | container_of(work, struct delayed_led_classdev, work); |
47 | |
48 | data->set_brightness(data, data->new_brightness); |
49 | } |
50 | |
51 | static inline void delayed_sysfs_set(struct led_classdev *led_cdev, |
52 | enum led_brightness brightness) |
53 | { |
54 | struct delayed_led_classdev *data = container_of(led_cdev, |
55 | struct delayed_led_classdev, led_classdev); |
56 | data->new_brightness = brightness; |
57 | schedule_work(work: &data->work); |
58 | } |
59 | |
60 | /* HP-specific accelerometer driver ------------------------------------ */ |
61 | |
62 | /* e0 25, e0 26, e0 27, e0 28 are scan codes that the accelerometer with acpi id |
63 | * HPQ6000 sends through the keyboard bus */ |
64 | #define ACCEL_1 0x25 |
65 | #define ACCEL_2 0x26 |
66 | #define ACCEL_3 0x27 |
67 | #define ACCEL_4 0x28 |
68 | |
69 | /* For automatic insertion of the module */ |
70 | static const struct acpi_device_id lis3lv02d_device_ids[] = { |
71 | {"HPQ0004" , 0}, /* HP Mobile Data Protection System PNP */ |
72 | {"HPQ6000" , 0}, /* HP Mobile Data Protection System PNP */ |
73 | {"HPQ6007" , 0}, /* HP Mobile Data Protection System PNP */ |
74 | {"" , 0}, |
75 | }; |
76 | MODULE_DEVICE_TABLE(acpi, lis3lv02d_device_ids); |
77 | |
78 | /** |
79 | * lis3lv02d_acpi_init - initialize the device for ACPI |
80 | * @lis3: pointer to the device struct |
81 | * |
82 | * Returns 0 on success. |
83 | */ |
84 | static int lis3lv02d_acpi_init(struct lis3lv02d *lis3) |
85 | { |
86 | return 0; |
87 | } |
88 | |
89 | /** |
90 | * lis3lv02d_acpi_read - ACPI ALRD method: read a register |
91 | * @lis3: pointer to the device struct |
92 | * @reg: the register to read |
93 | * @ret: result of the operation |
94 | * |
95 | * Returns 0 on success. |
96 | */ |
97 | static int lis3lv02d_acpi_read(struct lis3lv02d *lis3, int reg, u8 *ret) |
98 | { |
99 | struct acpi_device *dev = lis3->bus_priv; |
100 | union acpi_object arg0 = { ACPI_TYPE_INTEGER }; |
101 | struct acpi_object_list args = { 1, &arg0 }; |
102 | unsigned long long lret; |
103 | acpi_status status; |
104 | |
105 | arg0.integer.value = reg; |
106 | |
107 | status = acpi_evaluate_integer(handle: dev->handle, pathname: "ALRD" , arguments: &args, data: &lret); |
108 | if (ACPI_FAILURE(status)) |
109 | return -EINVAL; |
110 | *ret = lret; |
111 | return 0; |
112 | } |
113 | |
114 | /** |
115 | * lis3lv02d_acpi_write - ACPI ALWR method: write to a register |
116 | * @lis3: pointer to the device struct |
117 | * @reg: the register to write to |
118 | * @val: the value to write |
119 | * |
120 | * Returns 0 on success. |
121 | */ |
122 | static int lis3lv02d_acpi_write(struct lis3lv02d *lis3, int reg, u8 val) |
123 | { |
124 | struct acpi_device *dev = lis3->bus_priv; |
125 | unsigned long long ret; /* Not used when writting */ |
126 | union acpi_object in_obj[2]; |
127 | struct acpi_object_list args = { 2, in_obj }; |
128 | |
129 | in_obj[0].type = ACPI_TYPE_INTEGER; |
130 | in_obj[0].integer.value = reg; |
131 | in_obj[1].type = ACPI_TYPE_INTEGER; |
132 | in_obj[1].integer.value = val; |
133 | |
134 | if (acpi_evaluate_integer(handle: dev->handle, pathname: "ALWR" , arguments: &args, data: &ret) != AE_OK) |
135 | return -EINVAL; |
136 | |
137 | return 0; |
138 | } |
139 | |
140 | static int lis3lv02d_dmi_matched(const struct dmi_system_id *dmi) |
141 | { |
142 | lis3_dev.ac = *((union axis_conversion *)dmi->driver_data); |
143 | pr_info("hardware type %s found\n" , dmi->ident); |
144 | |
145 | return 1; |
146 | } |
147 | |
148 | /* Represents, for each axis seen by userspace, the corresponding hw axis (+1). |
149 | * If the value is negative, the opposite of the hw value is used. */ |
150 | #define DEFINE_CONV(name, x, y, z) \ |
151 | static union axis_conversion lis3lv02d_axis_##name = \ |
152 | { .as_array = { x, y, z } } |
153 | DEFINE_CONV(normal, 1, 2, 3); |
154 | DEFINE_CONV(y_inverted, 1, -2, 3); |
155 | DEFINE_CONV(x_inverted, -1, 2, 3); |
156 | DEFINE_CONV(x_inverted_usd, -1, 2, -3); |
157 | DEFINE_CONV(z_inverted, 1, 2, -3); |
158 | DEFINE_CONV(xy_swap, 2, 1, 3); |
159 | DEFINE_CONV(xy_rotated_left, -2, 1, 3); |
160 | DEFINE_CONV(xy_rotated_left_usd, -2, 1, -3); |
161 | DEFINE_CONV(xy_swap_inverted, -2, -1, 3); |
162 | DEFINE_CONV(xy_rotated_right, 2, -1, 3); |
163 | DEFINE_CONV(xy_swap_yz_inverted, 2, -1, -3); |
164 | |
165 | #define AXIS_DMI_MATCH(_ident, _name, _axis) { \ |
166 | .ident = _ident, \ |
167 | .callback = lis3lv02d_dmi_matched, \ |
168 | .matches = { \ |
169 | DMI_MATCH(DMI_PRODUCT_NAME, _name) \ |
170 | }, \ |
171 | .driver_data = &lis3lv02d_axis_##_axis \ |
172 | } |
173 | |
174 | #define AXIS_DMI_MATCH2(_ident, _class1, _name1, \ |
175 | _class2, _name2, \ |
176 | _axis) { \ |
177 | .ident = _ident, \ |
178 | .callback = lis3lv02d_dmi_matched, \ |
179 | .matches = { \ |
180 | DMI_MATCH(DMI_##_class1, _name1), \ |
181 | DMI_MATCH(DMI_##_class2, _name2), \ |
182 | }, \ |
183 | .driver_data = &lis3lv02d_axis_##_axis \ |
184 | } |
185 | static const struct dmi_system_id lis3lv02d_dmi_ids[] = { |
186 | /* product names are truncated to match all kinds of a same model */ |
187 | AXIS_DMI_MATCH("NC64x0" , "HP Compaq nc64" , x_inverted), |
188 | AXIS_DMI_MATCH("NC84x0" , "HP Compaq nc84" , z_inverted), |
189 | AXIS_DMI_MATCH("NX9420" , "HP Compaq nx9420" , x_inverted), |
190 | AXIS_DMI_MATCH("NW9440" , "HP Compaq nw9440" , x_inverted), |
191 | AXIS_DMI_MATCH("NC2510" , "HP Compaq 2510" , y_inverted), |
192 | AXIS_DMI_MATCH("NC2710" , "HP Compaq 2710" , xy_swap), |
193 | AXIS_DMI_MATCH("NC8510" , "HP Compaq 8510" , xy_swap_inverted), |
194 | AXIS_DMI_MATCH("HP2133" , "HP 2133" , xy_rotated_left), |
195 | AXIS_DMI_MATCH("HP2140" , "HP 2140" , xy_swap_inverted), |
196 | AXIS_DMI_MATCH("NC653x" , "HP Compaq 653" , xy_rotated_left_usd), |
197 | AXIS_DMI_MATCH("NC6730b" , "HP Compaq 6730b" , xy_rotated_left_usd), |
198 | AXIS_DMI_MATCH("NC6730s" , "HP Compaq 6730s" , xy_swap), |
199 | AXIS_DMI_MATCH("NC651xx" , "HP Compaq 651" , xy_rotated_right), |
200 | AXIS_DMI_MATCH("NC6710x" , "HP Compaq 6710" , xy_swap_yz_inverted), |
201 | AXIS_DMI_MATCH("NC6715x" , "HP Compaq 6715" , y_inverted), |
202 | AXIS_DMI_MATCH("NC693xx" , "HP EliteBook 693" , xy_rotated_right), |
203 | AXIS_DMI_MATCH("NC693xx" , "HP EliteBook 853" , xy_swap), |
204 | AXIS_DMI_MATCH("NC854xx" , "HP EliteBook 854" , y_inverted), |
205 | AXIS_DMI_MATCH("NC273xx" , "HP EliteBook 273" , y_inverted), |
206 | /* Intel-based HP Pavilion dv5 */ |
207 | AXIS_DMI_MATCH2("HPDV5_I" , |
208 | PRODUCT_NAME, "HP Pavilion dv5" , |
209 | BOARD_NAME, "3603" , |
210 | x_inverted), |
211 | /* AMD-based HP Pavilion dv5 */ |
212 | AXIS_DMI_MATCH2("HPDV5_A" , |
213 | PRODUCT_NAME, "HP Pavilion dv5" , |
214 | BOARD_NAME, "3600" , |
215 | y_inverted), |
216 | AXIS_DMI_MATCH("DV7" , "HP Pavilion dv7" , x_inverted), |
217 | AXIS_DMI_MATCH("HP8710" , "HP Compaq 8710" , y_inverted), |
218 | AXIS_DMI_MATCH("HDX18" , "HP HDX 18" , x_inverted), |
219 | AXIS_DMI_MATCH("HPB432x" , "HP ProBook 432" , xy_rotated_left), |
220 | AXIS_DMI_MATCH("HPB440G3" , "HP ProBook 440 G3" , x_inverted_usd), |
221 | AXIS_DMI_MATCH("HPB440G4" , "HP ProBook 440 G4" , x_inverted), |
222 | AXIS_DMI_MATCH("HPB442x" , "HP ProBook 442" , xy_rotated_left), |
223 | AXIS_DMI_MATCH("HPB450G0" , "HP ProBook 450 G0" , x_inverted), |
224 | AXIS_DMI_MATCH("HPB452x" , "HP ProBook 452" , y_inverted), |
225 | AXIS_DMI_MATCH("HPB522x" , "HP ProBook 522" , xy_swap), |
226 | AXIS_DMI_MATCH("HPB532x" , "HP ProBook 532" , y_inverted), |
227 | AXIS_DMI_MATCH("HPB655x" , "HP ProBook 655" , xy_swap_inverted), |
228 | AXIS_DMI_MATCH("Mini510x" , "HP Mini 510" , xy_rotated_left_usd), |
229 | AXIS_DMI_MATCH("HPB63xx" , "HP ProBook 63" , xy_swap), |
230 | AXIS_DMI_MATCH("HPB64xx" , "HP ProBook 64" , xy_swap), |
231 | AXIS_DMI_MATCH("HPB64xx" , "HP EliteBook 84" , xy_swap), |
232 | AXIS_DMI_MATCH("HPB65xx" , "HP ProBook 65" , x_inverted), |
233 | AXIS_DMI_MATCH("HPZBook15" , "HP ZBook 15" , x_inverted), |
234 | AXIS_DMI_MATCH("HPZBook17G5" , "HP ZBook 17 G5" , x_inverted), |
235 | AXIS_DMI_MATCH("HPZBook17" , "HP ZBook 17" , xy_swap_yz_inverted), |
236 | { NULL, } |
237 | /* Laptop models without axis info (yet): |
238 | * "NC6910" "HP Compaq 6910" |
239 | * "NC2400" "HP Compaq nc2400" |
240 | * "NX74x0" "HP Compaq nx74" |
241 | * "NX6325" "HP Compaq nx6325" |
242 | * "NC4400" "HP Compaq nc4400" |
243 | */ |
244 | }; |
245 | |
246 | static void hpled_set(struct delayed_led_classdev *led_cdev, enum led_brightness value) |
247 | { |
248 | struct acpi_device *dev = lis3_dev.bus_priv; |
249 | unsigned long long ret; /* Not used when writing */ |
250 | union acpi_object in_obj[1]; |
251 | struct acpi_object_list args = { 1, in_obj }; |
252 | |
253 | in_obj[0].type = ACPI_TYPE_INTEGER; |
254 | in_obj[0].integer.value = !!value; |
255 | |
256 | acpi_evaluate_integer(handle: dev->handle, pathname: "ALED" , arguments: &args, data: &ret); |
257 | } |
258 | |
259 | static struct delayed_led_classdev hpled_led = { |
260 | .led_classdev = { |
261 | .name = "hp::hddprotect" , |
262 | .default_trigger = "none" , |
263 | .brightness_set = delayed_sysfs_set, |
264 | .flags = LED_CORE_SUSPENDRESUME, |
265 | }, |
266 | .set_brightness = hpled_set, |
267 | }; |
268 | |
269 | static bool hp_accel_i8042_filter(unsigned char data, unsigned char str, |
270 | struct serio *port) |
271 | { |
272 | static bool extended; |
273 | |
274 | if (str & I8042_STR_AUXDATA) |
275 | return false; |
276 | |
277 | if (data == 0xe0) { |
278 | extended = true; |
279 | return true; |
280 | } else if (unlikely(extended)) { |
281 | extended = false; |
282 | |
283 | switch (data) { |
284 | case ACCEL_1: |
285 | case ACCEL_2: |
286 | case ACCEL_3: |
287 | case ACCEL_4: |
288 | return true; |
289 | default: |
290 | serio_interrupt(serio: port, data: 0xe0, flags: 0); |
291 | return false; |
292 | } |
293 | } |
294 | |
295 | return false; |
296 | } |
297 | |
298 | static int lis3lv02d_probe(struct platform_device *device) |
299 | { |
300 | int ret; |
301 | |
302 | lis3_dev.bus_priv = ACPI_COMPANION(&device->dev); |
303 | lis3_dev.init = lis3lv02d_acpi_init; |
304 | lis3_dev.read = lis3lv02d_acpi_read; |
305 | lis3_dev.write = lis3lv02d_acpi_write; |
306 | |
307 | /* obtain IRQ number of our device from ACPI */ |
308 | ret = platform_get_irq_optional(device, 0); |
309 | if (ret > 0) |
310 | lis3_dev.irq = ret; |
311 | |
312 | /* If possible use a "standard" axes order */ |
313 | if (lis3_dev.ac.x && lis3_dev.ac.y && lis3_dev.ac.z) { |
314 | pr_info("Using custom axes %d,%d,%d\n" , |
315 | lis3_dev.ac.x, lis3_dev.ac.y, lis3_dev.ac.z); |
316 | } else if (dmi_check_system(list: lis3lv02d_dmi_ids) == 0) { |
317 | pr_info("laptop model unknown, using default axes configuration\n" ); |
318 | lis3_dev.ac = lis3lv02d_axis_normal; |
319 | } |
320 | |
321 | /* call the core layer do its init */ |
322 | ret = lis3lv02d_init_device(lis3: &lis3_dev); |
323 | if (ret) |
324 | return ret; |
325 | |
326 | /* filter to remove HPQ6000 accelerometer data |
327 | * from keyboard bus stream */ |
328 | if (strstr(dev_name(dev: &device->dev), "HPQ6000" )) |
329 | i8042_install_filter(filter: hp_accel_i8042_filter); |
330 | |
331 | INIT_WORK(&hpled_led.work, delayed_set_status_worker); |
332 | ret = led_classdev_register(NULL, led_cdev: &hpled_led.led_classdev); |
333 | if (ret) { |
334 | i8042_remove_filter(filter: hp_accel_i8042_filter); |
335 | lis3lv02d_joystick_disable(lis3: &lis3_dev); |
336 | lis3lv02d_poweroff(lis3: &lis3_dev); |
337 | flush_work(work: &hpled_led.work); |
338 | lis3lv02d_remove_fs(lis3: &lis3_dev); |
339 | return ret; |
340 | } |
341 | |
342 | return ret; |
343 | } |
344 | |
345 | static void lis3lv02d_remove(struct platform_device *device) |
346 | { |
347 | i8042_remove_filter(filter: hp_accel_i8042_filter); |
348 | lis3lv02d_joystick_disable(lis3: &lis3_dev); |
349 | lis3lv02d_poweroff(lis3: &lis3_dev); |
350 | |
351 | led_classdev_unregister(led_cdev: &hpled_led.led_classdev); |
352 | flush_work(work: &hpled_led.work); |
353 | |
354 | lis3lv02d_remove_fs(lis3: &lis3_dev); |
355 | } |
356 | |
357 | static int __maybe_unused lis3lv02d_suspend(struct device *dev) |
358 | { |
359 | /* make sure the device is off when we suspend */ |
360 | lis3lv02d_poweroff(lis3: &lis3_dev); |
361 | return 0; |
362 | } |
363 | |
364 | static int __maybe_unused lis3lv02d_resume(struct device *dev) |
365 | { |
366 | lis3lv02d_poweron(lis3: &lis3_dev); |
367 | return 0; |
368 | } |
369 | |
370 | static SIMPLE_DEV_PM_OPS(hp_accel_pm, lis3lv02d_suspend, lis3lv02d_resume); |
371 | |
372 | /* For the HP MDPS aka 3D Driveguard */ |
373 | static struct platform_driver lis3lv02d_driver = { |
374 | .probe = lis3lv02d_probe, |
375 | .remove_new = lis3lv02d_remove, |
376 | .driver = { |
377 | .name = "hp_accel" , |
378 | .pm = &hp_accel_pm, |
379 | .acpi_match_table = lis3lv02d_device_ids, |
380 | }, |
381 | }; |
382 | module_platform_driver(lis3lv02d_driver); |
383 | |
384 | MODULE_DESCRIPTION("Glue between LIS3LV02Dx and HP ACPI BIOS and support for disk protection LED." ); |
385 | MODULE_AUTHOR("Yan Burman, Eric Piel, Pavel Machek" ); |
386 | MODULE_LICENSE("GPL" ); |
387 | |