-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
platform/x86: lenovo-yogabook-wmi: Add driver for Lenovo Yoga Book
Add driver to handle WMI events, control the keyboard backlight and bind/unbind the keyboard-touch / digitizer driver so that only one is active at a time. It may seem a bit weird to handle the toggling of the modes in the kernel, but the hw actually expects only 1 device to be active at a time. Changes by Hans de Goede: - Whole bunch of cleanups - Make the kernel do the driver bind/unbind itself instead of sending events to userspace and requiring a special userspace daemon to deal with this Signed-off-by: Yauhen Kharuzhy <jekhor@gmail.com> Co-developed-by: Hans de Goede <hdegoede@redhat.com> Signed-off-by: Hans de Goede <hdegoede@redhat.com> Link: https://lore.kernel.org/r/20211128190031.405620-4-hdegoede@redhat.com
- Loading branch information
Yauhen Kharuzhy
authored and
Hans de Goede
committed
Dec 7, 2021
1 parent
8c33915
commit c0549b7
Showing
3 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,339 @@ | ||
// SPDX-License-Identifier: GPL-2.0 | ||
/* WMI driver for Lenovo Yoga Book YB1-X90* / -X91* tablets */ | ||
|
||
#include <linux/acpi.h> | ||
#include <linux/devm-helpers.h> | ||
#include <linux/module.h> | ||
#include <linux/leds.h> | ||
#include <linux/wmi.h> | ||
#include <linux/workqueue.h> | ||
|
||
#define YB_MBTN_EVENT_GUID "243FEC1D-1963-41C1-8100-06A9D82A94B4" | ||
#define YB_MBTN_METHOD_GUID "742B0CA1-0B20-404B-9CAA-AEFCABF30CE0" | ||
|
||
#define YB_PAD_ENABLE 1 | ||
#define YB_PAD_DISABLE 2 | ||
#define YB_LIGHTUP_BTN 3 | ||
|
||
#define YB_KBD_BL_DEFAULT 128 | ||
|
||
/* flags */ | ||
enum { | ||
YB_KBD_IS_ON, | ||
YB_DIGITIZER_IS_ON, | ||
YB_DIGITIZER_MODE, | ||
YB_SUSPENDED, | ||
}; | ||
|
||
struct yogabook_wmi { | ||
struct wmi_device *wdev; | ||
struct acpi_device *kbd_adev; | ||
struct acpi_device *dig_adev; | ||
struct device *kbd_dev; | ||
struct device *dig_dev; | ||
struct work_struct work; | ||
struct led_classdev kbd_bl_led; | ||
unsigned long flags; | ||
uint8_t brightness; | ||
}; | ||
|
||
static int yogabook_wmi_do_action(struct wmi_device *wdev, int action) | ||
{ | ||
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; | ||
struct acpi_buffer input; | ||
acpi_status status; | ||
u32 dummy_arg = 0; | ||
|
||
dev_dbg(&wdev->dev, "Do action: %d\n", action); | ||
|
||
input.pointer = &dummy_arg; | ||
input.length = sizeof(dummy_arg); | ||
|
||
status = wmi_evaluate_method(YB_MBTN_METHOD_GUID, 0, action, &input, | ||
&output); | ||
if (ACPI_FAILURE(status)) { | ||
dev_err(&wdev->dev, "Calling WMI method failure: 0x%x\n", | ||
status); | ||
return status; | ||
} | ||
|
||
kfree(output.pointer); | ||
|
||
return 0; | ||
} | ||
|
||
/* | ||
* To control keyboard backlight, call the method KBLC() of the TCS1 ACPI | ||
* device (Goodix touchpad acts as virtual sensor keyboard). | ||
*/ | ||
static int yogabook_wmi_set_kbd_backlight(struct wmi_device *wdev, | ||
uint8_t level) | ||
{ | ||
struct yogabook_wmi *data = dev_get_drvdata(&wdev->dev); | ||
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; | ||
struct acpi_object_list input; | ||
union acpi_object param; | ||
acpi_status status; | ||
|
||
if (data->kbd_adev->power.state != ACPI_STATE_D0) { | ||
dev_warn(&wdev->dev, "keyboard touchscreen not in D0, cannot set brightness\n"); | ||
return -ENXIO; | ||
} | ||
|
||
dev_dbg(&wdev->dev, "Set KBLC level to %u\n", level); | ||
|
||
input.count = 1; | ||
input.pointer = ¶m; | ||
|
||
param.type = ACPI_TYPE_INTEGER; | ||
param.integer.value = 255 - level; | ||
|
||
status = acpi_evaluate_object(acpi_device_handle(data->kbd_adev), "KBLC", | ||
&input, &output); | ||
if (ACPI_FAILURE(status)) { | ||
dev_err(&wdev->dev, "Failed to call KBLC method: 0x%x\n", status); | ||
return status; | ||
} | ||
|
||
kfree(output.pointer); | ||
return 0; | ||
} | ||
|
||
static void yogabook_wmi_work(struct work_struct *work) | ||
{ | ||
struct yogabook_wmi *data = container_of(work, struct yogabook_wmi, work); | ||
struct device *dev = &data->wdev->dev; | ||
bool kbd_on, digitizer_on; | ||
int r; | ||
|
||
if (test_bit(YB_SUSPENDED, &data->flags)) | ||
return; | ||
|
||
if (test_bit(YB_DIGITIZER_MODE, &data->flags)) { | ||
digitizer_on = true; | ||
kbd_on = false; | ||
} else { | ||
kbd_on = true; | ||
digitizer_on = false; | ||
} | ||
|
||
if (!kbd_on && test_bit(YB_KBD_IS_ON, &data->flags)) { | ||
/* | ||
* Must be done before releasing the keyboard touchscreen driver, | ||
* so that the keyboard touchscreen dev is still in D0. | ||
*/ | ||
yogabook_wmi_set_kbd_backlight(data->wdev, 0); | ||
device_release_driver(data->kbd_dev); | ||
clear_bit(YB_KBD_IS_ON, &data->flags); | ||
} | ||
|
||
if (!digitizer_on && test_bit(YB_DIGITIZER_IS_ON, &data->flags)) { | ||
yogabook_wmi_do_action(data->wdev, YB_PAD_DISABLE); | ||
device_release_driver(data->dig_dev); | ||
clear_bit(YB_DIGITIZER_IS_ON, &data->flags); | ||
} | ||
|
||
if (kbd_on && !test_bit(YB_KBD_IS_ON, &data->flags)) { | ||
r = device_reprobe(data->kbd_dev); | ||
if (r) | ||
dev_warn(dev, "Reprobe of keyboard touchscreen failed: %d\n", r); | ||
|
||
yogabook_wmi_set_kbd_backlight(data->wdev, data->brightness); | ||
set_bit(YB_KBD_IS_ON, &data->flags); | ||
} | ||
|
||
if (digitizer_on && !test_bit(YB_DIGITIZER_IS_ON, &data->flags)) { | ||
r = device_reprobe(data->dig_dev); | ||
if (r) | ||
dev_warn(dev, "Reprobe of digitizer failed: %d\n", r); | ||
|
||
yogabook_wmi_do_action(data->wdev, YB_PAD_ENABLE); | ||
set_bit(YB_DIGITIZER_IS_ON, &data->flags); | ||
} | ||
} | ||
|
||
static void yogabook_wmi_notify(struct wmi_device *wdev, union acpi_object *dummy) | ||
{ | ||
struct yogabook_wmi *data = dev_get_drvdata(&wdev->dev); | ||
|
||
if (test_bit(YB_SUSPENDED, &data->flags)) | ||
return; | ||
|
||
if (test_bit(YB_DIGITIZER_MODE, &data->flags)) | ||
clear_bit(YB_DIGITIZER_MODE, &data->flags); | ||
else | ||
set_bit(YB_DIGITIZER_MODE, &data->flags); | ||
|
||
/* | ||
* We are called from the ACPI core and the driver [un]binding which is | ||
* done also needs ACPI functions, use a workqueue to avoid deadlocking. | ||
*/ | ||
schedule_work(&data->work); | ||
} | ||
|
||
static enum led_brightness kbd_brightness_get(struct led_classdev *cdev) | ||
{ | ||
struct yogabook_wmi *data = | ||
container_of(cdev, struct yogabook_wmi, kbd_bl_led); | ||
|
||
return data->brightness; | ||
} | ||
|
||
static int kbd_brightness_set(struct led_classdev *cdev, | ||
enum led_brightness value) | ||
{ | ||
struct yogabook_wmi *data = | ||
container_of(cdev, struct yogabook_wmi, kbd_bl_led); | ||
struct wmi_device *wdev = data->wdev; | ||
|
||
if ((value < 0) || (value > 255)) | ||
return -EINVAL; | ||
|
||
data->brightness = value; | ||
|
||
if (data->kbd_adev->power.state != ACPI_STATE_D0) | ||
return 0; | ||
|
||
return yogabook_wmi_set_kbd_backlight(wdev, data->brightness); | ||
} | ||
|
||
static int yogabook_wmi_probe(struct wmi_device *wdev, const void *context) | ||
{ | ||
struct yogabook_wmi *data; | ||
int r; | ||
|
||
data = devm_kzalloc(&wdev->dev, sizeof(struct yogabook_wmi), GFP_KERNEL); | ||
if (data == NULL) | ||
return -ENOMEM; | ||
|
||
dev_set_drvdata(&wdev->dev, data); | ||
|
||
data->wdev = wdev; | ||
data->brightness = YB_KBD_BL_DEFAULT; | ||
set_bit(YB_KBD_IS_ON, &data->flags); | ||
set_bit(YB_DIGITIZER_IS_ON, &data->flags); | ||
|
||
r = devm_work_autocancel(&wdev->dev, &data->work, yogabook_wmi_work); | ||
if (r) | ||
return r; | ||
|
||
data->kbd_adev = acpi_dev_get_first_match_dev("GDIX1001", NULL, -1); | ||
if (!data->kbd_adev) { | ||
dev_err(&wdev->dev, "Cannot find the touchpad device in ACPI tables\n"); | ||
return -ENODEV; | ||
} | ||
|
||
data->dig_adev = acpi_dev_get_first_match_dev("WCOM0019", NULL, -1); | ||
if (!data->dig_adev) { | ||
dev_err(&wdev->dev, "Cannot find the digitizer device in ACPI tables\n"); | ||
r = -ENODEV; | ||
goto error_put_devs; | ||
} | ||
|
||
data->kbd_dev = get_device(acpi_get_first_physical_node(data->kbd_adev)); | ||
if (!data->kbd_dev || !data->kbd_dev->driver) { | ||
r = -EPROBE_DEFER; | ||
goto error_put_devs; | ||
} | ||
|
||
data->dig_dev = get_device(acpi_get_first_physical_node(data->dig_adev)); | ||
if (!data->dig_dev || !data->dig_dev->driver) { | ||
r = -EPROBE_DEFER; | ||
goto error_put_devs; | ||
} | ||
|
||
schedule_work(&data->work); | ||
|
||
data->kbd_bl_led.name = "ybwmi::kbd_backlight"; | ||
data->kbd_bl_led.brightness_set_blocking = kbd_brightness_set; | ||
data->kbd_bl_led.brightness_get = kbd_brightness_get; | ||
data->kbd_bl_led.max_brightness = 255; | ||
|
||
r = devm_led_classdev_register(&wdev->dev, &data->kbd_bl_led); | ||
if (r < 0) { | ||
dev_err_probe(&wdev->dev, r, "Registering backlight LED device\n"); | ||
goto error_put_devs; | ||
} | ||
|
||
return 0; | ||
|
||
error_put_devs: | ||
put_device(data->dig_dev); | ||
put_device(data->kbd_dev); | ||
acpi_dev_put(data->dig_adev); | ||
acpi_dev_put(data->kbd_adev); | ||
return r; | ||
} | ||
|
||
static void yogabook_wmi_remove(struct wmi_device *wdev) | ||
{ | ||
struct yogabook_wmi *data = dev_get_drvdata(&wdev->dev); | ||
|
||
put_device(data->dig_dev); | ||
put_device(data->kbd_dev); | ||
acpi_dev_put(data->dig_adev); | ||
acpi_dev_put(data->kbd_adev); | ||
} | ||
|
||
static int __maybe_unused yogabook_wmi_suspend(struct device *dev) | ||
{ | ||
struct wmi_device *wdev = container_of(dev, struct wmi_device, dev); | ||
struct yogabook_wmi *data = dev_get_drvdata(dev); | ||
|
||
set_bit(YB_SUSPENDED, &data->flags); | ||
|
||
flush_work(&data->work); | ||
|
||
/* Turn off the pen button at sleep */ | ||
if (test_bit(YB_DIGITIZER_IS_ON, &data->flags)) | ||
yogabook_wmi_do_action(wdev, YB_PAD_DISABLE); | ||
|
||
return 0; | ||
} | ||
|
||
static int __maybe_unused yogabook_wmi_resume(struct device *dev) | ||
{ | ||
struct wmi_device *wdev = container_of(dev, struct wmi_device, dev); | ||
struct yogabook_wmi *data = dev_get_drvdata(dev); | ||
|
||
if (test_bit(YB_KBD_IS_ON, &data->flags)) { | ||
/* Ensure keyboard touchpad is on before we call KBLC() */ | ||
acpi_device_set_power(data->kbd_adev, ACPI_STATE_D0); | ||
yogabook_wmi_set_kbd_backlight(wdev, data->brightness); | ||
} | ||
|
||
if (test_bit(YB_DIGITIZER_IS_ON, &data->flags)) | ||
yogabook_wmi_do_action(wdev, YB_PAD_ENABLE); | ||
|
||
clear_bit(YB_SUSPENDED, &data->flags); | ||
|
||
return 0; | ||
} | ||
|
||
static const struct wmi_device_id yogabook_wmi_id_table[] = { | ||
{ | ||
.guid_string = YB_MBTN_EVENT_GUID, | ||
}, | ||
{ } /* Terminating entry */ | ||
}; | ||
|
||
static SIMPLE_DEV_PM_OPS(yogabook_wmi_pm_ops, | ||
yogabook_wmi_suspend, yogabook_wmi_resume); | ||
|
||
static struct wmi_driver yogabook_wmi_driver = { | ||
.driver = { | ||
.name = "yogabook-wmi", | ||
.pm = &yogabook_wmi_pm_ops, | ||
}, | ||
.no_notify_data = true, | ||
.id_table = yogabook_wmi_id_table, | ||
.probe = yogabook_wmi_probe, | ||
.remove = yogabook_wmi_remove, | ||
.notify = yogabook_wmi_notify, | ||
}; | ||
module_wmi_driver(yogabook_wmi_driver); | ||
|
||
MODULE_DEVICE_TABLE(wmi, yogabook_wmi_id_table); | ||
MODULE_AUTHOR("Yauhen Kharuzhy"); | ||
MODULE_DESCRIPTION("Lenovo Yoga Book WMI driver"); | ||
MODULE_LICENSE("GPL v2"); |