|  | // SPDX-License-Identifier: GPL-2.0 | 
|  | /* | 
|  | * Topstar Laptop ACPI Extras driver | 
|  | * | 
|  | * Copyright (c) 2009 Herton Ronaldo Krzesinski <[email protected]> | 
|  | * Copyright (c) 2018 Guillaume Douézan-Grard | 
|  | * | 
|  | * Implementation inspired by existing x86 platform drivers, in special | 
|  | * asus/eepc/fujitsu-laptop, thanks to their authors. | 
|  | */ | 
|  |  | 
|  | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | 
|  |  | 
|  | #include <linux/kernel.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/init.h> | 
|  | #include <linux/slab.h> | 
|  | #include <linux/acpi.h> | 
|  | #include <linux/dmi.h> | 
|  | #include <linux/input.h> | 
|  | #include <linux/input/sparse-keymap.h> | 
|  | #include <linux/leds.h> | 
|  | #include <linux/platform_device.h> | 
|  |  | 
|  | #define TOPSTAR_LAPTOP_CLASS "topstar" | 
|  |  | 
|  | struct topstar_laptop { | 
|  | struct acpi_device *device; | 
|  | struct platform_device *platform; | 
|  | struct input_dev *input; | 
|  | struct led_classdev led; | 
|  | }; | 
|  |  | 
|  | /* | 
|  | * LED | 
|  | */ | 
|  |  | 
|  | static enum led_brightness topstar_led_get(struct led_classdev *led) | 
|  | { | 
|  | return led->brightness; | 
|  | } | 
|  |  | 
|  | static int topstar_led_set(struct led_classdev *led, | 
|  | enum led_brightness state) | 
|  | { | 
|  | struct topstar_laptop *topstar = container_of(led, | 
|  | struct topstar_laptop, led); | 
|  |  | 
|  | struct acpi_object_list params; | 
|  | union acpi_object in_obj; | 
|  | unsigned long long int ret; | 
|  | acpi_status status; | 
|  |  | 
|  | params.count = 1; | 
|  | params.pointer = &in_obj; | 
|  | in_obj.type = ACPI_TYPE_INTEGER; | 
|  | in_obj.integer.value = 0x83; | 
|  |  | 
|  | /* | 
|  | * Topstar ACPI returns 0x30001 when the LED is ON and 0x30000 when it | 
|  | * is OFF. | 
|  | */ | 
|  | status = acpi_evaluate_integer(topstar->device->handle, | 
|  | "GETX", ¶ms, &ret); | 
|  | if (ACPI_FAILURE(status)) | 
|  | return -1; | 
|  |  | 
|  | /* | 
|  | * FNCX(0x83) toggles the LED (more precisely, it is supposed to | 
|  | * act as an hardware switch and disconnect the WLAN adapter but | 
|  | * it seems to be faulty on some models like the Topstar U931 | 
|  | * Notebook). | 
|  | */ | 
|  | if ((ret == 0x30001 && state == LED_OFF) | 
|  | || (ret == 0x30000 && state != LED_OFF)) { | 
|  | status = acpi_execute_simple_method(topstar->device->handle, | 
|  | "FNCX", 0x83); | 
|  | if (ACPI_FAILURE(status)) | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int topstar_led_init(struct topstar_laptop *topstar) | 
|  | { | 
|  | topstar->led = (struct led_classdev) { | 
|  | .default_trigger = "rfkill0", | 
|  | .brightness_get = topstar_led_get, | 
|  | .brightness_set_blocking = topstar_led_set, | 
|  | .name = TOPSTAR_LAPTOP_CLASS "::wlan", | 
|  | }; | 
|  |  | 
|  | return led_classdev_register(&topstar->platform->dev, &topstar->led); | 
|  | } | 
|  |  | 
|  | static void topstar_led_exit(struct topstar_laptop *topstar) | 
|  | { | 
|  | led_classdev_unregister(&topstar->led); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Input | 
|  | */ | 
|  |  | 
|  | static const struct key_entry topstar_keymap[] = { | 
|  | { KE_KEY, 0x80, { KEY_BRIGHTNESSUP } }, | 
|  | { KE_KEY, 0x81, { KEY_BRIGHTNESSDOWN } }, | 
|  | { KE_KEY, 0x83, { KEY_VOLUMEUP } }, | 
|  | { KE_KEY, 0x84, { KEY_VOLUMEDOWN } }, | 
|  | { KE_KEY, 0x85, { KEY_MUTE } }, | 
|  | { KE_KEY, 0x86, { KEY_SWITCHVIDEOMODE } }, | 
|  | { KE_KEY, 0x87, { KEY_F13 } }, /* touchpad enable/disable key */ | 
|  | { KE_KEY, 0x88, { KEY_WLAN } }, | 
|  | { KE_KEY, 0x8a, { KEY_WWW } }, | 
|  | { KE_KEY, 0x8b, { KEY_MAIL } }, | 
|  | { KE_KEY, 0x8c, { KEY_MEDIA } }, | 
|  |  | 
|  | /* Known non hotkey events don't handled or that we don't care yet */ | 
|  | { KE_IGNORE, 0x82, }, /* backlight event */ | 
|  | { KE_IGNORE, 0x8e, }, | 
|  | { KE_IGNORE, 0x8f, }, | 
|  | { KE_IGNORE, 0x90, }, | 
|  |  | 
|  | /* | 
|  | * 'G key' generate two event codes, convert to only | 
|  | * one event/key code for now, consider replacing by | 
|  | * a switch (3G switch - SW_3G?) | 
|  | */ | 
|  | { KE_KEY, 0x96, { KEY_F14 } }, | 
|  | { KE_KEY, 0x97, { KEY_F14 } }, | 
|  |  | 
|  | { KE_END, 0 } | 
|  | }; | 
|  |  | 
|  | static void topstar_input_notify(struct topstar_laptop *topstar, int event) | 
|  | { | 
|  | if (!sparse_keymap_report_event(topstar->input, event, 1, true)) | 
|  | pr_info("unknown event = 0x%02x\n", event); | 
|  | } | 
|  |  | 
|  | static int topstar_input_init(struct topstar_laptop *topstar) | 
|  | { | 
|  | struct input_dev *input; | 
|  | int err; | 
|  |  | 
|  | input = input_allocate_device(); | 
|  | if (!input) | 
|  | return -ENOMEM; | 
|  |  | 
|  | input->name = "Topstar Laptop extra buttons"; | 
|  | input->phys = TOPSTAR_LAPTOP_CLASS "/input0"; | 
|  | input->id.bustype = BUS_HOST; | 
|  | input->dev.parent = &topstar->platform->dev; | 
|  |  | 
|  | err = sparse_keymap_setup(input, topstar_keymap, NULL); | 
|  | if (err) { | 
|  | pr_err("Unable to setup input device keymap\n"); | 
|  | goto err_free_dev; | 
|  | } | 
|  |  | 
|  | err = input_register_device(input); | 
|  | if (err) { | 
|  | pr_err("Unable to register input device\n"); | 
|  | goto err_free_dev; | 
|  | } | 
|  |  | 
|  | topstar->input = input; | 
|  | return 0; | 
|  |  | 
|  | err_free_dev: | 
|  | input_free_device(input); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static void topstar_input_exit(struct topstar_laptop *topstar) | 
|  | { | 
|  | input_unregister_device(topstar->input); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Platform | 
|  | */ | 
|  |  | 
|  | static struct platform_driver topstar_platform_driver = { | 
|  | .driver = { | 
|  | .name = TOPSTAR_LAPTOP_CLASS, | 
|  | }, | 
|  | }; | 
|  |  | 
|  | static int topstar_platform_init(struct topstar_laptop *topstar) | 
|  | { | 
|  | int err; | 
|  |  | 
|  | topstar->platform = platform_device_alloc(TOPSTAR_LAPTOP_CLASS, -1); | 
|  | if (!topstar->platform) | 
|  | return -ENOMEM; | 
|  |  | 
|  | platform_set_drvdata(topstar->platform, topstar); | 
|  |  | 
|  | err = platform_device_add(topstar->platform); | 
|  | if (err) | 
|  | goto err_device_put; | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | err_device_put: | 
|  | platform_device_put(topstar->platform); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static void topstar_platform_exit(struct topstar_laptop *topstar) | 
|  | { | 
|  | platform_device_unregister(topstar->platform); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * ACPI | 
|  | */ | 
|  |  | 
|  | static int topstar_acpi_fncx_switch(struct acpi_device *device, bool state) | 
|  | { | 
|  | acpi_status status; | 
|  | u64 arg = state ? 0x86 : 0x87; | 
|  |  | 
|  | status = acpi_execute_simple_method(device->handle, "FNCX", arg); | 
|  | if (ACPI_FAILURE(status)) { | 
|  | pr_err("Unable to switch FNCX notifications\n"); | 
|  | return -ENODEV; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void topstar_acpi_notify(struct acpi_device *device, u32 event) | 
|  | { | 
|  | struct topstar_laptop *topstar = acpi_driver_data(device); | 
|  | static bool dup_evnt[2]; | 
|  | bool *dup; | 
|  |  | 
|  | /* 0x83 and 0x84 key events comes duplicated... */ | 
|  | if (event == 0x83 || event == 0x84) { | 
|  | dup = &dup_evnt[event - 0x83]; | 
|  | if (*dup) { | 
|  | *dup = false; | 
|  | return; | 
|  | } | 
|  | *dup = true; | 
|  | } | 
|  |  | 
|  | topstar_input_notify(topstar, event); | 
|  | } | 
|  |  | 
|  | static int topstar_acpi_init(struct topstar_laptop *topstar) | 
|  | { | 
|  | return topstar_acpi_fncx_switch(topstar->device, true); | 
|  | } | 
|  |  | 
|  | static void topstar_acpi_exit(struct topstar_laptop *topstar) | 
|  | { | 
|  | topstar_acpi_fncx_switch(topstar->device, false); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Enable software-based WLAN LED control on systems with defective | 
|  | * hardware switch. | 
|  | */ | 
|  | static bool led_workaround; | 
|  |  | 
|  | static int dmi_led_workaround(const struct dmi_system_id *id) | 
|  | { | 
|  | led_workaround = true; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct dmi_system_id topstar_dmi_ids[] = { | 
|  | { | 
|  | .callback = dmi_led_workaround, | 
|  | .ident = "Topstar U931/RVP7", | 
|  | .matches = { | 
|  | DMI_MATCH(DMI_BOARD_NAME, "U931"), | 
|  | DMI_MATCH(DMI_BOARD_VERSION, "RVP7"), | 
|  | }, | 
|  | }, | 
|  | {} | 
|  | }; | 
|  |  | 
|  | static int topstar_acpi_add(struct acpi_device *device) | 
|  | { | 
|  | struct topstar_laptop *topstar; | 
|  | int err; | 
|  |  | 
|  | dmi_check_system(topstar_dmi_ids); | 
|  |  | 
|  | topstar = kzalloc(sizeof(struct topstar_laptop), GFP_KERNEL); | 
|  | if (!topstar) | 
|  | return -ENOMEM; | 
|  |  | 
|  | strcpy(acpi_device_name(device), "Topstar TPSACPI"); | 
|  | strcpy(acpi_device_class(device), TOPSTAR_LAPTOP_CLASS); | 
|  | device->driver_data = topstar; | 
|  | topstar->device = device; | 
|  |  | 
|  | err = topstar_acpi_init(topstar); | 
|  | if (err) | 
|  | goto err_free; | 
|  |  | 
|  | err = topstar_platform_init(topstar); | 
|  | if (err) | 
|  | goto err_acpi_exit; | 
|  |  | 
|  | err = topstar_input_init(topstar); | 
|  | if (err) | 
|  | goto err_platform_exit; | 
|  |  | 
|  | if (led_workaround) { | 
|  | err = topstar_led_init(topstar); | 
|  | if (err) | 
|  | goto err_input_exit; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | err_input_exit: | 
|  | topstar_input_exit(topstar); | 
|  | err_platform_exit: | 
|  | topstar_platform_exit(topstar); | 
|  | err_acpi_exit: | 
|  | topstar_acpi_exit(topstar); | 
|  | err_free: | 
|  | kfree(topstar); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static int topstar_acpi_remove(struct acpi_device *device) | 
|  | { | 
|  | struct topstar_laptop *topstar = acpi_driver_data(device); | 
|  |  | 
|  | if (led_workaround) | 
|  | topstar_led_exit(topstar); | 
|  |  | 
|  | topstar_input_exit(topstar); | 
|  | topstar_platform_exit(topstar); | 
|  | topstar_acpi_exit(topstar); | 
|  |  | 
|  | kfree(topstar); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct acpi_device_id topstar_device_ids[] = { | 
|  | { "TPS0001", 0 }, | 
|  | { "TPSACPI01", 0 }, | 
|  | { "", 0 }, | 
|  | }; | 
|  | MODULE_DEVICE_TABLE(acpi, topstar_device_ids); | 
|  |  | 
|  | static struct acpi_driver topstar_acpi_driver = { | 
|  | .name = "Topstar laptop ACPI driver", | 
|  | .class = TOPSTAR_LAPTOP_CLASS, | 
|  | .ids = topstar_device_ids, | 
|  | .ops = { | 
|  | .add = topstar_acpi_add, | 
|  | .remove = topstar_acpi_remove, | 
|  | .notify = topstar_acpi_notify, | 
|  | }, | 
|  | }; | 
|  |  | 
|  | static int __init topstar_laptop_init(void) | 
|  | { | 
|  | int ret; | 
|  |  | 
|  | ret = platform_driver_register(&topstar_platform_driver); | 
|  | if (ret < 0) | 
|  | return ret; | 
|  |  | 
|  | ret = acpi_bus_register_driver(&topstar_acpi_driver); | 
|  | if (ret < 0) | 
|  | goto err_driver_unreg; | 
|  |  | 
|  | pr_info("ACPI extras driver loaded\n"); | 
|  | return 0; | 
|  |  | 
|  | err_driver_unreg: | 
|  | platform_driver_unregister(&topstar_platform_driver); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static void __exit topstar_laptop_exit(void) | 
|  | { | 
|  | acpi_bus_unregister_driver(&topstar_acpi_driver); | 
|  | platform_driver_unregister(&topstar_platform_driver); | 
|  | } | 
|  |  | 
|  | module_init(topstar_laptop_init); | 
|  | module_exit(topstar_laptop_exit); | 
|  |  | 
|  | MODULE_AUTHOR("Herton Ronaldo Krzesinski"); | 
|  | MODULE_AUTHOR("Guillaume Douézan-Grard"); | 
|  | MODULE_DESCRIPTION("Topstar Laptop ACPI Extras driver"); | 
|  | MODULE_LICENSE("GPL"); |