root/drivers/platform/loongarch/loongson-laptop.c
// 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];

static bool bl_powered;
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, &params, 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 ec_backlight_set_power(bool state)
{
        int status;
        union acpi_object arg0 = { ACPI_TYPE_INTEGER };
        struct acpi_object_list args = { 1, &arg0 };

        arg0.integer.value = state;
        status = acpi_evaluate_object(NULL, "\\BLSW", &args, NULL);
        if (ACPI_FAILURE(status)) {
                pr_info("Loongson lvds error: 0x%x\n", status);
                return -EIO;
        }

        return 0;
}

static int loongson_laptop_backlight_update(struct backlight_device *bd)
{
        bool target_powered = !backlight_is_blank(bd);
        int ret = 0, lvl = ec_backlight_level(bd->props.brightness);

        if (lvl < 0)
                return -EIO;

        if (ec_set_brightness(lvl))
                return -EIO;

        if (target_powered != bl_powered) {
                ret = ec_backlight_set_power(target_powered);
                if (ret < 0)
                        return ret;

                bl_powered = target_powered;
        }

        return ret;
}

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, ret;
        struct backlight_properties props;

        memset(&props, 0, sizeof(props));

        if (!acpi_evalf(hotkey_handle, &status, "ECLL", "d"))
                return -EIO;

        ret = ec_backlight_set_power(true);
        if (ret)
                return ret;

        bl_powered = true;

        props.max_brightness = status;
        props.brightness = ec_get_brightness();
        props.power = BACKLIGHT_POWER_ON;
        props.type = BACKLIGHT_PLATFORM;

        backlight_device_register("loongson_laptop",
                                NULL, NULL, &backlight_laptop_ops, &props);


        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)
{
        int i;

        if (generic_inputdev) {
                if (!input_device_registered) {
                        input_free_device(generic_inputdev);
                } else {
                        input_unregister_device(generic_inputdev);

                        for (i = 0; i < ARRAY_SIZE(generic_sub_drivers); i++)
                                generic_subdriver_exit(&generic_sub_drivers[i]);
                }
        }
}

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");