// SPDX-License-Identifier: GPL-2.0 /* * Generic Loongson processor based LAPTOP/ALL-IN-ONE driver * * Jianmin Lv <lvjianmin@loongson.cn> * Huacai Chen <chenhuacai@loongson.cn> * * Copyright (C) 2022 Loongson Technology Corporation Limited */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/acpi.h> #include <linux/backlight.h> #include <linux/device.h> #include <linux/input.h> #include <linux/input/sparse-keymap.h> #include <linux/platform_device.h> #include <linux/string.h> #include <linux/types.h> #include <acpi/video.h> /* 1. Driver-wide structs and misc. variables */ /* ACPI HIDs */ #define LOONGSON_ACPI_EC_HID "PNP0C09" #define LOONGSON_ACPI_HKEY_HID "LOON0000" #define ACPI_LAPTOP_NAME "loongson-laptop" #define ACPI_LAPTOP_ACPI_EVENT_PREFIX "loongson" #define MAX_ACPI_ARGS 3 #define GENERIC_HOTKEY_MAP_MAX 64 #define GENERIC_EVENT_TYPE_OFF 12 #define GENERIC_EVENT_TYPE_MASK 0xF000 #define GENERIC_EVENT_CODE_MASK 0x0FFF struct generic_sub_driver { u32 type; char *name; acpi_handle *handle; struct acpi_device *device; struct platform_driver *driver; int (*init)(struct generic_sub_driver *sub_driver); void (*notify)(struct generic_sub_driver *sub_driver, u32 event); u8 acpi_notify_installed; }; static u32 input_device_registered; static struct input_dev *generic_inputdev; static acpi_handle hotkey_handle; static struct key_entry hotkey_keycode_map[GENERIC_HOTKEY_MAP_MAX]; int loongson_laptop_turn_on_backlight(void); int loongson_laptop_turn_off_backlight(void); static int loongson_laptop_backlight_update(struct backlight_device *bd); /* 2. ACPI Helpers and device model */ static int acpi_evalf(acpi_handle handle, int *res, char *method, char *fmt, ...) { char res_type; char *fmt0 = fmt; va_list ap; int success, quiet; acpi_status status; struct acpi_object_list params; struct acpi_buffer result, *resultp; union acpi_object in_objs[MAX_ACPI_ARGS], out_obj; if (!*fmt) { pr_err("acpi_evalf() called with empty format\n"); return 0; } if (*fmt == 'q') { quiet = 1; fmt++; } else quiet = 0; res_type = *(fmt++); params.count = 0; params.pointer = &in_objs[0]; va_start(ap, fmt); while (*fmt) { char c = *(fmt++); switch (c) { case 'd': /* int */ in_objs[params.count].integer.value = va_arg(ap, int); in_objs[params.count++].type = ACPI_TYPE_INTEGER; break; /* add more types as needed */ default: pr_err("acpi_evalf() called with invalid format character '%c'\n", c); va_end(ap); return 0; } } va_end(ap); if (res_type != 'v') { result.length = sizeof(out_obj); result.pointer = &out_obj; resultp = &result; } else resultp = NULL; status = acpi_evaluate_object(handle, method, ¶ms, resultp); switch (res_type) { case 'd': /* int */ success = (status == AE_OK && out_obj.type == ACPI_TYPE_INTEGER); if (success && res) *res = out_obj.integer.value; break; case 'v': /* void */ success = status == AE_OK; break; /* add more types as needed */ default: pr_err("acpi_evalf() called with invalid format character '%c'\n", res_type); return 0; } if (!success && !quiet) pr_err("acpi_evalf(%s, %s, ...) failed: %s\n", method, fmt0, acpi_format_exception(status)); return success; } static int hotkey_status_get(int *status) { if (!acpi_evalf(hotkey_handle, status, "GSWS", "d")) return -EIO; return 0; } static void dispatch_acpi_notify(acpi_handle handle, u32 event, void *data) { struct generic_sub_driver *sub_driver = data; if (!sub_driver || !sub_driver->notify) return; sub_driver->notify(sub_driver, event); } static int __init setup_acpi_notify(struct generic_sub_driver *sub_driver) { acpi_status status; if (!*sub_driver->handle) return 0; sub_driver->device = acpi_fetch_acpi_dev(*sub_driver->handle); if (!sub_driver->device) { pr_err("acpi_fetch_acpi_dev(%s) failed\n", sub_driver->name); return -ENODEV; } sub_driver->device->driver_data = sub_driver; sprintf(acpi_device_class(sub_driver->device), "%s/%s", ACPI_LAPTOP_ACPI_EVENT_PREFIX, sub_driver->name); status = acpi_install_notify_handler(*sub_driver->handle, sub_driver->type, dispatch_acpi_notify, sub_driver); if (ACPI_FAILURE(status)) { if (status == AE_ALREADY_EXISTS) { pr_notice("Another device driver is already " "handling %s events\n", sub_driver->name); } else { pr_err("acpi_install_notify_handler(%s) failed: %s\n", sub_driver->name, acpi_format_exception(status)); } return -ENODEV; } sub_driver->acpi_notify_installed = 1; return 0; } static int loongson_hotkey_suspend(struct device *dev) { return 0; } static int loongson_hotkey_resume(struct device *dev) { int status = 0; struct key_entry ke; struct backlight_device *bd; bd = backlight_device_get_by_type(BACKLIGHT_PLATFORM); if (bd) { loongson_laptop_backlight_update(bd) ? pr_warn("Loongson_backlight: resume brightness failed") : pr_info("Loongson_backlight: resume brightness %d\n", bd->props.brightness); } /* * Only if the firmware supports SW_LID event model, we can handle the * event. This is for the consideration of development board without EC. */ if (test_bit(SW_LID, generic_inputdev->swbit)) { if (hotkey_status_get(&status) < 0) return -EIO; /* * The input device sw element records the last lid status. * When the system is awakened by other wake-up sources, * the lid event will also be reported. The judgment of * adding SW_LID bit which in sw element can avoid this * case. * * Input system will drop lid event when current lid event * value and last lid status in the same. So laptop driver * doesn't report repeated events. * * Lid status is generally 0, but hardware exception is * considered. So add lid status confirmation. */ if (test_bit(SW_LID, generic_inputdev->sw) && !(status & (1 << SW_LID))) { ke.type = KE_SW; ke.sw.value = (u8)status; ke.sw.code = SW_LID; sparse_keymap_report_entry(generic_inputdev, &ke, 1, true); } } return 0; } static DEFINE_SIMPLE_DEV_PM_OPS(loongson_hotkey_pm, loongson_hotkey_suspend, loongson_hotkey_resume); static int loongson_hotkey_probe(struct platform_device *pdev) { hotkey_handle = ACPI_HANDLE(&pdev->dev); if (!hotkey_handle) return -ENODEV; return 0; } static const struct acpi_device_id loongson_device_ids[] = { {LOONGSON_ACPI_HKEY_HID, 0}, {"", 0}, }; MODULE_DEVICE_TABLE(acpi, loongson_device_ids); static struct platform_driver loongson_hotkey_driver = { .probe = loongson_hotkey_probe, .driver = { .name = "loongson-hotkey", .owner = THIS_MODULE, .pm = pm_ptr(&loongson_hotkey_pm), .acpi_match_table = loongson_device_ids, }, }; static int hotkey_map(void) { u32 index; acpi_status status; struct acpi_buffer buf; union acpi_object *pack; buf.length = ACPI_ALLOCATE_BUFFER; status = acpi_evaluate_object_typed(hotkey_handle, "KMAP", NULL, &buf, ACPI_TYPE_PACKAGE); if (status != AE_OK) { pr_err("ACPI exception: %s\n", acpi_format_exception(status)); return -1; } pack = buf.pointer; for (index = 0; index < pack->package.count; index++) { union acpi_object *element, *sub_pack; sub_pack = &pack->package.elements[index]; element = &sub_pack->package.elements[0]; hotkey_keycode_map[index].type = element->integer.value; element = &sub_pack->package.elements[1]; hotkey_keycode_map[index].code = element->integer.value; element = &sub_pack->package.elements[2]; hotkey_keycode_map[index].keycode = element->integer.value; } return 0; } static int hotkey_backlight_set(bool enable) { if (!acpi_evalf(hotkey_handle, NULL, "VCBL", "vd", enable ? 1 : 0)) return -EIO; return 0; } static int ec_get_brightness(void) { int status = 0; if (!hotkey_handle) return -ENXIO; if (!acpi_evalf(hotkey_handle, &status, "ECBG", "d")) return -EIO; return status; } static int ec_set_brightness(int level) { int ret = 0; if (!hotkey_handle) return -ENXIO; if (!acpi_evalf(hotkey_handle, NULL, "ECBS", "vd", level)) ret = -EIO; return ret; } static int ec_backlight_level(u8 level) { int status = 0; if (!hotkey_handle) return -ENXIO; if (!acpi_evalf(hotkey_handle, &status, "ECLL", "d")) return -EIO; if ((status < 0) || (level > status)) return status; if (!acpi_evalf(hotkey_handle, &status, "ECSL", "d")) return -EIO; if ((status < 0) || (level < status)) return status; return level; } static int loongson_laptop_backlight_update(struct backlight_device *bd) { int lvl = ec_backlight_level(bd->props.brightness); if (lvl < 0) return -EIO; if (ec_set_brightness(lvl)) return -EIO; return 0; } static int loongson_laptop_get_brightness(struct backlight_device *bd) { int level; level = ec_get_brightness(); if (level < 0) return -EIO; return level; } static const struct backlight_ops backlight_laptop_ops = { .update_status = loongson_laptop_backlight_update, .get_brightness = loongson_laptop_get_brightness, }; static int laptop_backlight_register(void) { int status = 0; struct backlight_properties props; memset(&props, 0, sizeof(props)); if (!acpi_evalf(hotkey_handle, &status, "ECLL", "d")) return -EIO; props.brightness = 1; props.max_brightness = status; props.type = BACKLIGHT_PLATFORM; backlight_device_register("loongson_laptop", NULL, NULL, &backlight_laptop_ops, &props); return 0; } int loongson_laptop_turn_on_backlight(void) { int status; union acpi_object arg0 = { ACPI_TYPE_INTEGER }; struct acpi_object_list args = { 1, &arg0 }; arg0.integer.value = 1; status = acpi_evaluate_object(NULL, "\\BLSW", &args, NULL); if (ACPI_FAILURE(status)) { pr_info("Loongson lvds error: 0x%x\n", status); return -ENODEV; } return 0; } int loongson_laptop_turn_off_backlight(void) { int status; union acpi_object arg0 = { ACPI_TYPE_INTEGER }; struct acpi_object_list args = { 1, &arg0 }; arg0.integer.value = 0; status = acpi_evaluate_object(NULL, "\\BLSW", &args, NULL); if (ACPI_FAILURE(status)) { pr_info("Loongson lvds error: 0x%x\n", status); return -ENODEV; } return 0; } static int __init event_init(struct generic_sub_driver *sub_driver) { int ret; ret = hotkey_map(); if (ret < 0) { pr_err("Failed to parse keymap from DSDT\n"); return ret; } ret = sparse_keymap_setup(generic_inputdev, hotkey_keycode_map, NULL); if (ret < 0) { pr_err("Failed to setup input device keymap\n"); input_free_device(generic_inputdev); generic_inputdev = NULL; return ret; } /* * This hotkey driver handle backlight event when * acpi_video_get_backlight_type() gets acpi_backlight_vendor */ if (acpi_video_get_backlight_type() == acpi_backlight_vendor) hotkey_backlight_set(true); else hotkey_backlight_set(false); pr_info("ACPI: enabling firmware HKEY event interface...\n"); return ret; } static void event_notify(struct generic_sub_driver *sub_driver, u32 event) { int type, scan_code; struct key_entry *ke = NULL; scan_code = event & GENERIC_EVENT_CODE_MASK; type = (event & GENERIC_EVENT_TYPE_MASK) >> GENERIC_EVENT_TYPE_OFF; ke = sparse_keymap_entry_from_scancode(generic_inputdev, scan_code); if (ke) { if (type == KE_SW) { int status = 0; if (hotkey_status_get(&status) < 0) return; ke->sw.value = !!(status & (1 << ke->sw.code)); } sparse_keymap_report_entry(generic_inputdev, ke, 1, true); } } /* 3. Infrastructure */ static void generic_subdriver_exit(struct generic_sub_driver *sub_driver); static int __init generic_subdriver_init(struct generic_sub_driver *sub_driver) { int ret; if (!sub_driver || !sub_driver->driver) return -EINVAL; ret = platform_driver_register(sub_driver->driver); if (ret) return -EINVAL; if (sub_driver->init) { ret = sub_driver->init(sub_driver); if (ret) goto err_out; } if (sub_driver->notify) { ret = setup_acpi_notify(sub_driver); if (ret == -ENODEV) { ret = 0; goto err_out; } if (ret < 0) goto err_out; } return 0; err_out: generic_subdriver_exit(sub_driver); return ret; } static void generic_subdriver_exit(struct generic_sub_driver *sub_driver) { if (sub_driver->acpi_notify_installed) { acpi_remove_notify_handler(*sub_driver->handle, sub_driver->type, dispatch_acpi_notify); sub_driver->acpi_notify_installed = 0; } platform_driver_unregister(sub_driver->driver); } static struct generic_sub_driver generic_sub_drivers[] __refdata = { { .name = "hotkey", .init = event_init, .notify = event_notify, .handle = &hotkey_handle, .type = ACPI_DEVICE_NOTIFY, .driver = &loongson_hotkey_driver, }, }; static int __init generic_acpi_laptop_init(void) { bool ec_found; int i, ret, status; if (acpi_disabled) return -ENODEV; /* The EC device is required */ ec_found = acpi_dev_found(LOONGSON_ACPI_EC_HID); if (!ec_found) return -ENODEV; /* Enable SCI for EC */ acpi_write_bit_register(ACPI_BITREG_SCI_ENABLE, 1); generic_inputdev = input_allocate_device(); if (!generic_inputdev) { pr_err("Unable to allocate input device\n"); return -ENOMEM; } /* Prepare input device, but don't register */ generic_inputdev->name = "Loongson Generic Laptop/All-in-One Extra Buttons"; generic_inputdev->phys = ACPI_LAPTOP_NAME "/input0"; generic_inputdev->id.bustype = BUS_HOST; generic_inputdev->dev.parent = NULL; /* Init subdrivers */ for (i = 0; i < ARRAY_SIZE(generic_sub_drivers); i++) { ret = generic_subdriver_init(&generic_sub_drivers[i]); if (ret < 0) { input_free_device(generic_inputdev); while (--i >= 0) generic_subdriver_exit(&generic_sub_drivers[i]); return ret; } } ret = input_register_device(generic_inputdev); if (ret < 0) { input_free_device(generic_inputdev); while (--i >= 0) generic_subdriver_exit(&generic_sub_drivers[i]); pr_err("Unable to register input device\n"); return ret; } input_device_registered = 1; if (acpi_evalf(hotkey_handle, &status, "ECBG", "d")) { pr_info("Loongson Laptop used, init brightness is 0x%x\n", status); ret = laptop_backlight_register(); if (ret < 0) pr_err("Loongson Laptop: laptop-backlight device register failed\n"); } return 0; } static void __exit generic_acpi_laptop_exit(void) { if (generic_inputdev) { if (input_device_registered) input_unregister_device(generic_inputdev); else input_free_device(generic_inputdev); } } module_init(generic_acpi_laptop_init); module_exit(generic_acpi_laptop_exit); MODULE_AUTHOR("Jianmin Lv <lvjianmin@loongson.cn>"); MODULE_AUTHOR("Huacai Chen <chenhuacai@loongson.cn>"); MODULE_DESCRIPTION("Loongson Laptop/All-in-One ACPI Driver"); MODULE_LICENSE("GPL");