629 lines · c
1// SPDX-License-Identifier: GPL-2.02/*3 * Generic Loongson processor based LAPTOP/ALL-IN-ONE driver4 *5 * Jianmin Lv <lvjianmin@loongson.cn>6 * Huacai Chen <chenhuacai@loongson.cn>7 *8 * Copyright (C) 2022 Loongson Technology Corporation Limited9 */10 11#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt12 13#include <linux/init.h>14#include <linux/kernel.h>15#include <linux/module.h>16#include <linux/acpi.h>17#include <linux/backlight.h>18#include <linux/device.h>19#include <linux/input.h>20#include <linux/input/sparse-keymap.h>21#include <linux/platform_device.h>22#include <linux/string.h>23#include <linux/types.h>24#include <acpi/video.h>25 26/* 1. Driver-wide structs and misc. variables */27 28/* ACPI HIDs */29#define LOONGSON_ACPI_EC_HID "PNP0C09"30#define LOONGSON_ACPI_HKEY_HID "LOON0000"31 32#define ACPI_LAPTOP_NAME "loongson-laptop"33#define ACPI_LAPTOP_ACPI_EVENT_PREFIX "loongson"34 35#define MAX_ACPI_ARGS 336#define GENERIC_HOTKEY_MAP_MAX 6437 38#define GENERIC_EVENT_TYPE_OFF 1239#define GENERIC_EVENT_TYPE_MASK 0xF00040#define GENERIC_EVENT_CODE_MASK 0x0FFF41 42struct generic_sub_driver {43 u32 type;44 char *name;45 acpi_handle *handle;46 struct acpi_device *device;47 struct platform_driver *driver;48 int (*init)(struct generic_sub_driver *sub_driver);49 void (*notify)(struct generic_sub_driver *sub_driver, u32 event);50 u8 acpi_notify_installed;51};52 53static u32 input_device_registered;54static struct input_dev *generic_inputdev;55 56static acpi_handle hotkey_handle;57static struct key_entry hotkey_keycode_map[GENERIC_HOTKEY_MAP_MAX];58 59int loongson_laptop_turn_on_backlight(void);60int loongson_laptop_turn_off_backlight(void);61static int loongson_laptop_backlight_update(struct backlight_device *bd);62 63/* 2. ACPI Helpers and device model */64 65static int acpi_evalf(acpi_handle handle, int *res, char *method, char *fmt, ...)66{67 char res_type;68 char *fmt0 = fmt;69 va_list ap;70 int success, quiet;71 acpi_status status;72 struct acpi_object_list params;73 struct acpi_buffer result, *resultp;74 union acpi_object in_objs[MAX_ACPI_ARGS], out_obj;75 76 if (!*fmt) {77 pr_err("acpi_evalf() called with empty format\n");78 return 0;79 }80 81 if (*fmt == 'q') {82 quiet = 1;83 fmt++;84 } else85 quiet = 0;86 87 res_type = *(fmt++);88 89 params.count = 0;90 params.pointer = &in_objs[0];91 92 va_start(ap, fmt);93 while (*fmt) {94 char c = *(fmt++);95 switch (c) {96 case 'd': /* int */97 in_objs[params.count].integer.value = va_arg(ap, int);98 in_objs[params.count++].type = ACPI_TYPE_INTEGER;99 break;100 /* add more types as needed */101 default:102 pr_err("acpi_evalf() called with invalid format character '%c'\n", c);103 va_end(ap);104 return 0;105 }106 }107 va_end(ap);108 109 if (res_type != 'v') {110 result.length = sizeof(out_obj);111 result.pointer = &out_obj;112 resultp = &result;113 } else114 resultp = NULL;115 116 status = acpi_evaluate_object(handle, method, ¶ms, resultp);117 118 switch (res_type) {119 case 'd': /* int */120 success = (status == AE_OK && out_obj.type == ACPI_TYPE_INTEGER);121 if (success && res)122 *res = out_obj.integer.value;123 break;124 case 'v': /* void */125 success = status == AE_OK;126 break;127 /* add more types as needed */128 default:129 pr_err("acpi_evalf() called with invalid format character '%c'\n", res_type);130 return 0;131 }132 133 if (!success && !quiet)134 pr_err("acpi_evalf(%s, %s, ...) failed: %s\n",135 method, fmt0, acpi_format_exception(status));136 137 return success;138}139 140static int hotkey_status_get(int *status)141{142 if (!acpi_evalf(hotkey_handle, status, "GSWS", "d"))143 return -EIO;144 145 return 0;146}147 148static void dispatch_acpi_notify(acpi_handle handle, u32 event, void *data)149{150 struct generic_sub_driver *sub_driver = data;151 152 if (!sub_driver || !sub_driver->notify)153 return;154 sub_driver->notify(sub_driver, event);155}156 157static int __init setup_acpi_notify(struct generic_sub_driver *sub_driver)158{159 acpi_status status;160 161 if (!*sub_driver->handle)162 return 0;163 164 sub_driver->device = acpi_fetch_acpi_dev(*sub_driver->handle);165 if (!sub_driver->device) {166 pr_err("acpi_fetch_acpi_dev(%s) failed\n", sub_driver->name);167 return -ENODEV;168 }169 170 sub_driver->device->driver_data = sub_driver;171 sprintf(acpi_device_class(sub_driver->device), "%s/%s",172 ACPI_LAPTOP_ACPI_EVENT_PREFIX, sub_driver->name);173 174 status = acpi_install_notify_handler(*sub_driver->handle,175 sub_driver->type, dispatch_acpi_notify, sub_driver);176 if (ACPI_FAILURE(status)) {177 if (status == AE_ALREADY_EXISTS) {178 pr_notice("Another device driver is already "179 "handling %s events\n", sub_driver->name);180 } else {181 pr_err("acpi_install_notify_handler(%s) failed: %s\n",182 sub_driver->name, acpi_format_exception(status));183 }184 return -ENODEV;185 }186 sub_driver->acpi_notify_installed = 1;187 188 return 0;189}190 191static int loongson_hotkey_suspend(struct device *dev)192{193 return 0;194}195 196static int loongson_hotkey_resume(struct device *dev)197{198 int status = 0;199 struct key_entry ke;200 struct backlight_device *bd;201 202 bd = backlight_device_get_by_type(BACKLIGHT_PLATFORM);203 if (bd) {204 loongson_laptop_backlight_update(bd) ?205 pr_warn("Loongson_backlight: resume brightness failed") :206 pr_info("Loongson_backlight: resume brightness %d\n", bd->props.brightness);207 }208 209 /*210 * Only if the firmware supports SW_LID event model, we can handle the211 * event. This is for the consideration of development board without EC.212 */213 if (test_bit(SW_LID, generic_inputdev->swbit)) {214 if (hotkey_status_get(&status) < 0)215 return -EIO;216 /*217 * The input device sw element records the last lid status.218 * When the system is awakened by other wake-up sources,219 * the lid event will also be reported. The judgment of220 * adding SW_LID bit which in sw element can avoid this221 * case.222 *223 * Input system will drop lid event when current lid event224 * value and last lid status in the same. So laptop driver225 * doesn't report repeated events.226 *227 * Lid status is generally 0, but hardware exception is228 * considered. So add lid status confirmation.229 */230 if (test_bit(SW_LID, generic_inputdev->sw) && !(status & (1 << SW_LID))) {231 ke.type = KE_SW;232 ke.sw.value = (u8)status;233 ke.sw.code = SW_LID;234 sparse_keymap_report_entry(generic_inputdev, &ke, 1, true);235 }236 }237 238 return 0;239}240 241static DEFINE_SIMPLE_DEV_PM_OPS(loongson_hotkey_pm,242 loongson_hotkey_suspend, loongson_hotkey_resume);243 244static int loongson_hotkey_probe(struct platform_device *pdev)245{246 hotkey_handle = ACPI_HANDLE(&pdev->dev);247 248 if (!hotkey_handle)249 return -ENODEV;250 251 return 0;252}253 254static const struct acpi_device_id loongson_device_ids[] = {255 {LOONGSON_ACPI_HKEY_HID, 0},256 {"", 0},257};258MODULE_DEVICE_TABLE(acpi, loongson_device_ids);259 260static struct platform_driver loongson_hotkey_driver = {261 .probe = loongson_hotkey_probe,262 .driver = {263 .name = "loongson-hotkey",264 .owner = THIS_MODULE,265 .pm = pm_ptr(&loongson_hotkey_pm),266 .acpi_match_table = loongson_device_ids,267 },268};269 270static int hotkey_map(void)271{272 u32 index;273 acpi_status status;274 struct acpi_buffer buf;275 union acpi_object *pack;276 277 buf.length = ACPI_ALLOCATE_BUFFER;278 status = acpi_evaluate_object_typed(hotkey_handle, "KMAP", NULL, &buf, ACPI_TYPE_PACKAGE);279 if (status != AE_OK) {280 pr_err("ACPI exception: %s\n", acpi_format_exception(status));281 return -1;282 }283 pack = buf.pointer;284 for (index = 0; index < pack->package.count; index++) {285 union acpi_object *element, *sub_pack;286 287 sub_pack = &pack->package.elements[index];288 289 element = &sub_pack->package.elements[0];290 hotkey_keycode_map[index].type = element->integer.value;291 element = &sub_pack->package.elements[1];292 hotkey_keycode_map[index].code = element->integer.value;293 element = &sub_pack->package.elements[2];294 hotkey_keycode_map[index].keycode = element->integer.value;295 }296 297 return 0;298}299 300static int hotkey_backlight_set(bool enable)301{302 if (!acpi_evalf(hotkey_handle, NULL, "VCBL", "vd", enable ? 1 : 0))303 return -EIO;304 305 return 0;306}307 308static int ec_get_brightness(void)309{310 int status = 0;311 312 if (!hotkey_handle)313 return -ENXIO;314 315 if (!acpi_evalf(hotkey_handle, &status, "ECBG", "d"))316 return -EIO;317 318 return status;319}320 321static int ec_set_brightness(int level)322{323 324 int ret = 0;325 326 if (!hotkey_handle)327 return -ENXIO;328 329 if (!acpi_evalf(hotkey_handle, NULL, "ECBS", "vd", level))330 ret = -EIO;331 332 return ret;333}334 335static int ec_backlight_level(u8 level)336{337 int status = 0;338 339 if (!hotkey_handle)340 return -ENXIO;341 342 if (!acpi_evalf(hotkey_handle, &status, "ECLL", "d"))343 return -EIO;344 345 if ((status < 0) || (level > status))346 return status;347 348 if (!acpi_evalf(hotkey_handle, &status, "ECSL", "d"))349 return -EIO;350 351 if ((status < 0) || (level < status))352 return status;353 354 return level;355}356 357static int loongson_laptop_backlight_update(struct backlight_device *bd)358{359 int lvl = ec_backlight_level(bd->props.brightness);360 361 if (lvl < 0)362 return -EIO;363 if (ec_set_brightness(lvl))364 return -EIO;365 366 return 0;367}368 369static int loongson_laptop_get_brightness(struct backlight_device *bd)370{371 int level;372 373 level = ec_get_brightness();374 if (level < 0)375 return -EIO;376 377 return level;378}379 380static const struct backlight_ops backlight_laptop_ops = {381 .update_status = loongson_laptop_backlight_update,382 .get_brightness = loongson_laptop_get_brightness,383};384 385static int laptop_backlight_register(void)386{387 int status = 0;388 struct backlight_properties props;389 390 memset(&props, 0, sizeof(props));391 392 if (!acpi_evalf(hotkey_handle, &status, "ECLL", "d"))393 return -EIO;394 395 props.brightness = 1;396 props.max_brightness = status;397 props.type = BACKLIGHT_PLATFORM;398 399 backlight_device_register("loongson_laptop",400 NULL, NULL, &backlight_laptop_ops, &props);401 402 return 0;403}404 405int loongson_laptop_turn_on_backlight(void)406{407 int status;408 union acpi_object arg0 = { ACPI_TYPE_INTEGER };409 struct acpi_object_list args = { 1, &arg0 };410 411 arg0.integer.value = 1;412 status = acpi_evaluate_object(NULL, "\\BLSW", &args, NULL);413 if (ACPI_FAILURE(status)) {414 pr_info("Loongson lvds error: 0x%x\n", status);415 return -ENODEV;416 }417 418 return 0;419}420 421int loongson_laptop_turn_off_backlight(void)422{423 int status;424 union acpi_object arg0 = { ACPI_TYPE_INTEGER };425 struct acpi_object_list args = { 1, &arg0 };426 427 arg0.integer.value = 0;428 status = acpi_evaluate_object(NULL, "\\BLSW", &args, NULL);429 if (ACPI_FAILURE(status)) {430 pr_info("Loongson lvds error: 0x%x\n", status);431 return -ENODEV;432 }433 434 return 0;435}436 437static int __init event_init(struct generic_sub_driver *sub_driver)438{439 int ret;440 441 ret = hotkey_map();442 if (ret < 0) {443 pr_err("Failed to parse keymap from DSDT\n");444 return ret;445 }446 447 ret = sparse_keymap_setup(generic_inputdev, hotkey_keycode_map, NULL);448 if (ret < 0) {449 pr_err("Failed to setup input device keymap\n");450 input_free_device(generic_inputdev);451 generic_inputdev = NULL;452 453 return ret;454 }455 456 /*457 * This hotkey driver handle backlight event when458 * acpi_video_get_backlight_type() gets acpi_backlight_vendor459 */460 if (acpi_video_get_backlight_type() == acpi_backlight_vendor)461 hotkey_backlight_set(true);462 else463 hotkey_backlight_set(false);464 465 pr_info("ACPI: enabling firmware HKEY event interface...\n");466 467 return ret;468}469 470static void event_notify(struct generic_sub_driver *sub_driver, u32 event)471{472 int type, scan_code;473 struct key_entry *ke = NULL;474 475 scan_code = event & GENERIC_EVENT_CODE_MASK;476 type = (event & GENERIC_EVENT_TYPE_MASK) >> GENERIC_EVENT_TYPE_OFF;477 ke = sparse_keymap_entry_from_scancode(generic_inputdev, scan_code);478 if (ke) {479 if (type == KE_SW) {480 int status = 0;481 482 if (hotkey_status_get(&status) < 0)483 return;484 485 ke->sw.value = !!(status & (1 << ke->sw.code));486 }487 sparse_keymap_report_entry(generic_inputdev, ke, 1, true);488 }489}490 491/* 3. Infrastructure */492 493static void generic_subdriver_exit(struct generic_sub_driver *sub_driver);494 495static int __init generic_subdriver_init(struct generic_sub_driver *sub_driver)496{497 int ret;498 499 if (!sub_driver || !sub_driver->driver)500 return -EINVAL;501 502 ret = platform_driver_register(sub_driver->driver);503 if (ret)504 return -EINVAL;505 506 if (sub_driver->init) {507 ret = sub_driver->init(sub_driver);508 if (ret)509 goto err_out;510 }511 512 if (sub_driver->notify) {513 ret = setup_acpi_notify(sub_driver);514 if (ret == -ENODEV) {515 ret = 0;516 goto err_out;517 }518 if (ret < 0)519 goto err_out;520 }521 522 return 0;523 524err_out:525 generic_subdriver_exit(sub_driver);526 return ret;527}528 529static void generic_subdriver_exit(struct generic_sub_driver *sub_driver)530{531 532 if (sub_driver->acpi_notify_installed) {533 acpi_remove_notify_handler(*sub_driver->handle,534 sub_driver->type, dispatch_acpi_notify);535 sub_driver->acpi_notify_installed = 0;536 }537 platform_driver_unregister(sub_driver->driver);538}539 540static struct generic_sub_driver generic_sub_drivers[] __refdata = {541 {542 .name = "hotkey",543 .init = event_init,544 .notify = event_notify,545 .handle = &hotkey_handle,546 .type = ACPI_DEVICE_NOTIFY,547 .driver = &loongson_hotkey_driver,548 },549};550 551static int __init generic_acpi_laptop_init(void)552{553 bool ec_found;554 int i, ret, status;555 556 if (acpi_disabled)557 return -ENODEV;558 559 /* The EC device is required */560 ec_found = acpi_dev_found(LOONGSON_ACPI_EC_HID);561 if (!ec_found)562 return -ENODEV;563 564 /* Enable SCI for EC */565 acpi_write_bit_register(ACPI_BITREG_SCI_ENABLE, 1);566 567 generic_inputdev = input_allocate_device();568 if (!generic_inputdev) {569 pr_err("Unable to allocate input device\n");570 return -ENOMEM;571 }572 573 /* Prepare input device, but don't register */574 generic_inputdev->name =575 "Loongson Generic Laptop/All-in-One Extra Buttons";576 generic_inputdev->phys = ACPI_LAPTOP_NAME "/input0";577 generic_inputdev->id.bustype = BUS_HOST;578 generic_inputdev->dev.parent = NULL;579 580 /* Init subdrivers */581 for (i = 0; i < ARRAY_SIZE(generic_sub_drivers); i++) {582 ret = generic_subdriver_init(&generic_sub_drivers[i]);583 if (ret < 0) {584 input_free_device(generic_inputdev);585 while (--i >= 0)586 generic_subdriver_exit(&generic_sub_drivers[i]);587 return ret;588 }589 }590 591 ret = input_register_device(generic_inputdev);592 if (ret < 0) {593 input_free_device(generic_inputdev);594 while (--i >= 0)595 generic_subdriver_exit(&generic_sub_drivers[i]);596 pr_err("Unable to register input device\n");597 return ret;598 }599 600 input_device_registered = 1;601 602 if (acpi_evalf(hotkey_handle, &status, "ECBG", "d")) {603 pr_info("Loongson Laptop used, init brightness is 0x%x\n", status);604 ret = laptop_backlight_register();605 if (ret < 0)606 pr_err("Loongson Laptop: laptop-backlight device register failed\n");607 }608 609 return 0;610}611 612static void __exit generic_acpi_laptop_exit(void)613{614 if (generic_inputdev) {615 if (input_device_registered)616 input_unregister_device(generic_inputdev);617 else618 input_free_device(generic_inputdev);619 }620}621 622module_init(generic_acpi_laptop_init);623module_exit(generic_acpi_laptop_exit);624 625MODULE_AUTHOR("Jianmin Lv <lvjianmin@loongson.cn>");626MODULE_AUTHOR("Huacai Chen <chenhuacai@loongson.cn>");627MODULE_DESCRIPTION("Loongson Laptop/All-in-One ACPI Driver");628MODULE_LICENSE("GPL");629