1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Platform driver for Lenovo Yoga Book YB1-X90F/L tablets (Android model) |
4 | * WMI driver for Lenovo Yoga Book YB1-X91F/L tablets (Windows model) |
5 | * |
6 | * The keyboard half of the YB1 models can function as both a capacitive |
7 | * touch keyboard or as a Wacom digitizer, but not at the same time. |
8 | * |
9 | * This driver takes care of switching between the 2 functions. |
10 | * |
11 | * Copyright 2023 Hans de Goede <hansg@kernel.org> |
12 | */ |
13 | |
14 | #include <linux/acpi.h> |
15 | #include <linux/gpio/consumer.h> |
16 | #include <linux/gpio/machine.h> |
17 | #include <linux/i2c.h> |
18 | #include <linux/interrupt.h> |
19 | #include <linux/leds.h> |
20 | #include <linux/module.h> |
21 | #include <linux/platform_device.h> |
22 | #include <linux/pwm.h> |
23 | #include <linux/wmi.h> |
24 | #include <linux/workqueue.h> |
25 | |
26 | #define YB_MBTN_EVENT_GUID "243FEC1D-1963-41C1-8100-06A9D82A94B4" |
27 | |
28 | #define YB_KBD_BL_DEFAULT 128 |
29 | #define YB_KBD_BL_MAX 255 |
30 | #define YB_KBD_BL_PWM_PERIOD 13333 |
31 | |
32 | #define YB_PDEV_NAME "yogabook-touch-kbd-digitizer-switch" |
33 | |
34 | /* flags */ |
35 | enum { |
36 | YB_KBD_IS_ON, |
37 | YB_DIGITIZER_IS_ON, |
38 | YB_DIGITIZER_MODE, |
39 | YB_TABLET_MODE, |
40 | YB_SUSPENDED, |
41 | }; |
42 | |
43 | struct yogabook_data { |
44 | struct device *dev; |
45 | struct acpi_device *kbd_adev; |
46 | struct acpi_device *dig_adev; |
47 | struct device *kbd_dev; |
48 | struct device *dig_dev; |
49 | struct led_classdev *pen_led; |
50 | struct gpio_desc *pen_touch_event; |
51 | struct gpio_desc *kbd_bl_led_enable; |
52 | struct gpio_desc *backside_hall_gpio; |
53 | struct pwm_device *kbd_bl_pwm; |
54 | int (*set_kbd_backlight)(struct yogabook_data *data, uint8_t level); |
55 | int pen_touch_irq; |
56 | int backside_hall_irq; |
57 | struct work_struct work; |
58 | struct led_classdev kbd_bl_led; |
59 | unsigned long flags; |
60 | uint8_t brightness; |
61 | }; |
62 | |
63 | static void yogabook_work(struct work_struct *work) |
64 | { |
65 | struct yogabook_data *data = container_of(work, struct yogabook_data, work); |
66 | bool kbd_on, digitizer_on; |
67 | int r; |
68 | |
69 | if (test_bit(YB_SUSPENDED, &data->flags)) |
70 | return; |
71 | |
72 | if (test_bit(YB_TABLET_MODE, &data->flags)) { |
73 | kbd_on = false; |
74 | digitizer_on = false; |
75 | } else if (test_bit(YB_DIGITIZER_MODE, &data->flags)) { |
76 | digitizer_on = true; |
77 | kbd_on = false; |
78 | } else { |
79 | kbd_on = true; |
80 | digitizer_on = false; |
81 | } |
82 | |
83 | if (!kbd_on && test_bit(YB_KBD_IS_ON, &data->flags)) { |
84 | /* |
85 | * Must be done before releasing the keyboard touchscreen driver, |
86 | * so that the keyboard touchscreen dev is still in D0. |
87 | */ |
88 | data->set_kbd_backlight(data, 0); |
89 | device_release_driver(dev: data->kbd_dev); |
90 | clear_bit(nr: YB_KBD_IS_ON, addr: &data->flags); |
91 | } |
92 | |
93 | if (!digitizer_on && test_bit(YB_DIGITIZER_IS_ON, &data->flags)) { |
94 | led_set_brightness(led_cdev: data->pen_led, brightness: LED_OFF); |
95 | device_release_driver(dev: data->dig_dev); |
96 | clear_bit(nr: YB_DIGITIZER_IS_ON, addr: &data->flags); |
97 | } |
98 | |
99 | if (kbd_on && !test_bit(YB_KBD_IS_ON, &data->flags)) { |
100 | r = device_reprobe(dev: data->kbd_dev); |
101 | if (r) |
102 | dev_warn(data->dev, "Reprobe of keyboard touchscreen failed: %d\n" , r); |
103 | |
104 | data->set_kbd_backlight(data, data->brightness); |
105 | set_bit(nr: YB_KBD_IS_ON, addr: &data->flags); |
106 | } |
107 | |
108 | if (digitizer_on && !test_bit(YB_DIGITIZER_IS_ON, &data->flags)) { |
109 | r = device_reprobe(dev: data->dig_dev); |
110 | if (r) |
111 | dev_warn(data->dev, "Reprobe of digitizer failed: %d\n" , r); |
112 | |
113 | led_set_brightness(led_cdev: data->pen_led, brightness: LED_FULL); |
114 | set_bit(nr: YB_DIGITIZER_IS_ON, addr: &data->flags); |
115 | } |
116 | } |
117 | |
118 | static void yogabook_toggle_digitizer_mode(struct yogabook_data *data) |
119 | { |
120 | if (test_bit(YB_SUSPENDED, &data->flags)) |
121 | return; |
122 | |
123 | if (test_bit(YB_DIGITIZER_MODE, &data->flags)) |
124 | clear_bit(nr: YB_DIGITIZER_MODE, addr: &data->flags); |
125 | else |
126 | set_bit(nr: YB_DIGITIZER_MODE, addr: &data->flags); |
127 | |
128 | /* |
129 | * We are called from the ACPI core and the driver [un]binding which is |
130 | * done also needs ACPI functions, use a workqueue to avoid deadlocking. |
131 | */ |
132 | schedule_work(work: &data->work); |
133 | } |
134 | |
135 | static irqreturn_t yogabook_backside_hall_irq(int irq, void *_data) |
136 | { |
137 | struct yogabook_data *data = _data; |
138 | |
139 | if (gpiod_get_value(desc: data->backside_hall_gpio)) |
140 | set_bit(nr: YB_TABLET_MODE, addr: &data->flags); |
141 | else |
142 | clear_bit(nr: YB_TABLET_MODE, addr: &data->flags); |
143 | |
144 | schedule_work(work: &data->work); |
145 | |
146 | return IRQ_HANDLED; |
147 | } |
148 | |
149 | #define kbd_led_to_yogabook(cdev) container_of(cdev, struct yogabook_data, kbd_bl_led) |
150 | |
151 | static enum led_brightness kbd_brightness_get(struct led_classdev *cdev) |
152 | { |
153 | struct yogabook_data *data = kbd_led_to_yogabook(cdev); |
154 | |
155 | return data->brightness; |
156 | } |
157 | |
158 | static int kbd_brightness_set(struct led_classdev *cdev, |
159 | enum led_brightness value) |
160 | { |
161 | struct yogabook_data *data = kbd_led_to_yogabook(cdev); |
162 | |
163 | if ((value < 0) || (value > YB_KBD_BL_MAX)) |
164 | return -EINVAL; |
165 | |
166 | data->brightness = value; |
167 | |
168 | if (!test_bit(YB_KBD_IS_ON, &data->flags)) |
169 | return 0; |
170 | |
171 | return data->set_kbd_backlight(data, data->brightness); |
172 | } |
173 | |
174 | static struct gpiod_lookup_table yogabook_gpios = { |
175 | .table = { |
176 | GPIO_LOOKUP("INT33FF:02" , 18, "backside_hall_sw" , GPIO_ACTIVE_LOW), |
177 | {} |
178 | }, |
179 | }; |
180 | |
181 | static struct led_lookup_data yogabook_pen_led = { |
182 | .provider = "platform::indicator" , |
183 | .con_id = "pen-icon-led" , |
184 | }; |
185 | |
186 | static int yogabook_probe(struct device *dev, struct yogabook_data *data, |
187 | const char *kbd_bl_led_name) |
188 | { |
189 | int r; |
190 | |
191 | data->dev = dev; |
192 | data->brightness = YB_KBD_BL_DEFAULT; |
193 | set_bit(nr: YB_KBD_IS_ON, addr: &data->flags); |
194 | set_bit(nr: YB_DIGITIZER_IS_ON, addr: &data->flags); |
195 | INIT_WORK(&data->work, yogabook_work); |
196 | |
197 | yogabook_pen_led.dev_id = dev_name(dev); |
198 | led_add_lookup(led_lookup: &yogabook_pen_led); |
199 | data->pen_led = devm_led_get(dev, con_id: "pen-icon-led" ); |
200 | led_remove_lookup(led_lookup: &yogabook_pen_led); |
201 | |
202 | if (IS_ERR(ptr: data->pen_led)) |
203 | return dev_err_probe(dev, err: PTR_ERR(ptr: data->pen_led), fmt: "Getting pen icon LED\n" ); |
204 | |
205 | yogabook_gpios.dev_id = dev_name(dev); |
206 | gpiod_add_lookup_table(table: &yogabook_gpios); |
207 | data->backside_hall_gpio = devm_gpiod_get(dev, con_id: "backside_hall_sw" , flags: GPIOD_IN); |
208 | gpiod_remove_lookup_table(table: &yogabook_gpios); |
209 | |
210 | if (IS_ERR(ptr: data->backside_hall_gpio)) |
211 | return dev_err_probe(dev, err: PTR_ERR(ptr: data->backside_hall_gpio), |
212 | fmt: "Getting backside_hall_sw GPIO\n" ); |
213 | |
214 | r = gpiod_to_irq(desc: data->backside_hall_gpio); |
215 | if (r < 0) |
216 | return dev_err_probe(dev, err: r, fmt: "Getting backside_hall_sw IRQ\n" ); |
217 | |
218 | data->backside_hall_irq = r; |
219 | |
220 | /* Set default brightness before enabling the IRQ */ |
221 | data->set_kbd_backlight(data, YB_KBD_BL_DEFAULT); |
222 | |
223 | r = request_irq(irq: data->backside_hall_irq, handler: yogabook_backside_hall_irq, |
224 | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, |
225 | name: "backside_hall_sw" , dev: data); |
226 | if (r) |
227 | return dev_err_probe(dev, err: r, fmt: "Requesting backside_hall_sw IRQ\n" ); |
228 | |
229 | schedule_work(work: &data->work); |
230 | |
231 | data->kbd_bl_led.name = kbd_bl_led_name; |
232 | data->kbd_bl_led.brightness_set_blocking = kbd_brightness_set; |
233 | data->kbd_bl_led.brightness_get = kbd_brightness_get; |
234 | data->kbd_bl_led.max_brightness = YB_KBD_BL_MAX; |
235 | |
236 | r = devm_led_classdev_register(parent: dev, led_cdev: &data->kbd_bl_led); |
237 | if (r < 0) { |
238 | dev_err_probe(dev, err: r, fmt: "Registering backlight LED device\n" ); |
239 | goto error_free_irq; |
240 | } |
241 | |
242 | dev_set_drvdata(dev, data); |
243 | return 0; |
244 | |
245 | error_free_irq: |
246 | free_irq(data->backside_hall_irq, data); |
247 | cancel_work_sync(work: &data->work); |
248 | return r; |
249 | } |
250 | |
251 | static void yogabook_remove(struct yogabook_data *data) |
252 | { |
253 | int r = 0; |
254 | |
255 | free_irq(data->backside_hall_irq, data); |
256 | cancel_work_sync(work: &data->work); |
257 | |
258 | if (!test_bit(YB_KBD_IS_ON, &data->flags)) |
259 | r |= device_reprobe(dev: data->kbd_dev); |
260 | |
261 | if (!test_bit(YB_DIGITIZER_IS_ON, &data->flags)) |
262 | r |= device_reprobe(dev: data->dig_dev); |
263 | |
264 | if (r) |
265 | dev_warn(data->dev, "Reprobe of devices failed\n" ); |
266 | } |
267 | |
268 | static int yogabook_suspend(struct device *dev) |
269 | { |
270 | struct yogabook_data *data = dev_get_drvdata(dev); |
271 | |
272 | set_bit(nr: YB_SUSPENDED, addr: &data->flags); |
273 | flush_work(work: &data->work); |
274 | |
275 | if (test_bit(YB_KBD_IS_ON, &data->flags)) |
276 | data->set_kbd_backlight(data, 0); |
277 | |
278 | return 0; |
279 | } |
280 | |
281 | static int yogabook_resume(struct device *dev) |
282 | { |
283 | struct yogabook_data *data = dev_get_drvdata(dev); |
284 | |
285 | if (test_bit(YB_KBD_IS_ON, &data->flags)) |
286 | data->set_kbd_backlight(data, data->brightness); |
287 | |
288 | clear_bit(nr: YB_SUSPENDED, addr: &data->flags); |
289 | |
290 | /* Check for YB_TABLET_MODE changes made during suspend */ |
291 | schedule_work(work: &data->work); |
292 | |
293 | return 0; |
294 | } |
295 | |
296 | static DEFINE_SIMPLE_DEV_PM_OPS(yogabook_pm_ops, yogabook_suspend, yogabook_resume); |
297 | |
298 | /********** WMI driver code **********/ |
299 | |
300 | /* |
301 | * To control keyboard backlight, call the method KBLC() of the TCS1 ACPI |
302 | * device (Goodix touchpad acts as virtual sensor keyboard). |
303 | */ |
304 | static int yogabook_wmi_set_kbd_backlight(struct yogabook_data *data, |
305 | uint8_t level) |
306 | { |
307 | struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; |
308 | struct acpi_object_list input; |
309 | union acpi_object param; |
310 | acpi_status status; |
311 | |
312 | dev_dbg(data->dev, "Set KBLC level to %u\n" , level); |
313 | |
314 | /* Ensure keyboard touchpad is on before we call KBLC() */ |
315 | acpi_device_set_power(device: data->kbd_adev, ACPI_STATE_D0); |
316 | |
317 | input.count = 1; |
318 | input.pointer = ¶m; |
319 | |
320 | param.type = ACPI_TYPE_INTEGER; |
321 | param.integer.value = YB_KBD_BL_MAX - level; |
322 | |
323 | status = acpi_evaluate_object(object: acpi_device_handle(adev: data->kbd_adev), pathname: "KBLC" , |
324 | parameter_objects: &input, return_object_buffer: &output); |
325 | if (ACPI_FAILURE(status)) { |
326 | dev_err(data->dev, "Failed to call KBLC method: 0x%x\n" , status); |
327 | return status; |
328 | } |
329 | |
330 | kfree(objp: output.pointer); |
331 | return 0; |
332 | } |
333 | |
334 | static int yogabook_wmi_probe(struct wmi_device *wdev, const void *context) |
335 | { |
336 | struct device *dev = &wdev->dev; |
337 | struct yogabook_data *data; |
338 | int r; |
339 | |
340 | data = devm_kzalloc(dev, size: sizeof(*data), GFP_KERNEL); |
341 | if (data == NULL) |
342 | return -ENOMEM; |
343 | |
344 | data->kbd_adev = acpi_dev_get_first_match_dev(hid: "GDIX1001" , NULL, hrv: -1); |
345 | if (!data->kbd_adev) |
346 | return dev_err_probe(dev, err: -ENODEV, fmt: "Cannot find the touchpad device in ACPI tables\n" ); |
347 | |
348 | data->dig_adev = acpi_dev_get_first_match_dev(hid: "WCOM0019" , NULL, hrv: -1); |
349 | if (!data->dig_adev) { |
350 | r = dev_err_probe(dev, err: -ENODEV, fmt: "Cannot find the digitizer device in ACPI tables\n" ); |
351 | goto error_put_devs; |
352 | } |
353 | |
354 | data->kbd_dev = get_device(dev: acpi_get_first_physical_node(adev: data->kbd_adev)); |
355 | if (!data->kbd_dev || !data->kbd_dev->driver) { |
356 | r = -EPROBE_DEFER; |
357 | goto error_put_devs; |
358 | } |
359 | |
360 | data->dig_dev = get_device(dev: acpi_get_first_physical_node(adev: data->dig_adev)); |
361 | if (!data->dig_dev || !data->dig_dev->driver) { |
362 | r = -EPROBE_DEFER; |
363 | goto error_put_devs; |
364 | } |
365 | |
366 | data->set_kbd_backlight = yogabook_wmi_set_kbd_backlight; |
367 | |
368 | r = yogabook_probe(dev, data, kbd_bl_led_name: "ybwmi::kbd_backlight" ); |
369 | if (r) |
370 | goto error_put_devs; |
371 | |
372 | return 0; |
373 | |
374 | error_put_devs: |
375 | put_device(dev: data->dig_dev); |
376 | put_device(dev: data->kbd_dev); |
377 | acpi_dev_put(adev: data->dig_adev); |
378 | acpi_dev_put(adev: data->kbd_adev); |
379 | return r; |
380 | } |
381 | |
382 | static void yogabook_wmi_remove(struct wmi_device *wdev) |
383 | { |
384 | struct yogabook_data *data = dev_get_drvdata(dev: &wdev->dev); |
385 | |
386 | yogabook_remove(data); |
387 | |
388 | put_device(dev: data->dig_dev); |
389 | put_device(dev: data->kbd_dev); |
390 | acpi_dev_put(adev: data->dig_adev); |
391 | acpi_dev_put(adev: data->kbd_adev); |
392 | } |
393 | |
394 | static void yogabook_wmi_notify(struct wmi_device *wdev, union acpi_object *dummy) |
395 | { |
396 | yogabook_toggle_digitizer_mode(data: dev_get_drvdata(dev: &wdev->dev)); |
397 | } |
398 | |
399 | static const struct wmi_device_id yogabook_wmi_id_table[] = { |
400 | { |
401 | .guid_string = YB_MBTN_EVENT_GUID, |
402 | }, |
403 | { } /* Terminating entry */ |
404 | }; |
405 | MODULE_DEVICE_TABLE(wmi, yogabook_wmi_id_table); |
406 | |
407 | static struct wmi_driver yogabook_wmi_driver = { |
408 | .driver = { |
409 | .name = "yogabook-wmi" , |
410 | .pm = pm_sleep_ptr(&yogabook_pm_ops), |
411 | }, |
412 | .no_notify_data = true, |
413 | .id_table = yogabook_wmi_id_table, |
414 | .probe = yogabook_wmi_probe, |
415 | .remove = yogabook_wmi_remove, |
416 | .notify = yogabook_wmi_notify, |
417 | }; |
418 | |
419 | /********** platform driver code **********/ |
420 | |
421 | static struct gpiod_lookup_table yogabook_pdev_gpios = { |
422 | .dev_id = YB_PDEV_NAME, |
423 | .table = { |
424 | GPIO_LOOKUP("INT33FF:00" , 95, "pen_touch_event" , GPIO_ACTIVE_HIGH), |
425 | GPIO_LOOKUP("INT33FF:03" , 52, "enable_keyboard_led" , GPIO_ACTIVE_HIGH), |
426 | {} |
427 | }, |
428 | }; |
429 | |
430 | static int yogabook_pdev_set_kbd_backlight(struct yogabook_data *data, u8 level) |
431 | { |
432 | struct pwm_state state = { |
433 | .period = YB_KBD_BL_PWM_PERIOD, |
434 | .duty_cycle = YB_KBD_BL_PWM_PERIOD * level / YB_KBD_BL_MAX, |
435 | .enabled = level, |
436 | }; |
437 | |
438 | pwm_apply_might_sleep(pwm: data->kbd_bl_pwm, state: &state); |
439 | gpiod_set_value(desc: data->kbd_bl_led_enable, value: level ? 1 : 0); |
440 | return 0; |
441 | } |
442 | |
443 | static irqreturn_t yogabook_pen_touch_irq(int irq, void *data) |
444 | { |
445 | yogabook_toggle_digitizer_mode(data); |
446 | return IRQ_HANDLED; |
447 | } |
448 | |
449 | static int yogabook_pdev_probe(struct platform_device *pdev) |
450 | { |
451 | struct device *dev = &pdev->dev; |
452 | struct yogabook_data *data; |
453 | int r; |
454 | |
455 | data = devm_kzalloc(dev, size: sizeof(*data), GFP_KERNEL); |
456 | if (data == NULL) |
457 | return -ENOMEM; |
458 | |
459 | data->kbd_dev = bus_find_device_by_name(bus: &i2c_bus_type, NULL, name: "i2c-goodix_ts" ); |
460 | if (!data->kbd_dev || !data->kbd_dev->driver) { |
461 | r = -EPROBE_DEFER; |
462 | goto error_put_devs; |
463 | } |
464 | |
465 | data->dig_dev = bus_find_device_by_name(bus: &i2c_bus_type, NULL, name: "i2c-wacom" ); |
466 | if (!data->dig_dev || !data->dig_dev->driver) { |
467 | r = -EPROBE_DEFER; |
468 | goto error_put_devs; |
469 | } |
470 | |
471 | gpiod_add_lookup_table(table: &yogabook_pdev_gpios); |
472 | data->pen_touch_event = devm_gpiod_get(dev, con_id: "pen_touch_event" , flags: GPIOD_IN); |
473 | data->kbd_bl_led_enable = devm_gpiod_get(dev, con_id: "enable_keyboard_led" , flags: GPIOD_OUT_HIGH); |
474 | gpiod_remove_lookup_table(table: &yogabook_pdev_gpios); |
475 | |
476 | if (IS_ERR(ptr: data->pen_touch_event)) { |
477 | r = dev_err_probe(dev, err: PTR_ERR(ptr: data->pen_touch_event), |
478 | fmt: "Getting pen_touch_event GPIO\n" ); |
479 | goto error_put_devs; |
480 | } |
481 | |
482 | if (IS_ERR(ptr: data->kbd_bl_led_enable)) { |
483 | r = dev_err_probe(dev, err: PTR_ERR(ptr: data->kbd_bl_led_enable), |
484 | fmt: "Getting enable_keyboard_led GPIO\n" ); |
485 | goto error_put_devs; |
486 | } |
487 | |
488 | data->kbd_bl_pwm = devm_pwm_get(dev, con_id: "pwm_soc_lpss_2" ); |
489 | if (IS_ERR(ptr: data->kbd_bl_pwm)) { |
490 | r = dev_err_probe(dev, err: PTR_ERR(ptr: data->kbd_bl_pwm), |
491 | fmt: "Getting keyboard backlight PWM\n" ); |
492 | goto error_put_devs; |
493 | } |
494 | |
495 | r = gpiod_to_irq(desc: data->pen_touch_event); |
496 | if (r < 0) { |
497 | dev_err_probe(dev, err: r, fmt: "Getting pen_touch_event IRQ\n" ); |
498 | goto error_put_devs; |
499 | } |
500 | data->pen_touch_irq = r; |
501 | |
502 | r = request_irq(irq: data->pen_touch_irq, handler: yogabook_pen_touch_irq, IRQF_TRIGGER_FALLING, |
503 | name: "pen_touch_event" , dev: data); |
504 | if (r) { |
505 | dev_err_probe(dev, err: r, fmt: "Requesting pen_touch_event IRQ\n" ); |
506 | goto error_put_devs; |
507 | } |
508 | |
509 | data->set_kbd_backlight = yogabook_pdev_set_kbd_backlight; |
510 | |
511 | r = yogabook_probe(dev, data, kbd_bl_led_name: "yogabook::kbd_backlight" ); |
512 | if (r) |
513 | goto error_free_irq; |
514 | |
515 | return 0; |
516 | |
517 | error_free_irq: |
518 | free_irq(data->pen_touch_irq, data); |
519 | cancel_work_sync(work: &data->work); |
520 | error_put_devs: |
521 | put_device(dev: data->dig_dev); |
522 | put_device(dev: data->kbd_dev); |
523 | return r; |
524 | } |
525 | |
526 | static void yogabook_pdev_remove(struct platform_device *pdev) |
527 | { |
528 | struct yogabook_data *data = platform_get_drvdata(pdev); |
529 | |
530 | yogabook_remove(data); |
531 | free_irq(data->pen_touch_irq, data); |
532 | cancel_work_sync(work: &data->work); |
533 | put_device(dev: data->dig_dev); |
534 | put_device(dev: data->kbd_dev); |
535 | } |
536 | |
537 | static struct platform_driver yogabook_pdev_driver = { |
538 | .probe = yogabook_pdev_probe, |
539 | .remove_new = yogabook_pdev_remove, |
540 | .driver = { |
541 | .name = YB_PDEV_NAME, |
542 | .pm = pm_sleep_ptr(&yogabook_pm_ops), |
543 | }, |
544 | }; |
545 | |
546 | static int __init yogabook_module_init(void) |
547 | { |
548 | int r; |
549 | |
550 | r = wmi_driver_register(&yogabook_wmi_driver); |
551 | if (r) |
552 | return r; |
553 | |
554 | r = platform_driver_register(&yogabook_pdev_driver); |
555 | if (r) |
556 | wmi_driver_unregister(driver: &yogabook_wmi_driver); |
557 | |
558 | return r; |
559 | } |
560 | |
561 | static void __exit yogabook_module_exit(void) |
562 | { |
563 | platform_driver_unregister(&yogabook_pdev_driver); |
564 | wmi_driver_unregister(driver: &yogabook_wmi_driver); |
565 | } |
566 | |
567 | module_init(yogabook_module_init); |
568 | module_exit(yogabook_module_exit); |
569 | |
570 | MODULE_ALIAS("platform:" YB_PDEV_NAME); |
571 | MODULE_AUTHOR("Yauhen Kharuzhy" ); |
572 | MODULE_DESCRIPTION("Lenovo Yoga Book driver" ); |
573 | MODULE_LICENSE("GPL v2" ); |
574 | |