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