This update brings on the use of WMI BIOS management interface found on Huawei laptops. This interface can control the micmute LED found on most of these laptops, control charging thresholds values, and control fn-lock feature.
Signed-off-by: Ayman Bagabas <ayman.baga...@gmail.com> --- drivers/platform/x86/Kconfig | 8 +- drivers/platform/x86/huawei-wmi.c | 578 +++++++++++++++++++++++++----- 2 files changed, 500 insertions(+), 86 deletions(-) diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index a1ed13183559..e46261b6def5 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -1287,7 +1287,7 @@ config INTEL_ATOMISP2_PM will be called intel_atomisp2_pm. config HUAWEI_WMI - tristate "Huawei WMI hotkeys driver" + tristate "Huawei WMI laptop extras driver" depends on ACPI_WMI depends on INPUT select INPUT_SPARSEKMAP @@ -1296,9 +1296,9 @@ config HUAWEI_WMI select LEDS_TRIGGER_AUDIO select NEW_LEDS help - This driver provides support for Huawei WMI hotkeys. - It enables the missing keys and adds support to the micmute - LED found on some of these laptops. + This driver provides support for some extra features found on Huawei + laptops that are controlled through WMI. These features are keyboard + hotkeys, micmute LED, charging thresholds, and fn-lock state. To compile this driver as a module, choose M here: the module will be called huawei-wmi. diff --git a/drivers/platform/x86/huawei-wmi.c b/drivers/platform/x86/huawei-wmi.c index 52fcac5b393a..4ec04196f386 100644 --- a/drivers/platform/x86/huawei-wmi.c +++ b/drivers/platform/x86/huawei-wmi.c @@ -1,32 +1,63 @@ // SPDX-License-Identifier: GPL-2.0 /* - * Huawei WMI hotkeys + * Huawei WMI laptop extras driver * * Copyright (C) 2018 Ayman Bagabas <ayman.baga...@gmail.com> */ #include <linux/acpi.h> +#include <linux/delay.h> +#include <linux/dmi.h> #include <linux/input.h> #include <linux/input/sparse-keymap.h> #include <linux/leds.h> #include <linux/module.h> +#include <linux/mutex.h> +#include <linux/platform_device.h> +#include <linux/sysfs.h> #include <linux/wmi.h> /* * Huawei WMI GUIDs */ -#define WMI0_EVENT_GUID "59142400-C6A3-40fa-BADB-8A2652834100" +#define AMW0_METHOD_GUID "ABBC0F5B-8EA1-11D1-A000-C90629100000" #define AMW0_EVENT_GUID "ABBC0F5C-8EA1-11D1-A000-C90629100000" +/* Legacy GUIDs */ #define WMI0_EXPENSIVE_GUID "39142400-C6A3-40fa-BADB-8A2652834100" +#define WMI0_EVENT_GUID "59142400-C6A3-40fa-BADB-8A2652834100" -struct huawei_wmi_priv { - struct input_dev *idev; +/* AMW0_commands */ + +enum wmaa_cmd { + BATTERY_GET, /* \GBTT 0x00001103 */ + BATTERY_SET, /* \SBTT 0xXXYY1003 */ + FN_LOCK_GET, /* \GFRS 0x00000604 */ + FN_LOCK_SET, /* \SFRS 0x000X0704 */ + MICMUTE_LED, /* \SMLS 0x000X0b04 */ +}; + +enum fn_state { + FN_LOCK_OFF = 0x01, + FN_LOCK_ON = 0x02, +}; + +struct quirk_entry { + bool battery_reset; + bool ec_micmute; +}; + +static struct quirk_entry *quirks; + +struct huawei_wmi { struct led_classdev cdev; - acpi_handle handle; - char *acpi_method; + struct mutex wmi_lock; + struct mutex battery_lock; + struct platform_device *pdev; }; +struct platform_device *huawei_wmi_pdev; + static const struct key_entry huawei_wmi_keymap[] = { { KE_KEY, 0x281, { KEY_BRIGHTNESSDOWN } }, { KE_KEY, 0x282, { KEY_BRIGHTNESSUP } }, @@ -37,73 +68,169 @@ static const struct key_entry huawei_wmi_keymap[] = { { KE_KEY, 0x289, { KEY_WLAN } }, // Huawei |M| key { KE_KEY, 0x28a, { KEY_CONFIG } }, - // Keyboard backlight + // Keyboard backlit { KE_IGNORE, 0x293, { KEY_KBDILLUMTOGGLE } }, { KE_IGNORE, 0x294, { KEY_KBDILLUMUP } }, { KE_IGNORE, 0x295, { KEY_KBDILLUMUP } }, { KE_END, 0 } }; -static int huawei_wmi_micmute_led_set(struct led_classdev *led_cdev, - enum led_brightness brightness) +/* Quirks */ + +static int __init dmi_matched(const struct dmi_system_id *dmi) { - struct huawei_wmi_priv *priv = dev_get_drvdata(led_cdev->dev->parent); + quirks = dmi->driver_data; + return 1; +} + +static struct quirk_entry quirk_battery_reset = { + .battery_reset = true, +}; + +static struct quirk_entry quirk_ec_micmute = { + .ec_micmute = true, +}; + +static const struct dmi_system_id huawei_quirks[] = { + { + .callback = dmi_matched, + .ident = "Huawei MACH-WX9", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), + DMI_MATCH(DMI_PRODUCT_NAME, "MACH-WX9"), + }, + .driver_data = &quirk_battery_reset + }, + { + .callback = dmi_matched, + .ident = "Huawei MateBook X", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), + DMI_MATCH(DMI_PRODUCT_NAME, "HUAWEI MateBook X") + }, + .driver_data = &quirk_ec_micmute + } +}; + +/* Utils */ + +static int huawei_wmi_eval(struct device *dev, char *arg, + char *buf, size_t buflen) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer in; + union acpi_object *obj; acpi_status status; - union acpi_object args[3]; - struct acpi_object_list arg_list = { - .pointer = args, - .count = ARRAY_SIZE(args), - }; - - args[0].type = args[1].type = args[2].type = ACPI_TYPE_INTEGER; - args[1].integer.value = 0x04; - - if (strcmp(priv->acpi_method, "SPIN") == 0) { - args[0].integer.value = 0; - args[2].integer.value = brightness ? 1 : 0; - } else if (strcmp(priv->acpi_method, "WPIN") == 0) { - args[0].integer.value = 1; - args[2].integer.value = brightness ? 0 : 1; - } else { - return -EINVAL; + size_t len; + int err = -ENODEV; + + in.length = sizeof(char) * 4; + in.pointer = (u32 *)arg; + mutex_lock(&huawei->wmi_lock); + status = wmi_evaluate_method(AMW0_METHOD_GUID, 0, 1, &in, &out); + if (ACPI_FAILURE(status)) { + dev_err(dev, "Failed to evaluate wmi method\n"); + goto wmi_eval_fail; } - status = acpi_evaluate_object(priv->handle, priv->acpi_method, &arg_list, NULL); - if (ACPI_FAILURE(status)) - return -ENXIO; + /* WMAA takes a 4 bytes buffer as an input. It returns a package + * with two buffer elements. The first buffer is 4 bytes long and + * the second is 0x100 (256) bytes long. The first buffer is always + * zeros. The second stores the output from every call. The first + * byte of the second buffer always have the return status of the + * called command. + */ + obj = out.pointer; + if (!obj) + goto wmi_eval_fail; + if (obj->type != ACPI_TYPE_PACKAGE || + obj->package.count != 2) { + dev_err(dev, "Unknown response type %d\n", obj->type); + goto wmi_eval_fail; + } - return 0; + obj = &(obj->package.elements[1]); + if (!obj || obj->type != ACPI_TYPE_BUFFER) + goto wmi_eval_fail; + + if (buf) { + len = min(buflen, obj->buffer.length); + memcpy(buf, obj->buffer.pointer, len); + } + err = 0; + +wmi_eval_fail: + mutex_unlock(&huawei->wmi_lock); + kfree(out.pointer); + return err; } -static int huawei_wmi_leds_setup(struct wmi_device *wdev) +static int huawei_wmi_cmd(struct device *dev, enum wmaa_cmd cmd, char *arg, + char *out, size_t outlen) { - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); - - priv->handle = ec_get_handle(); - if (!priv->handle) - return 0; + char parm[4] = { 0 }; + char buf[0x100] = { 0xff }; + int err; - if (acpi_has_method(priv->handle, "SPIN")) - priv->acpi_method = "SPIN"; - else if (acpi_has_method(priv->handle, "WPIN")) - priv->acpi_method = "WPIN"; - else - return 0; + if (!arg) + arg = parm; + + switch (cmd) { + case BATTERY_SET: + arg[0] = 0x03; + arg[1] = 0x10; + break; + case BATTERY_GET: + arg[0] = 0x03; + arg[1] = 0x11; + break; + case FN_LOCK_GET: + arg[0] = 0x04; + arg[1] = 0x06; + break; + case FN_LOCK_SET: + arg[0] = 0x04; + arg[1] = 0x07; + break; + case MICMUTE_LED: + arg[0] = 0x04; + arg[1] = 0x0b; + break; + default: + dev_err(dev, "Command not supported\n"); + return -EINVAL; + } - priv->cdev.name = "platform::micmute"; - priv->cdev.max_brightness = 1; - priv->cdev.brightness_set_blocking = huawei_wmi_micmute_led_set; - priv->cdev.default_trigger = "audio-micmute"; - priv->cdev.brightness = ledtrig_audio_get(LED_AUDIO_MICMUTE); - priv->cdev.dev = &wdev->dev; - priv->cdev.flags = LED_CORE_SUSPENDRESUME; + /* Some models require calling WMAA twice to execute + * a command. We call WMAA and if we get a non-zero return + * status we evaluate WMAA again. If we get another non-zero + * return, we return -ENXIO. This way we don't need to + * check for return status anywhere we call huawei_wmi_cmd. + */ + err = huawei_wmi_eval(dev, arg, buf, 0x100); + if (err) + return err; + if (buf[0]) { + err = huawei_wmi_eval(dev, arg, buf, 0x100); + if (err) + return err; + if (buf[0]) { + dev_err(dev, "Invalid command, got: %d\n", buf[0]); + return -ENXIO; + } + } + if (out) + memcpy(out, buf, outlen); - return devm_led_classdev_register(&wdev->dev, &priv->cdev); + return 0; } +/* Input */ + static void huawei_wmi_process_key(struct wmi_device *wdev, int code) { - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + struct input_dev *idev = dev_get_drvdata(&wdev->dev); const struct key_entry *key; /* @@ -127,16 +254,16 @@ static void huawei_wmi_process_key(struct wmi_device *wdev, int code) kfree(response.pointer); } - key = sparse_keymap_entry_from_scancode(priv->idev, code); + key = sparse_keymap_entry_from_scancode(idev, code); if (!key) { dev_info(&wdev->dev, "Unknown key pressed, code: 0x%04x\n", code); return; } - sparse_keymap_report_entry(priv->idev, key, 1, true); + sparse_keymap_report_entry(idev, key, 1, true); } -static void huawei_wmi_notify(struct wmi_device *wdev, +static void huawei_wmi_input_notify(struct wmi_device *wdev, union acpi_object *obj) { if (obj->type == ACPI_TYPE_INTEGER) @@ -147,61 +274,348 @@ static void huawei_wmi_notify(struct wmi_device *wdev, static int huawei_wmi_input_setup(struct wmi_device *wdev) { - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); + struct input_dev *idev; int err; - priv->idev = devm_input_allocate_device(&wdev->dev); - if (!priv->idev) + idev = devm_input_allocate_device(&wdev->dev); + if (!idev) return -ENOMEM; - priv->idev->name = "Huawei WMI hotkeys"; - priv->idev->phys = "wmi/input0"; - priv->idev->id.bustype = BUS_HOST; - priv->idev->dev.parent = &wdev->dev; + dev_set_drvdata(&wdev->dev, idev); - err = sparse_keymap_setup(priv->idev, huawei_wmi_keymap, NULL); + idev->name = "Huawei WMI hotkeys"; + idev->phys = "wmi/input0"; + idev->id.bustype = BUS_HOST; + idev->dev.parent = &wdev->dev; + + err = sparse_keymap_setup(idev, huawei_wmi_keymap, NULL); if (err) return err; - return input_register_device(priv->idev); + return input_register_device(idev); } -static int huawei_wmi_probe(struct wmi_device *wdev) +static int huawei_wmi_input_destroy(struct wmi_device *wdev) +{ + struct input_dev *idev = dev_get_drvdata(&wdev->dev); + + input_unregister_device(idev); + return 0; +} + +static const struct wmi_device_id huawei_wmi_input_id_table[] = { + { .guid_string = WMI0_EVENT_GUID }, + { .guid_string = AMW0_EVENT_GUID }, + { } +}; + +static struct wmi_driver huawei_wmi_input_driver = { + .driver = { + .name = "huawei-wmi", + }, + .id_table = huawei_wmi_input_id_table, + .probe = huawei_wmi_input_setup, + .remove = huawei_wmi_input_destroy, + .notify = huawei_wmi_input_notify, +}; + +/* LEDs */ + +static void huawei_wmi_micmute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) { - struct huawei_wmi_priv *priv; + if (quirks && quirks->ec_micmute) { + char *acpi_method; + acpi_handle handle; + union acpi_object args[3]; + struct acpi_object_list arg_list = { + .pointer = args, + .count = ARRAY_SIZE(args), + }; + + handle = ec_get_handle(); + if (!handle) { + dev_err(led_cdev->dev->parent, "Failed to get EC handle\n"); + return; + } + + args[0].type = args[1].type = args[2].type = ACPI_TYPE_INTEGER; + args[1].integer.value = 0x04; + + if (acpi_has_method(handle, "SPIN")) { + acpi_method = "SPIN"; + args[0].integer.value = 0; + args[2].integer.value = brightness ? 1 : 0; + } else if (acpi_has_method(handle, "WPIN")) { + acpi_method = "WPIN"; + args[0].integer.value = 1; + args[2].integer.value = brightness ? 0 : 1; + } else { + return; + } + + acpi_evaluate_object(handle, acpi_method, &arg_list, NULL); + } else { + char arg[] = { 0, 0, brightness, 0 }; + + huawei_wmi_cmd(led_cdev->dev->parent, MICMUTE_LED, arg, NULL, NULL); + } +} + +static int huawei_wmi_leds_setup(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + + huawei->cdev.name = "platform::micmute"; + huawei->cdev.max_brightness = 1; + huawei->cdev.brightness_set = huawei_wmi_micmute_led_set; + huawei->cdev.default_trigger = "audio-micmute"; + huawei->cdev.brightness = ledtrig_audio_get(LED_AUDIO_MICMUTE); + huawei->cdev.dev = dev->parent; + huawei->cdev.flags = LED_CORE_SUSPENDRESUME; + + return devm_led_classdev_register(dev, &huawei->cdev); +} + +/* Battery protection */ + +static int huawei_wmi_battery_get(struct device *dev, int *low, int *high) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + char ret[0x100] = { 0 }; + int err, i; + + mutex_lock(&huawei->battery_lock); + err = huawei_wmi_cmd(dev, BATTERY_GET, NULL, ret, 0x100); + mutex_unlock(&huawei->battery_lock); + if (err) + return -EINVAL; + + /* Returned buffer positions battery thresholds either in index + * 3 and 2 or in 2 and 1. 0 reserved for return status. + */ + for (i = 0x100 - 1; i > 0; i--) { + if (ret[i]) { + *high = ret[i]; + *low = ret[i-1]; + break; + } + } + + return 0; +} + +static int huawei_wmi_battery_set(struct device *dev, int low, int high) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + char arg[] = { 0, 0, low, high }; int err; - priv = devm_kzalloc(&wdev->dev, sizeof(struct huawei_wmi_priv), GFP_KERNEL); - if (!priv) + /* This is an edge case were some models turn battery protection + * off without changing their thresholds values. We clear the + * values before turning off protection. We need wait blocking to + * make sure these values make its way to EC. + */ + if (low == 0 && high == 100) + huawei_wmi_battery_set(dev, 0, 0); + + mutex_lock(&huawei->battery_lock); + err = huawei_wmi_cmd(dev, BATTERY_SET, arg, NULL, NULL); + if (quirks && quirks->battery_reset) + msleep(jiffies_to_msecs(0.5 * HZ)); + mutex_unlock(&huawei->battery_lock); + if (err) + return err; + + return 0; +} + +/* Fn lock */ + +static int huawei_wmi_fn_lock_get(struct device *dev, int *on) +{ + char ret[0x100] = { 0 }; + int err, i; + + err = huawei_wmi_cmd(dev, FN_LOCK_GET, NULL, ret, 0x100); + if (err) + return -EINVAL; + + for (i = 0x100 - 1; i > 0; i--) { + if (ret[i]) { + *on = (ret[i] == FN_LOCK_OFF) ? 0 : 1; + break; + } + } + + return 0; +} + +static int huawei_wmi_fn_lock_set(struct device *dev, int on) +{ + char arg[] = { 0, 0, (on) ? FN_LOCK_ON : FN_LOCK_OFF, 0 }; + + return huawei_wmi_cmd(dev, FN_LOCK_SET, arg, NULL, NULL); +} + +/* sysfs */ + +static ssize_t charge_thresholds_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int low, high; + + if (sscanf(buf, "%d %d", &low, &high) != 2 || + low < 0 || high > 100 || + low > high || + huawei_wmi_battery_set(dev, low, high)) + return -EINVAL; + + return size; +} + +static ssize_t fn_lock_state_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int on; + + if (kstrtoint(buf, 10, &on) || + on < 0 || on > 1 || + huawei_wmi_fn_lock_set(dev, on)) + return -EINVAL; + + return size; +} + +static ssize_t charge_thresholds_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, low, high; + + low = high = 0; + err = huawei_wmi_battery_get(dev, &low, &high); + if (err) + return -EINVAL; + + return sprintf(buf, "%d %d\n", low, high); +} + +static ssize_t fn_lock_state_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, on; + + on = 0; + err = huawei_wmi_fn_lock_get(dev, &on); + if (err) + return -EINVAL; + + return sprintf(buf, "%d\n", on); +} + +static DEVICE_ATTR_RW(charge_thresholds); +static DEVICE_ATTR_RW(fn_lock_state); + +static struct attribute *huawei_wmi_attrs[] = { + &dev_attr_charge_thresholds.attr, + &dev_attr_fn_lock_state.attr, + NULL +}; + +static const struct attribute_group huawei_wmi_group = { + .attrs = huawei_wmi_attrs +}; + +static int huawei_wmi_probe(struct platform_device *pdev) +{ + struct huawei_wmi *huawei; + int err; + + huawei = devm_kzalloc(&pdev->dev, sizeof(struct huawei_wmi), GFP_KERNEL); + if (!huawei) return -ENOMEM; - dev_set_drvdata(&wdev->dev, priv); + huawei->pdev = pdev; + dev_set_drvdata(&pdev->dev, huawei); + mutex_init(&huawei->wmi_lock); + mutex_init(&huawei->battery_lock); - err = huawei_wmi_input_setup(wdev); + err = sysfs_create_group(&pdev->dev.kobj, &huawei_wmi_group); if (err) return err; - return huawei_wmi_leds_setup(wdev); + return huawei_wmi_leds_setup(&pdev->dev); } -static const struct wmi_device_id huawei_wmi_id_table[] = { - { .guid_string = WMI0_EVENT_GUID }, - { .guid_string = AMW0_EVENT_GUID }, - { } -}; +static int huawei_wmi_remove(struct platform_device *pdev) +{ + sysfs_remove_group(&pdev->dev.kobj, &huawei_wmi_group); + return 0; +} + +/* Huawei driver */ -static struct wmi_driver huawei_wmi_driver = { +static struct platform_driver huawei_wmi_driver = { .driver = { .name = "huawei-wmi", }, - .id_table = huawei_wmi_id_table, .probe = huawei_wmi_probe, - .notify = huawei_wmi_notify, + .remove = huawei_wmi_remove, }; -module_wmi_driver(huawei_wmi_driver); +static __init int huawei_wmi_init(void) +{ + int event_capable = wmi_has_guid(WMI0_EVENT_GUID) || + wmi_has_guid(AMW0_EVENT_GUID); + int bios_capable = wmi_has_guid(AMW0_METHOD_GUID); + int err; + + if (!event_capable && !bios_capable) + return -ENODEV; + + dmi_check_system(huawei_quirks); + + if (event_capable && wmi_driver_register(&huawei_wmi_input_driver)) + pr_err("Failed to register wmi input driver\n"); + + if (bios_capable) { + huawei_wmi_pdev = + platform_device_register_simple("huawei-wmi", -1, NULL, 0); + if (IS_ERR(huawei_wmi_pdev)) { + pr_err("Failed to register platform device\n"); + return PTR_ERR(huawei_wmi_pdev); + } + + err = platform_driver_register(&huawei_wmi_driver); + if (err) { + pr_err("Failed to register platform driver\n"); + platform_device_unregister(huawei_wmi_pdev); + return err; + } + } + + return 0; +} + +static __exit void huawei_wmi_exit(void) +{ + wmi_driver_unregister(&huawei_wmi_input_driver); + if (wmi_has_guid(AMW0_METHOD_GUID)) { + platform_device_unregister(huawei_wmi_pdev); + platform_driver_unregister(&huawei_wmi_driver); + } +} + +module_init(huawei_wmi_init); +module_exit(huawei_wmi_exit); -MODULE_DEVICE_TABLE(wmi, huawei_wmi_id_table); +MODULE_ALIAS("wmi:"AMW0_METHOD_GUID); +MODULE_ALIAS("wmi:"AMW0_EVENT_GUID); +MODULE_ALIAS("wmi:"WMI0_EVENT_GUID); MODULE_AUTHOR("Ayman Bagabas <ayman.baga...@gmail.com>"); -MODULE_DESCRIPTION("Huawei WMI hotkeys"); +MODULE_DESCRIPTION("Huawei WMI laptop driver"); MODULE_LICENSE("GPL v2"); -- 2.20.1