1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Input driver for slidebars on some Lenovo IdeaPad laptops |
4 | * |
5 | * Copyright (C) 2013 Andrey Moiseev <o2g.org.ru@gmail.com> |
6 | * |
7 | * Reverse-engineered from Lenovo SlideNav software (SBarHook.dll). |
8 | * |
9 | * Trademarks are the property of their respective owners. |
10 | */ |
11 | |
12 | /* |
13 | * Currently tested and works on: |
14 | * Lenovo IdeaPad Y550 |
15 | * Lenovo IdeaPad Y550P |
16 | * |
17 | * Other models can be added easily. To test, |
18 | * load with 'force' parameter set 'true'. |
19 | * |
20 | * LEDs blinking and input mode are managed via sysfs, |
21 | * (hex, unsigned byte value): |
22 | * /sys/devices/platform/ideapad_slidebar/slidebar_mode |
23 | * |
24 | * The value is in byte range, however, I only figured out |
25 | * how bits 0b10011001 work. Some other bits, probably, |
26 | * are meaningfull too. |
27 | * |
28 | * Possible states: |
29 | * |
30 | * STD_INT, ONMOV_INT, OFF_INT, LAST_POLL, OFF_POLL |
31 | * |
32 | * Meaning: |
33 | * released touched |
34 | * STD 'heartbeat' lights follow the finger |
35 | * ONMOV no lights lights follow the finger |
36 | * LAST at last pos lights follow the finger |
37 | * OFF no lights no lights |
38 | * |
39 | * INT all input events are generated, interrupts are used |
40 | * POLL no input events by default, to get them, |
41 | * send 0b10000000 (read below) |
42 | * |
43 | * Commands: write |
44 | * |
45 | * All | 0b01001 -> STD_INT |
46 | * possible | 0b10001 -> ONMOV_INT |
47 | * states | 0b01000 -> OFF_INT |
48 | * |
49 | * | 0b0 -> LAST_POLL |
50 | * STD_INT or ONMOV_INT | |
51 | * | 0b1 -> STD_INT |
52 | * |
53 | * | 0b0 -> OFF_POLL |
54 | * OFF_INT or OFF_POLL | |
55 | * | 0b1 -> OFF_INT |
56 | * |
57 | * Any state | 0b10000000 -> if the slidebar has updated data, |
58 | * produce one input event (last position), |
59 | * switch to respective POLL mode |
60 | * (like 0x0), if not in POLL mode yet. |
61 | * |
62 | * Get current state: read |
63 | * |
64 | * masked by 0x11 read value means: |
65 | * |
66 | * 0x00 LAST |
67 | * 0x01 STD |
68 | * 0x10 OFF |
69 | * 0x11 ONMOV |
70 | */ |
71 | |
72 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
73 | |
74 | #include <linux/module.h> |
75 | #include <linux/kernel.h> |
76 | #include <linux/dmi.h> |
77 | #include <linux/spinlock.h> |
78 | #include <linux/platform_device.h> |
79 | #include <linux/input.h> |
80 | #include <linux/io.h> |
81 | #include <linux/ioport.h> |
82 | #include <linux/i8042.h> |
83 | #include <linux/serio.h> |
84 | |
85 | #define IDEAPAD_BASE 0xff29 |
86 | |
87 | static bool force; |
88 | module_param(force, bool, 0); |
89 | MODULE_PARM_DESC(force, "Force driver load, ignore DMI data" ); |
90 | |
91 | static DEFINE_SPINLOCK(io_lock); |
92 | |
93 | static struct input_dev *slidebar_input_dev; |
94 | static struct platform_device *slidebar_platform_dev; |
95 | |
96 | static u8 slidebar_pos_get(void) |
97 | { |
98 | u8 res; |
99 | unsigned long flags; |
100 | |
101 | spin_lock_irqsave(&io_lock, flags); |
102 | outb(value: 0xf4, port: 0xff29); |
103 | outb(value: 0xbf, port: 0xff2a); |
104 | res = inb(port: 0xff2b); |
105 | spin_unlock_irqrestore(lock: &io_lock, flags); |
106 | |
107 | return res; |
108 | } |
109 | |
110 | static u8 slidebar_mode_get(void) |
111 | { |
112 | u8 res; |
113 | unsigned long flags; |
114 | |
115 | spin_lock_irqsave(&io_lock, flags); |
116 | outb(value: 0xf7, port: 0xff29); |
117 | outb(value: 0x8b, port: 0xff2a); |
118 | res = inb(port: 0xff2b); |
119 | spin_unlock_irqrestore(lock: &io_lock, flags); |
120 | |
121 | return res; |
122 | } |
123 | |
124 | static void slidebar_mode_set(u8 mode) |
125 | { |
126 | unsigned long flags; |
127 | |
128 | spin_lock_irqsave(&io_lock, flags); |
129 | outb(value: 0xf7, port: 0xff29); |
130 | outb(value: 0x8b, port: 0xff2a); |
131 | outb(value: mode, port: 0xff2b); |
132 | spin_unlock_irqrestore(lock: &io_lock, flags); |
133 | } |
134 | |
135 | static bool slidebar_i8042_filter(unsigned char data, unsigned char str, |
136 | struct serio *port) |
137 | { |
138 | static bool extended = false; |
139 | |
140 | /* We are only interested in data coming form KBC port */ |
141 | if (str & I8042_STR_AUXDATA) |
142 | return false; |
143 | |
144 | /* Scancodes: e03b on move, e0bb on release. */ |
145 | if (data == 0xe0) { |
146 | extended = true; |
147 | return true; |
148 | } |
149 | |
150 | if (!extended) |
151 | return false; |
152 | |
153 | extended = false; |
154 | |
155 | if (likely((data & 0x7f) != 0x3b)) { |
156 | serio_interrupt(serio: port, data: 0xe0, flags: 0); |
157 | return false; |
158 | } |
159 | |
160 | if (data & 0x80) { |
161 | input_report_key(dev: slidebar_input_dev, BTN_TOUCH, value: 0); |
162 | } else { |
163 | input_report_key(dev: slidebar_input_dev, BTN_TOUCH, value: 1); |
164 | input_report_abs(dev: slidebar_input_dev, ABS_X, value: slidebar_pos_get()); |
165 | } |
166 | input_sync(dev: slidebar_input_dev); |
167 | |
168 | return true; |
169 | } |
170 | |
171 | static ssize_t show_slidebar_mode(struct device *dev, |
172 | struct device_attribute *attr, |
173 | char *buf) |
174 | { |
175 | return sprintf(buf, fmt: "%x\n" , slidebar_mode_get()); |
176 | } |
177 | |
178 | static ssize_t store_slidebar_mode(struct device *dev, |
179 | struct device_attribute *attr, |
180 | const char *buf, size_t count) |
181 | { |
182 | u8 mode; |
183 | int error; |
184 | |
185 | error = kstrtou8(s: buf, base: 0, res: &mode); |
186 | if (error) |
187 | return error; |
188 | |
189 | slidebar_mode_set(mode); |
190 | |
191 | return count; |
192 | } |
193 | |
194 | static DEVICE_ATTR(slidebar_mode, S_IWUSR | S_IRUGO, |
195 | show_slidebar_mode, store_slidebar_mode); |
196 | |
197 | static struct attribute *ideapad_attrs[] = { |
198 | &dev_attr_slidebar_mode.attr, |
199 | NULL |
200 | }; |
201 | |
202 | static struct attribute_group ideapad_attr_group = { |
203 | .attrs = ideapad_attrs |
204 | }; |
205 | |
206 | static const struct attribute_group *ideapad_attr_groups[] = { |
207 | &ideapad_attr_group, |
208 | NULL |
209 | }; |
210 | |
211 | static int __init ideapad_probe(struct platform_device* pdev) |
212 | { |
213 | int err; |
214 | |
215 | if (!request_region(IDEAPAD_BASE, 3, "ideapad_slidebar" )) { |
216 | dev_err(&pdev->dev, "IO ports are busy\n" ); |
217 | return -EBUSY; |
218 | } |
219 | |
220 | slidebar_input_dev = input_allocate_device(); |
221 | if (!slidebar_input_dev) { |
222 | dev_err(&pdev->dev, "Failed to allocate input device\n" ); |
223 | err = -ENOMEM; |
224 | goto err_release_ports; |
225 | } |
226 | |
227 | slidebar_input_dev->name = "IdeaPad Slidebar" ; |
228 | slidebar_input_dev->id.bustype = BUS_HOST; |
229 | slidebar_input_dev->dev.parent = &pdev->dev; |
230 | input_set_capability(dev: slidebar_input_dev, EV_KEY, BTN_TOUCH); |
231 | input_set_capability(dev: slidebar_input_dev, EV_ABS, ABS_X); |
232 | input_set_abs_params(dev: slidebar_input_dev, ABS_X, min: 0, max: 0xff, fuzz: 0, flat: 0); |
233 | |
234 | err = i8042_install_filter(filter: slidebar_i8042_filter); |
235 | if (err) { |
236 | dev_err(&pdev->dev, |
237 | "Failed to install i8042 filter: %d\n" , err); |
238 | goto err_free_dev; |
239 | } |
240 | |
241 | err = input_register_device(slidebar_input_dev); |
242 | if (err) { |
243 | dev_err(&pdev->dev, |
244 | "Failed to register input device: %d\n" , err); |
245 | goto err_remove_filter; |
246 | } |
247 | |
248 | return 0; |
249 | |
250 | err_remove_filter: |
251 | i8042_remove_filter(filter: slidebar_i8042_filter); |
252 | err_free_dev: |
253 | input_free_device(dev: slidebar_input_dev); |
254 | err_release_ports: |
255 | release_region(IDEAPAD_BASE, 3); |
256 | return err; |
257 | } |
258 | |
259 | static void ideapad_remove(struct platform_device *pdev) |
260 | { |
261 | i8042_remove_filter(filter: slidebar_i8042_filter); |
262 | input_unregister_device(slidebar_input_dev); |
263 | release_region(IDEAPAD_BASE, 3); |
264 | } |
265 | |
266 | static struct platform_driver slidebar_drv = { |
267 | .driver = { |
268 | .name = "ideapad_slidebar" , |
269 | }, |
270 | .remove_new = ideapad_remove, |
271 | }; |
272 | |
273 | static int __init ideapad_dmi_check(const struct dmi_system_id *id) |
274 | { |
275 | pr_info("Laptop model '%s'\n" , id->ident); |
276 | return 1; |
277 | } |
278 | |
279 | static const struct dmi_system_id ideapad_dmi[] __initconst = { |
280 | { |
281 | .ident = "Lenovo IdeaPad Y550" , |
282 | .matches = { |
283 | DMI_MATCH(DMI_SYS_VENDOR, "LENOVO" ), |
284 | DMI_MATCH(DMI_PRODUCT_NAME, "20017" ), |
285 | DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo IdeaPad Y550" ) |
286 | }, |
287 | .callback = ideapad_dmi_check |
288 | }, |
289 | { |
290 | .ident = "Lenovo IdeaPad Y550P" , |
291 | .matches = { |
292 | DMI_MATCH(DMI_SYS_VENDOR, "LENOVO" ), |
293 | DMI_MATCH(DMI_PRODUCT_NAME, "20035" ), |
294 | DMI_MATCH(DMI_PRODUCT_VERSION, "Lenovo IdeaPad Y550P" ) |
295 | }, |
296 | .callback = ideapad_dmi_check |
297 | }, |
298 | { NULL, } |
299 | }; |
300 | MODULE_DEVICE_TABLE(dmi, ideapad_dmi); |
301 | |
302 | static int __init slidebar_init(void) |
303 | { |
304 | int err; |
305 | |
306 | if (!force && !dmi_check_system(list: ideapad_dmi)) { |
307 | pr_err("DMI does not match\n" ); |
308 | return -ENODEV; |
309 | } |
310 | |
311 | slidebar_platform_dev = platform_device_alloc(name: "ideapad_slidebar" , id: -1); |
312 | if (!slidebar_platform_dev) { |
313 | pr_err("Not enough memory\n" ); |
314 | return -ENOMEM; |
315 | } |
316 | |
317 | slidebar_platform_dev->dev.groups = ideapad_attr_groups; |
318 | |
319 | err = platform_device_add(pdev: slidebar_platform_dev); |
320 | if (err) { |
321 | pr_err("Failed to register platform device\n" ); |
322 | goto err_free_dev; |
323 | } |
324 | |
325 | err = platform_driver_probe(&slidebar_drv, ideapad_probe); |
326 | if (err) { |
327 | pr_err("Failed to register platform driver\n" ); |
328 | goto err_delete_dev; |
329 | } |
330 | |
331 | return 0; |
332 | |
333 | err_delete_dev: |
334 | platform_device_del(pdev: slidebar_platform_dev); |
335 | err_free_dev: |
336 | platform_device_put(pdev: slidebar_platform_dev); |
337 | return err; |
338 | } |
339 | |
340 | static void __exit slidebar_exit(void) |
341 | { |
342 | platform_device_unregister(slidebar_platform_dev); |
343 | platform_driver_unregister(&slidebar_drv); |
344 | } |
345 | |
346 | module_init(slidebar_init); |
347 | module_exit(slidebar_exit); |
348 | |
349 | MODULE_AUTHOR("Andrey Moiseev <o2g.org.ru@gmail.com>" ); |
350 | MODULE_DESCRIPTION("Slidebar input support for some Lenovo IdeaPad laptops" ); |
351 | MODULE_LICENSE("GPL" ); |
352 | |