Message ID | 20240713163521.21958-2-mustafa.eskieksi@gmail.com |
---|---|
State | New |
Headers | show |
Series | [v6] platform/x86: Add wmi driver for Casper Excalibur laptops | expand |
Hi Mustafa, Thank you for submitting a new version of this! Here is how far I got with reviewing this driver today. It would be good if you can prepare a v7 addressing the review comments so far. I'll try to review the rest later, but that may problably have to wait till next week Monday (which is why a v7 would be good). On 7/13/24 6:35 PM, Mustafa Ekşi wrote: > This wmi driver supports changing Casper Excalibur laptops' keyboard > backlight brightness and color, reading fan speeds and changing power > profiles. Multicolor led device is used for backlight, platform_profile > for power management and hwmon for fan speeds. It supports both old (10th > gen or older) and new (11th gen or newer) laptops. It uses x86_match_cpu > to check if the laptop is old or new. > > Signed-off-by: Mustafa Ekşi <mustafa.eskieksi@gmail.com> > --- > Changes in v6: > - Added "rgb" to zone names and changed kbd_zoned_backlight-corners to > backlight. > - Changed led structure to have 3 seperate subleds instead of one rgb > subled. > - Removed led_cache. > - Removing platform_profile and destroying casper_mutex is managed by > devm_add_action_or_reset now. > - Removed casper_wmi_remove. > - Reordered some variables. > Changes in v5: > - Added mutex_destroy to casper_wmi_probe error handling > - casper_multicolor_register now sets all leds to CASPER_DEFAULT_COLOR > - Some minor changes > Changes in v4: > - Renamed casper_driver to casper_drv > - Moved all global variables to casper_drv struct. Devices access > casper_drv via wdev's driver_data. > - Removed struct led_cache, because only its u32 array was used. It is > replaced with color_cache. > - Added mutex_locks in casper_set and casper_query, so they now accept > casper_drv instead of wmi_device as argument. > - Changed endianess conversion in hwmon_read to something sparse doesn't > complain about. > - Moved registrations of multicolor leds and platform profile to their > own functions. This makes casper_wmi_probe more readable. > - Added .no_singleton to wmi_device. > - Some minor changes. > Changes in v3: > - Replaced led_control attribute with multicolor led interface. > - Added struct led_cache, instead of storing only last color change. > - Added dmi list to prevent registering platform_profile driver in models > that doesn't have this feature. > - Added a x86_cpu_id to differentiate older laptops that are reporting > fan speeds in big-endian. Also newer laptops have a different power > profile scheme. I'm using x86_cpu_id because they don't have a > difference in model names, only in cpu generations (the official driver > download page makes you select your cpu's generation too). > - Removed hwmon_pwm device in favor of platform_profile driver. It > indirectly affects fans' speed but they also affect frequency and > power consumption as well. > - Replaced handwritten masks with GENMASK equivalents. > - Replaced led_classdev_register with > devm_led_classdev_multicolor_register. This should solve the bug > where led_classdev remains registered even if casper_wmi_probe > returns -ENODEV. > - Removed select NEW_LEDS and LEDS_CLASS, because it creates recursive > dependencies. > - And some minor changes. > Changes in v2: > - Added masks for > - Changed casper_set and casper_query returns Linux error code rather > than acpi_status. > - replaced complicated bit operations with FIELD_GET. > - Fixed some indentation and spacing. > - Broke fan speeds further. > - Moved module metadata to the end of the file. > --- > MAINTAINERS | 6 + > drivers/platform/x86/Kconfig | 14 + > drivers/platform/x86/Makefile | 1 + > drivers/platform/x86/casper-wmi.c | 656 ++++++++++++++++++++++++++++++ > 4 files changed, 677 insertions(+) > create mode 100644 drivers/platform/x86/casper-wmi.c > > diff --git a/MAINTAINERS b/MAINTAINERS > index a48ddea7b9b..13844ad3d12 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -4907,6 +4907,12 @@ S: Maintained > W: https://wireless.wiki.kernel.org/en/users/Drivers/carl9170 > F: drivers/net/wireless/ath/carl9170/ > > +CASPER EXCALIBUR WMI DRIVER > +M: Mustafa Ekşi <mustafa.eskieksi@gmail.com> > +L: platform-driver-x86@vger.kernel.org > +S: Maintained > +F: drivers/platform/x86/casper-wmi.c > + > CAVIUM I2C DRIVER > M: Robert Richter <rric@kernel.org> > S: Odd Fixes > diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig > index 665fa952498..7560d90ce75 100644 > --- a/drivers/platform/x86/Kconfig > +++ b/drivers/platform/x86/Kconfig > @@ -1182,6 +1182,20 @@ config SEL3350_PLATFORM > To compile this driver as a module, choose M here: the module > will be called sel3350-platform. > > +config CASPER_WMI > + tristate "Casper Excalibur Laptop WMI driver" > + depends on ACPI_WMI > + depends on HWMON > + depends on LEDS_CLASS_MULTICOLOR > + select ACPI_PLATFORM_PROFILE > + help > + Say Y here if you want to support WMI-based fan speed reporting, > + power management and keyboard backlight support on Casper Excalibur > + Laptops. > + > + To compile this driver as a module, choose M here: the module will > + be called casper-wmi. > + > endif # X86_PLATFORM_DEVICES > > config P2SB > diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile > index e1b14294706..639509f9afa 100644 > --- a/drivers/platform/x86/Makefile > +++ b/drivers/platform/x86/Makefile > @@ -14,6 +14,7 @@ obj-$(CONFIG_MXM_WMI) += mxm-wmi.o > obj-$(CONFIG_NVIDIA_WMI_EC_BACKLIGHT) += nvidia-wmi-ec-backlight.o > obj-$(CONFIG_XIAOMI_WMI) += xiaomi-wmi.o > obj-$(CONFIG_GIGABYTE_WMI) += gigabyte-wmi.o > +obj-$(CONFIG_CASPER_WMI) += casper-wmi.o > > # Acer > obj-$(CONFIG_ACERHDF) += acerhdf.o > diff --git a/drivers/platform/x86/casper-wmi.c b/drivers/platform/x86/casper-wmi.c > new file mode 100644 > index 00000000000..51981e591ee > --- /dev/null > +++ b/drivers/platform/x86/casper-wmi.c > @@ -0,0 +1,656 @@ > +// SPDX-License-Identifier: GPL-2.0-or-later > +#include <linux/module.h> > +#include <linux/bits.h> > +#include <linux/bitops.h> > +#include <linux/acpi.h> > +#include <linux/leds.h> > +#include <linux/slab.h> > +#include <linux/wmi.h> > +#include <linux/device.h> > +#include <linux/hwmon.h> > +#include <linux/sysfs.h> > +#include <linux/types.h> > +#include <acpi/acexcep.h> This one looks a bit weird, do you really need this one ? > +#include <linux/bitfield.h> > +#include <linux/platform_profile.h> > +#include <linux/led-class-multicolor.h> > +#include <linux/mutex_types.h> > +#include <linux/err.h> > +#include <linux/mutex.h> > +#include <linux/container_of.h> Please sort all the #include <linux/...h> lines alphabetically. > + > +#include <linux/dmi.h> > +#include <asm/cpu_device_id.h> > +#include <asm/intel-family.h> > + > +#define CASPER_WMI_GUID "644C5791-B7B0-4123-A90B-E93876E0DAAD" > + > +#define CASPER_READ 0xfa00 > +#define CASPER_WRITE 0xfb00 > +#define CASPER_GET_HARDWAREINFO 0x0200 > +#define CASPER_SET_LED 0x0100 > +#define CASPER_POWERPLAN 0x0300 > + > +#define CASPER_KEYBOARD_LED_1 0x03 > +#define CASPER_KEYBOARD_LED_2 0x04 > +#define CASPER_KEYBOARD_LED_3 0x05 > +#define CASPER_ALL_KEYBOARD_LEDS 0x06 > +#define CASPER_CORNER_LEDS 0x07 > +#define CASPER_LED_COUNT 4 > + > +static const char * const zone_names[CASPER_LED_COUNT] = { > + "casper:rgb:kbd_zoned_backlight-right", > + "casper:rgb:kbd_zoned_backlight-middle", > + "casper:rgb:kbd_zoned_backlight-left", > + "casper:rgb:backlight", > +}; As mentioned in my reply to your cover-letter lets make the last one "casper:rgb:biaslight". > + > +#define CASPER_LED_ALPHA GENMASK(31, 24) > +#define CASPER_LED_RED GENMASK(23, 16) > +#define CASPER_LED_GREEN GENMASK(15, 8) > +#define CASPER_LED_BLUE GENMASK(7, 0) > +#define CASPER_DEFAULT_COLOR (CASPER_LED_RED | CASPER_LED_GREEN | \ > + CASPER_LED_BLUE) > +#define CASPER_FAN_CPU 0 > +#define CASPER_FAN_GPU 1 > + > +enum casper_power_profile_old { > + CASPER_HIGH_PERFORMANCE = 1, > + CASPER_GAMING = 2, > + CASPER_TEXT_MODE = 3, > + CASPER_POWERSAVE = 4 > +}; > + > +enum casper_power_profile_new { > + CASPER_NEW_HIGH_PERFORMANCE = 0, > + CASPER_NEW_GAMING = 1, > + CASPER_NEW_AUDIO = 2 > +}; > + > +struct casper_quirk_entry { > + bool big_endian_fans; > + bool no_power_profiles; > + bool new_power_scheme; > +}; > + > +struct casper_fourzone_led { > + struct led_classdev_mc mc_led; > + struct mc_subled *subleds; Since there are always 3 subleds you can simply make this: struct casper_fourzone_led { struct led_classdev_mc mc_led; struct mc_subled subleds[3]; }; and then drop the devm_kzalloc + error checking of the kzalloc for the subleds. > +}; > + > +struct casper_drv { > + struct platform_profile_handler handler; > + struct mutex casper_mutex; I see that you use devm_add_action_or_reset() for the cleanup of the mutex, but we have devm_mutex_init() now which does this for you (without needing to specify your own custom action), please switch to devm_mutex_init(). Also I do not believe the casper_ prefix adds anything, so how about just: struct mutex mutex; ? (yes this is allowed) > + struct casper_fourzone_led *leds; > + struct wmi_device *wdev; > + struct casper_quirk_entry *quirk_applied; > +}; > + > +struct casper_wmi_args { > + u16 a0, a1; > + u32 a2, a3, a4, a5, a6, a7, a8; > +}; > + > +enum casper_led_mode { > + LED_NORMAL = 0x10, > + LED_BLINK = 0x20, > + LED_FADE = 0x30, > + LED_HEARTBEAT = 0x40, > + LED_REPEAT = 0x50, > + LED_RANDOM = 0x60 > +}; > + > +static int casper_set(struct casper_drv *drv, u16 a1, u8 led_id, u32 data) > +{ > + struct casper_wmi_args wmi_args; > + struct acpi_buffer input; > + acpi_status status = 0; There is no need to initialize status, it is not used before it is set the first time. > + int ret = 0; > + > + wmi_args = (struct casper_wmi_args) { > + .a0 = CASPER_WRITE, > + .a1 = a1, > + .a2 = led_id, > + .a3 = data > + }; > + > + input = (struct acpi_buffer) { > + (acpi_size) sizeof(struct casper_wmi_args), > + &wmi_args > + }; > + > + mutex_lock(&drv->casper_mutex); Please add #include <linux/cleanup.h> at the top and then use: guard(mutex)(&drv->casper_mutex); instead of mutex_lock(), this will automatically unlock as soon as you leave the function. > + > + status = wmidev_block_set(drv->wdev, 0, &input); > + if (ACPI_FAILURE(status)) > + ret = -EIO; And then you can replace this with: if (ACPI_FAILURE(status)) return -EIO; > + mutex_unlock(&drv->casper_mutex); > + return ret; And drop the unlock as that is now done automatically and then replace the unlock + return ret with just: return 0; And then you can also drop the ret variable. Using guard(mutex)(&drv->casper_mutex); for auto-unlock on leaving the function is especially handy for more complex functions like the casper_query() below, since this will allow you to avoid using goto-s to exit the function. Please switch to using guard(mutex)(&drv->casper_mutex); everywhere. > +} > + > +static int casper_query(struct casper_drv *drv, u16 a1, > + struct casper_wmi_args *out) > +{ > + struct casper_wmi_args wmi_args; > + struct acpi_buffer input; > + union acpi_object *obj; > + acpi_status status = 0; Again no need to initialize status. > + int ret = 0; > + > + wmi_args = (struct casper_wmi_args) { > + .a0 = CASPER_READ, > + .a1 = a1 > + }; > + input = (struct acpi_buffer) { > + (acpi_size) sizeof(struct casper_wmi_args), > + &wmi_args > + }; > + > + mutex_lock(&drv->casper_mutex); As mentioned please switch to guard(mutex)(&drv->casper_mutex); > + > + status = wmidev_block_set(drv->wdev, 0, &input); > + if (ACPI_FAILURE(status)) { > + ret = -EIO; > + goto unlock; > + } And then this becomes: if (ACPI_FAILURE(status)) return -EIO; without needing a goto. > + > + obj = wmidev_block_query(drv->wdev, 0); > + if (!obj) { > + ret = -EIO; > + goto unlock; > + } Same here, just directly return -EIO. > + > + if (obj->type != ACPI_TYPE_BUFFER) { // obj will be 0x10 on failure > + ret = -EINVAL; > + goto freeobj; > + } > + if (obj->buffer.length != sizeof(struct casper_wmi_args)) { > + ret = -EIO; > + goto freeobj; You still need the freeobj label though ... > + } > + > + memcpy(out, obj->buffer.pointer, sizeof(struct casper_wmi_args)); > + > +freeobj: > + kfree(obj); > +unlock: > + mutex_unlock(&drv->casper_mutex); > + return ret; > +} Ok, this is as far as I have been able to review this code today, more later... Regards, Hans > + > +static u32 get_zone_color(struct casper_fourzone_led z) > +{ > + return FIELD_PREP(CASPER_LED_RED, z.subleds[0].intensity) | > + FIELD_PREP(CASPER_LED_GREEN, z.subleds[1].intensity) | > + FIELD_PREP(CASPER_LED_BLUE, z.subleds[2].intensity); > +} > + > +static enum led_brightness get_casper_brightness(struct led_classdev *led_cdev) > +{ > + struct casper_drv *drv = dev_get_drvdata(led_cdev->dev->parent); > + struct casper_wmi_args hardware_alpha = {0}; > + > + if (strcmp(led_cdev->name, zone_names[3]) == 0) > + return drv->leds[3].mc_led.led_cdev.brightness; > + > + casper_query(drv, CASPER_GET_HARDWAREINFO, &hardware_alpha); > + > + return hardware_alpha.a6; > +} > + > +static void set_casper_brightness(struct led_classdev *led_cdev, > + enum led_brightness brightness) > +{ > + u32 bright_with_mode, bright_prep, led_data, led_data_no_alpha; > + struct casper_drv *drv; > + u8 zone_to_change; > + size_t zone; > + > + drv = dev_get_drvdata(led_cdev->dev->parent); > + > + for (size_t i = 0; i < CASPER_LED_COUNT; i++) > + if (strcmp(led_cdev->name, zone_names[i]) == 0) > + zone = i; > + if (zone == 3) > + zone_to_change = CASPER_CORNER_LEDS; > + else > + zone_to_change = zone + CASPER_KEYBOARD_LED_1; > + > + led_data_no_alpha = get_zone_color(drv->leds[zone]) & ~CASPER_LED_ALPHA; > + > + bright_with_mode = brightness | LED_NORMAL; > + > + bright_prep = FIELD_PREP(CASPER_LED_ALPHA, bright_with_mode); > + led_data = bright_prep | led_data_no_alpha; > + casper_set(drv, CASPER_SET_LED, zone_to_change, led_data); > +} > + > +static int casper_platform_profile_get(struct platform_profile_handler *pprof, > + enum platform_profile_option *profile) > +{ > + struct casper_drv *drv = container_of(pprof, struct casper_drv, > + handler); > + struct casper_wmi_args ret_buff = {0}; > + int ret; > + > + ret = casper_query(drv, CASPER_POWERPLAN, &ret_buff); > + if (ret) > + return ret; > + > + if (drv->quirk_applied->new_power_scheme) { > + switch (ret_buff.a2) { > + case CASPER_NEW_HIGH_PERFORMANCE: > + *profile = PLATFORM_PROFILE_PERFORMANCE; > + break; > + case CASPER_NEW_GAMING: > + *profile = PLATFORM_PROFILE_BALANCED; > + break; > + case CASPER_NEW_AUDIO: > + *profile = PLATFORM_PROFILE_LOW_POWER; > + break; > + default: > + return -EINVAL; > + } > + return 0; > + } > + > + switch (ret_buff.a2) { > + case CASPER_HIGH_PERFORMANCE: > + *profile = PLATFORM_PROFILE_PERFORMANCE; > + break; > + case CASPER_GAMING: > + *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; > + break; > + case CASPER_TEXT_MODE: > + *profile = PLATFORM_PROFILE_BALANCED; > + break; > + case CASPER_POWERSAVE: > + *profile = PLATFORM_PROFILE_LOW_POWER; > + break; > + default: > + return -EINVAL; > + } > + > + return 0; > +} > + > +static int casper_platform_profile_set(struct platform_profile_handler *pprof, > + enum platform_profile_option profile) > +{ > + struct casper_drv *drv = container_of(pprof, struct casper_drv, > + handler); > + enum casper_power_profile_old prf_old; > + enum casper_power_profile_new prf_new; > + > + if (drv->quirk_applied->new_power_scheme) { > + > + switch (profile) { > + case PLATFORM_PROFILE_PERFORMANCE: > + prf_new = CASPER_NEW_HIGH_PERFORMANCE; > + break; > + case PLATFORM_PROFILE_BALANCED: > + prf_new = CASPER_NEW_GAMING; > + break; > + case PLATFORM_PROFILE_LOW_POWER: > + prf_new = CASPER_NEW_AUDIO; > + break; > + default: > + return -EINVAL; > + } > + > + return casper_set(drv, CASPER_POWERPLAN, prf_new, 0); > + } > + > + switch (profile) { > + case PLATFORM_PROFILE_PERFORMANCE: > + prf_old = CASPER_HIGH_PERFORMANCE; > + break; > + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: > + prf_old = CASPER_GAMING; > + break; > + case PLATFORM_PROFILE_BALANCED: > + prf_old = CASPER_TEXT_MODE; > + break; > + case PLATFORM_PROFILE_LOW_POWER: > + prf_old = CASPER_POWERSAVE; > + break; > + default: > + return -EINVAL; > + } > + > + return casper_set(drv, CASPER_POWERPLAN, prf_old, 0); > +} > + > +static umode_t casper_wmi_hwmon_is_visible(const void *drvdata, > + enum hwmon_sensor_types type, > + u32 attr, int channel) > +{ > + return 0444; > +} > + > +static int casper_wmi_hwmon_read(struct device *dev, > + enum hwmon_sensor_types type, u32 attr, > + int channel, long *val) > +{ > + struct casper_drv *drv = dev_get_drvdata(dev->parent); > + struct casper_wmi_args out = { 0 }; > + int ret; > + > + ret = casper_query(drv, CASPER_GET_HARDWAREINFO, &out); > + if (ret) > + return ret; > + > + switch (channel) { > + case CASPER_FAN_CPU: > + if (drv->quirk_applied->big_endian_fans) > + *val = be16_to_cpu(*(__be16 *)&out.a4); > + else > + *val = out.a5; > + break; > + case CASPER_FAN_GPU: > + if (drv->quirk_applied->big_endian_fans) > + *val = be16_to_cpu(*(__be16 *)&out.a5); > + else > + *val = out.a5; > + break; > + } > + > + return 0; > +} > + > +static int casper_wmi_hwmon_read_string(struct device *dev, > + enum hwmon_sensor_types type, u32 attr, > + int channel, const char **str) > +{ > + if (channel == CASPER_FAN_CPU) > + *str = "cpu_fan_speed"; > + else if (channel == CASPER_FAN_GPU) > + *str = "gpu_fan_speed"; > + return 0; > +} > + > +static const struct hwmon_ops casper_wmi_hwmon_ops = { > + .is_visible = &casper_wmi_hwmon_is_visible, > + .read = &casper_wmi_hwmon_read, > + .read_string = &casper_wmi_hwmon_read_string, > +}; > + > +static const struct hwmon_channel_info *const casper_wmi_hwmon_info[] = { > + HWMON_CHANNEL_INFO(fan, > + HWMON_F_INPUT | HWMON_F_LABEL, > + HWMON_F_INPUT | HWMON_F_LABEL), > + NULL > +}; > + > +static const struct hwmon_chip_info casper_wmi_hwmon_chip_info = { > + .ops = &casper_wmi_hwmon_ops, > + .info = casper_wmi_hwmon_info, > +}; > + > +static struct casper_quirk_entry gen_older_than_11 = { > + .big_endian_fans = true, > + .new_power_scheme = false > +}; > + > +static struct casper_quirk_entry gen_newer_than_11 = { > + .big_endian_fans = false, > + .new_power_scheme = true > +}; > + > +static const struct x86_cpu_id casper_gen[] = { > + X86_MATCH_INTEL_FAM6_MODEL(KABYLAKE, &gen_older_than_11), > + X86_MATCH_INTEL_FAM6_MODEL(COMETLAKE, &gen_older_than_11), > + X86_MATCH_INTEL_FAM6_MODEL(TIGERLAKE, &gen_newer_than_11), > + X86_MATCH_INTEL_FAM6_MODEL(ALDERLAKE, &gen_newer_than_11), > + X86_MATCH_INTEL_FAM6_MODEL(RAPTORLAKE, &gen_newer_than_11), > + X86_MATCH_INTEL_FAM6_MODEL(METEORLAKE, &gen_newer_than_11), > + {} > +}; > + > +static struct casper_quirk_entry quirk_no_power_profile = { > + .no_power_profiles = true > +}; > + > +static struct casper_quirk_entry quirk_has_power_profile = { > + .no_power_profiles = false > +}; > + > +static const struct dmi_system_id casper_quirks[] = { > + { > + .ident = "CASPER EXCALIBUR G650", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G650") > + }, > + .driver_data = &quirk_no_power_profile > + }, > + { > + .ident = "CASPER EXCALIBUR G670", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G670") > + }, > + .driver_data = &quirk_no_power_profile > + }, > + { > + .ident = "CASPER EXCALIBUR G750", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G750") > + }, > + .driver_data = &quirk_no_power_profile > + }, > + { > + .ident = "CASPER EXCALIBUR G770", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G770") > + }, > + .driver_data = &quirk_has_power_profile > + }, > + { > + .ident = "CASPER EXCALIBUR G780", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G780") > + }, > + .driver_data = &quirk_has_power_profile > + }, > + { > + .ident = "CASPER EXCALIBUR G870", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G870") > + }, > + .driver_data = &quirk_has_power_profile > + }, > + { > + .ident = "CASPER EXCALIBUR G900", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G900") > + }, > + .driver_data = &quirk_has_power_profile > + }, > + { > + .ident = "CASPER EXCALIBUR G911", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, > + "CASPER BILGISAYAR SISTEMLERI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G911") > + }, > + .driver_data = &quirk_has_power_profile > + }, > + { } > +}; > + > +static void casper_pp_remove(void *data) > +{ > + platform_profile_remove(); > +} > + > +static int casper_platform_profile_register(struct casper_drv *drv) > +{ > + int ret = 0; > + > + drv->handler.profile_get = casper_platform_profile_get; > + drv->handler.profile_set = casper_platform_profile_set; > + > + set_bit(PLATFORM_PROFILE_LOW_POWER, drv->handler.choices); > + set_bit(PLATFORM_PROFILE_BALANCED, drv->handler.choices); > + if (!drv->quirk_applied->new_power_scheme) > + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, > + drv->handler.choices); > + set_bit(PLATFORM_PROFILE_PERFORMANCE, drv->handler.choices); > + > + ret = platform_profile_register(&drv->handler); > + if (ret) > + return ret; > + > + ret = devm_add_action_or_reset(&drv->wdev->dev, casper_pp_remove, > + NULL); > + if (ret) > + platform_profile_remove(); > + > + return ret; > +} > + > +static int casper_multicolor_register(struct casper_drv *drv) > +{ > + int ret = 0; > + > + drv->leds = devm_kcalloc(&drv->wdev->dev, > + CASPER_LED_COUNT, sizeof(*drv->leds), GFP_KERNEL); > + if (!drv->leds) > + return -ENOMEM; > + > + for (size_t i = 0; i < CASPER_LED_COUNT; i++) { > + drv->leds[i].subleds = devm_kcalloc(&drv->wdev->dev, 3, > + sizeof(struct mc_subled), GFP_KERNEL); > + if (!drv->leds[i].subleds) > + return -ENOMEM; > + for (size_t j = 0; j < 3; j++) { > + drv->leds[i].subleds[j] = (struct mc_subled) { > + .color_index = LED_COLOR_ID_RED + j, > + .brightness = 255, > + .intensity = 255 > + }; > + } > + drv->leds[i].mc_led = (struct led_classdev_mc){ > + .led_cdev = { > + .name = zone_names[i], > + .brightness = 0, > + .max_brightness = 2, > + .brightness_set = &set_casper_brightness, > + .brightness_get = &get_casper_brightness, > + .color = LED_COLOR_ID_MULTI, > + }, > + .num_colors = 3, > + .subled_info = drv->leds[i].subleds > + }; > + > + ret = devm_led_classdev_multicolor_register(&drv->wdev->dev, > + &drv->leds[i].mc_led); > + if (ret) > + return -ENODEV; > + } > + > + // Setting leds to the default color > + ret = casper_set(drv, CASPER_SET_LED, CASPER_ALL_KEYBOARD_LEDS, > + CASPER_DEFAULT_COLOR); > + if (ret) > + return ret; > + > + ret = casper_set(drv, CASPER_SET_LED, CASPER_CORNER_LEDS, > + CASPER_DEFAULT_COLOR); > + return ret; > +} > + > +static void casper_mutex_destroy(void *data) > +{ > + mutex_destroy((struct mutex *)data); > +} > + > +static int casper_wmi_probe(struct wmi_device *wdev, const void *context) > +{ > + struct casper_quirk_entry *pp_quirk; > + const struct dmi_system_id *dmi_id; > + const struct x86_cpu_id *gen_id; > + struct device *hwmon_dev; > + struct casper_drv *drv; > + int ret; > + > + drv = devm_kzalloc(&wdev->dev, sizeof(*drv), GFP_KERNEL); > + if (!drv) > + return -ENOMEM; > + > + drv->wdev = wdev; > + dev_set_drvdata(&wdev->dev, drv); > + > + gen_id = x86_match_cpu(casper_gen); > + if (!gen_id) > + return -ENODEV; > + > + drv->quirk_applied = (struct casper_quirk_entry *)gen_id->driver_data; > + > + dmi_id = dmi_first_match(casper_quirks); > + if (!dmi_id) > + return -ENODEV; > + > + pp_quirk = (struct casper_quirk_entry *)dmi_id->driver_data; > + drv->quirk_applied->no_power_profiles = pp_quirk->no_power_profiles; > + > + mutex_init(&drv->casper_mutex); > + ret = devm_add_action_or_reset(&wdev->dev, casper_mutex_destroy, > + &drv->casper_mutex); > + if (ret) > + return ret; > + > + ret = casper_multicolor_register(drv); > + if (ret) > + return ret; > + > + hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev, > + "casper_wmi", wdev, > + &casper_wmi_hwmon_chip_info, > + NULL); > + if (IS_ERR(hwmon_dev)) > + return PTR_ERR(hwmon_dev); > + > + if (!drv->quirk_applied->no_power_profiles) { > + ret = casper_platform_profile_register(drv); > + if (ret) > + return ret; > + } > + > + return 0; > +} > + > +static const struct wmi_device_id casper_wmi_id_table[] = { > + { CASPER_WMI_GUID, NULL }, > + { } > +}; > +MODULE_DEVICE_TABLE(wmi, casper_wmi_id_table); > + > +static struct wmi_driver casper_drv = { > + .driver = { > + .name = "casper-wmi", > + }, > + .id_table = casper_wmi_id_table, > + .probe = casper_wmi_probe, > + .no_singleton = true, > +}; > + > +module_wmi_driver(casper_drv); > + > +MODULE_AUTHOR("Mustafa Ekşi <mustafa.eskieksi@gmail.com>"); > +MODULE_DESCRIPTION("Casper Excalibur Laptop WMI driver"); > +MODULE_LICENSE("GPL");
diff --git a/MAINTAINERS b/MAINTAINERS index a48ddea7b9b..13844ad3d12 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4907,6 +4907,12 @@ S: Maintained W: https://wireless.wiki.kernel.org/en/users/Drivers/carl9170 F: drivers/net/wireless/ath/carl9170/ +CASPER EXCALIBUR WMI DRIVER +M: Mustafa Ekşi <mustafa.eskieksi@gmail.com> +L: platform-driver-x86@vger.kernel.org +S: Maintained +F: drivers/platform/x86/casper-wmi.c + CAVIUM I2C DRIVER M: Robert Richter <rric@kernel.org> S: Odd Fixes diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 665fa952498..7560d90ce75 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -1182,6 +1182,20 @@ config SEL3350_PLATFORM To compile this driver as a module, choose M here: the module will be called sel3350-platform. +config CASPER_WMI + tristate "Casper Excalibur Laptop WMI driver" + depends on ACPI_WMI + depends on HWMON + depends on LEDS_CLASS_MULTICOLOR + select ACPI_PLATFORM_PROFILE + help + Say Y here if you want to support WMI-based fan speed reporting, + power management and keyboard backlight support on Casper Excalibur + Laptops. + + To compile this driver as a module, choose M here: the module will + be called casper-wmi. + endif # X86_PLATFORM_DEVICES config P2SB diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index e1b14294706..639509f9afa 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -14,6 +14,7 @@ obj-$(CONFIG_MXM_WMI) += mxm-wmi.o obj-$(CONFIG_NVIDIA_WMI_EC_BACKLIGHT) += nvidia-wmi-ec-backlight.o obj-$(CONFIG_XIAOMI_WMI) += xiaomi-wmi.o obj-$(CONFIG_GIGABYTE_WMI) += gigabyte-wmi.o +obj-$(CONFIG_CASPER_WMI) += casper-wmi.o # Acer obj-$(CONFIG_ACERHDF) += acerhdf.o diff --git a/drivers/platform/x86/casper-wmi.c b/drivers/platform/x86/casper-wmi.c new file mode 100644 index 00000000000..51981e591ee --- /dev/null +++ b/drivers/platform/x86/casper-wmi.c @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <linux/module.h> +#include <linux/bits.h> +#include <linux/bitops.h> +#include <linux/acpi.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include <linux/wmi.h> +#include <linux/device.h> +#include <linux/hwmon.h> +#include <linux/sysfs.h> +#include <linux/types.h> +#include <acpi/acexcep.h> +#include <linux/bitfield.h> +#include <linux/platform_profile.h> +#include <linux/led-class-multicolor.h> +#include <linux/mutex_types.h> +#include <linux/err.h> +#include <linux/mutex.h> +#include <linux/container_of.h> + +#include <linux/dmi.h> +#include <asm/cpu_device_id.h> +#include <asm/intel-family.h> + +#define CASPER_WMI_GUID "644C5791-B7B0-4123-A90B-E93876E0DAAD" + +#define CASPER_READ 0xfa00 +#define CASPER_WRITE 0xfb00 +#define CASPER_GET_HARDWAREINFO 0x0200 +#define CASPER_SET_LED 0x0100 +#define CASPER_POWERPLAN 0x0300 + +#define CASPER_KEYBOARD_LED_1 0x03 +#define CASPER_KEYBOARD_LED_2 0x04 +#define CASPER_KEYBOARD_LED_3 0x05 +#define CASPER_ALL_KEYBOARD_LEDS 0x06 +#define CASPER_CORNER_LEDS 0x07 +#define CASPER_LED_COUNT 4 + +static const char * const zone_names[CASPER_LED_COUNT] = { + "casper:rgb:kbd_zoned_backlight-right", + "casper:rgb:kbd_zoned_backlight-middle", + "casper:rgb:kbd_zoned_backlight-left", + "casper:rgb:backlight", +}; + +#define CASPER_LED_ALPHA GENMASK(31, 24) +#define CASPER_LED_RED GENMASK(23, 16) +#define CASPER_LED_GREEN GENMASK(15, 8) +#define CASPER_LED_BLUE GENMASK(7, 0) +#define CASPER_DEFAULT_COLOR (CASPER_LED_RED | CASPER_LED_GREEN | \ + CASPER_LED_BLUE) +#define CASPER_FAN_CPU 0 +#define CASPER_FAN_GPU 1 + +enum casper_power_profile_old { + CASPER_HIGH_PERFORMANCE = 1, + CASPER_GAMING = 2, + CASPER_TEXT_MODE = 3, + CASPER_POWERSAVE = 4 +}; + +enum casper_power_profile_new { + CASPER_NEW_HIGH_PERFORMANCE = 0, + CASPER_NEW_GAMING = 1, + CASPER_NEW_AUDIO = 2 +}; + +struct casper_quirk_entry { + bool big_endian_fans; + bool no_power_profiles; + bool new_power_scheme; +}; + +struct casper_fourzone_led { + struct led_classdev_mc mc_led; + struct mc_subled *subleds; +}; + +struct casper_drv { + struct platform_profile_handler handler; + struct mutex casper_mutex; + struct casper_fourzone_led *leds; + struct wmi_device *wdev; + struct casper_quirk_entry *quirk_applied; +}; + +struct casper_wmi_args { + u16 a0, a1; + u32 a2, a3, a4, a5, a6, a7, a8; +}; + +enum casper_led_mode { + LED_NORMAL = 0x10, + LED_BLINK = 0x20, + LED_FADE = 0x30, + LED_HEARTBEAT = 0x40, + LED_REPEAT = 0x50, + LED_RANDOM = 0x60 +}; + +static int casper_set(struct casper_drv *drv, u16 a1, u8 led_id, u32 data) +{ + struct casper_wmi_args wmi_args; + struct acpi_buffer input; + acpi_status status = 0; + int ret = 0; + + wmi_args = (struct casper_wmi_args) { + .a0 = CASPER_WRITE, + .a1 = a1, + .a2 = led_id, + .a3 = data + }; + + input = (struct acpi_buffer) { + (acpi_size) sizeof(struct casper_wmi_args), + &wmi_args + }; + + mutex_lock(&drv->casper_mutex); + + status = wmidev_block_set(drv->wdev, 0, &input); + if (ACPI_FAILURE(status)) + ret = -EIO; + + mutex_unlock(&drv->casper_mutex); + return ret; +} + +static int casper_query(struct casper_drv *drv, u16 a1, + struct casper_wmi_args *out) +{ + struct casper_wmi_args wmi_args; + struct acpi_buffer input; + union acpi_object *obj; + acpi_status status = 0; + int ret = 0; + + wmi_args = (struct casper_wmi_args) { + .a0 = CASPER_READ, + .a1 = a1 + }; + input = (struct acpi_buffer) { + (acpi_size) sizeof(struct casper_wmi_args), + &wmi_args + }; + + mutex_lock(&drv->casper_mutex); + + status = wmidev_block_set(drv->wdev, 0, &input); + if (ACPI_FAILURE(status)) { + ret = -EIO; + goto unlock; + } + + obj = wmidev_block_query(drv->wdev, 0); + if (!obj) { + ret = -EIO; + goto unlock; + } + + if (obj->type != ACPI_TYPE_BUFFER) { // obj will be 0x10 on failure + ret = -EINVAL; + goto freeobj; + } + if (obj->buffer.length != sizeof(struct casper_wmi_args)) { + ret = -EIO; + goto freeobj; + } + + memcpy(out, obj->buffer.pointer, sizeof(struct casper_wmi_args)); + +freeobj: + kfree(obj); +unlock: + mutex_unlock(&drv->casper_mutex); + return ret; +} + +static u32 get_zone_color(struct casper_fourzone_led z) +{ + return FIELD_PREP(CASPER_LED_RED, z.subleds[0].intensity) | + FIELD_PREP(CASPER_LED_GREEN, z.subleds[1].intensity) | + FIELD_PREP(CASPER_LED_BLUE, z.subleds[2].intensity); +} + +static enum led_brightness get_casper_brightness(struct led_classdev *led_cdev) +{ + struct casper_drv *drv = dev_get_drvdata(led_cdev->dev->parent); + struct casper_wmi_args hardware_alpha = {0}; + + if (strcmp(led_cdev->name, zone_names[3]) == 0) + return drv->leds[3].mc_led.led_cdev.brightness; + + casper_query(drv, CASPER_GET_HARDWAREINFO, &hardware_alpha); + + return hardware_alpha.a6; +} + +static void set_casper_brightness(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + u32 bright_with_mode, bright_prep, led_data, led_data_no_alpha; + struct casper_drv *drv; + u8 zone_to_change; + size_t zone; + + drv = dev_get_drvdata(led_cdev->dev->parent); + + for (size_t i = 0; i < CASPER_LED_COUNT; i++) + if (strcmp(led_cdev->name, zone_names[i]) == 0) + zone = i; + if (zone == 3) + zone_to_change = CASPER_CORNER_LEDS; + else + zone_to_change = zone + CASPER_KEYBOARD_LED_1; + + led_data_no_alpha = get_zone_color(drv->leds[zone]) & ~CASPER_LED_ALPHA; + + bright_with_mode = brightness | LED_NORMAL; + + bright_prep = FIELD_PREP(CASPER_LED_ALPHA, bright_with_mode); + led_data = bright_prep | led_data_no_alpha; + casper_set(drv, CASPER_SET_LED, zone_to_change, led_data); +} + +static int casper_platform_profile_get(struct platform_profile_handler *pprof, + enum platform_profile_option *profile) +{ + struct casper_drv *drv = container_of(pprof, struct casper_drv, + handler); + struct casper_wmi_args ret_buff = {0}; + int ret; + + ret = casper_query(drv, CASPER_POWERPLAN, &ret_buff); + if (ret) + return ret; + + if (drv->quirk_applied->new_power_scheme) { + switch (ret_buff.a2) { + case CASPER_NEW_HIGH_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case CASPER_NEW_GAMING: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case CASPER_NEW_AUDIO: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + default: + return -EINVAL; + } + return 0; + } + + switch (ret_buff.a2) { + case CASPER_HIGH_PERFORMANCE: + *profile = PLATFORM_PROFILE_PERFORMANCE; + break; + case CASPER_GAMING: + *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; + break; + case CASPER_TEXT_MODE: + *profile = PLATFORM_PROFILE_BALANCED; + break; + case CASPER_POWERSAVE: + *profile = PLATFORM_PROFILE_LOW_POWER; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int casper_platform_profile_set(struct platform_profile_handler *pprof, + enum platform_profile_option profile) +{ + struct casper_drv *drv = container_of(pprof, struct casper_drv, + handler); + enum casper_power_profile_old prf_old; + enum casper_power_profile_new prf_new; + + if (drv->quirk_applied->new_power_scheme) { + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + prf_new = CASPER_NEW_HIGH_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED: + prf_new = CASPER_NEW_GAMING; + break; + case PLATFORM_PROFILE_LOW_POWER: + prf_new = CASPER_NEW_AUDIO; + break; + default: + return -EINVAL; + } + + return casper_set(drv, CASPER_POWERPLAN, prf_new, 0); + } + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + prf_old = CASPER_HIGH_PERFORMANCE; + break; + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + prf_old = CASPER_GAMING; + break; + case PLATFORM_PROFILE_BALANCED: + prf_old = CASPER_TEXT_MODE; + break; + case PLATFORM_PROFILE_LOW_POWER: + prf_old = CASPER_POWERSAVE; + break; + default: + return -EINVAL; + } + + return casper_set(drv, CASPER_POWERPLAN, prf_old, 0); +} + +static umode_t casper_wmi_hwmon_is_visible(const void *drvdata, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return 0444; +} + +static int casper_wmi_hwmon_read(struct device *dev, + enum hwmon_sensor_types type, u32 attr, + int channel, long *val) +{ + struct casper_drv *drv = dev_get_drvdata(dev->parent); + struct casper_wmi_args out = { 0 }; + int ret; + + ret = casper_query(drv, CASPER_GET_HARDWAREINFO, &out); + if (ret) + return ret; + + switch (channel) { + case CASPER_FAN_CPU: + if (drv->quirk_applied->big_endian_fans) + *val = be16_to_cpu(*(__be16 *)&out.a4); + else + *val = out.a5; + break; + case CASPER_FAN_GPU: + if (drv->quirk_applied->big_endian_fans) + *val = be16_to_cpu(*(__be16 *)&out.a5); + else + *val = out.a5; + break; + } + + return 0; +} + +static int casper_wmi_hwmon_read_string(struct device *dev, + enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + if (channel == CASPER_FAN_CPU) + *str = "cpu_fan_speed"; + else if (channel == CASPER_FAN_GPU) + *str = "gpu_fan_speed"; + return 0; +} + +static const struct hwmon_ops casper_wmi_hwmon_ops = { + .is_visible = &casper_wmi_hwmon_is_visible, + .read = &casper_wmi_hwmon_read, + .read_string = &casper_wmi_hwmon_read_string, +}; + +static const struct hwmon_channel_info *const casper_wmi_hwmon_info[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + NULL +}; + +static const struct hwmon_chip_info casper_wmi_hwmon_chip_info = { + .ops = &casper_wmi_hwmon_ops, + .info = casper_wmi_hwmon_info, +}; + +static struct casper_quirk_entry gen_older_than_11 = { + .big_endian_fans = true, + .new_power_scheme = false +}; + +static struct casper_quirk_entry gen_newer_than_11 = { + .big_endian_fans = false, + .new_power_scheme = true +}; + +static const struct x86_cpu_id casper_gen[] = { + X86_MATCH_INTEL_FAM6_MODEL(KABYLAKE, &gen_older_than_11), + X86_MATCH_INTEL_FAM6_MODEL(COMETLAKE, &gen_older_than_11), + X86_MATCH_INTEL_FAM6_MODEL(TIGERLAKE, &gen_newer_than_11), + X86_MATCH_INTEL_FAM6_MODEL(ALDERLAKE, &gen_newer_than_11), + X86_MATCH_INTEL_FAM6_MODEL(RAPTORLAKE, &gen_newer_than_11), + X86_MATCH_INTEL_FAM6_MODEL(METEORLAKE, &gen_newer_than_11), + {} +}; + +static struct casper_quirk_entry quirk_no_power_profile = { + .no_power_profiles = true +}; + +static struct casper_quirk_entry quirk_has_power_profile = { + .no_power_profiles = false +}; + +static const struct dmi_system_id casper_quirks[] = { + { + .ident = "CASPER EXCALIBUR G650", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G650") + }, + .driver_data = &quirk_no_power_profile + }, + { + .ident = "CASPER EXCALIBUR G670", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G670") + }, + .driver_data = &quirk_no_power_profile + }, + { + .ident = "CASPER EXCALIBUR G750", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G750") + }, + .driver_data = &quirk_no_power_profile + }, + { + .ident = "CASPER EXCALIBUR G770", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G770") + }, + .driver_data = &quirk_has_power_profile + }, + { + .ident = "CASPER EXCALIBUR G780", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G780") + }, + .driver_data = &quirk_has_power_profile + }, + { + .ident = "CASPER EXCALIBUR G870", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G870") + }, + .driver_data = &quirk_has_power_profile + }, + { + .ident = "CASPER EXCALIBUR G900", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G900") + }, + .driver_data = &quirk_has_power_profile + }, + { + .ident = "CASPER EXCALIBUR G911", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, + "CASPER BILGISAYAR SISTEMLERI"), + DMI_MATCH(DMI_PRODUCT_NAME, "EXCALIBUR G911") + }, + .driver_data = &quirk_has_power_profile + }, + { } +}; + +static void casper_pp_remove(void *data) +{ + platform_profile_remove(); +} + +static int casper_platform_profile_register(struct casper_drv *drv) +{ + int ret = 0; + + drv->handler.profile_get = casper_platform_profile_get; + drv->handler.profile_set = casper_platform_profile_set; + + set_bit(PLATFORM_PROFILE_LOW_POWER, drv->handler.choices); + set_bit(PLATFORM_PROFILE_BALANCED, drv->handler.choices); + if (!drv->quirk_applied->new_power_scheme) + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, + drv->handler.choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, drv->handler.choices); + + ret = platform_profile_register(&drv->handler); + if (ret) + return ret; + + ret = devm_add_action_or_reset(&drv->wdev->dev, casper_pp_remove, + NULL); + if (ret) + platform_profile_remove(); + + return ret; +} + +static int casper_multicolor_register(struct casper_drv *drv) +{ + int ret = 0; + + drv->leds = devm_kcalloc(&drv->wdev->dev, + CASPER_LED_COUNT, sizeof(*drv->leds), GFP_KERNEL); + if (!drv->leds) + return -ENOMEM; + + for (size_t i = 0; i < CASPER_LED_COUNT; i++) { + drv->leds[i].subleds = devm_kcalloc(&drv->wdev->dev, 3, + sizeof(struct mc_subled), GFP_KERNEL); + if (!drv->leds[i].subleds) + return -ENOMEM; + for (size_t j = 0; j < 3; j++) { + drv->leds[i].subleds[j] = (struct mc_subled) { + .color_index = LED_COLOR_ID_RED + j, + .brightness = 255, + .intensity = 255 + }; + } + drv->leds[i].mc_led = (struct led_classdev_mc){ + .led_cdev = { + .name = zone_names[i], + .brightness = 0, + .max_brightness = 2, + .brightness_set = &set_casper_brightness, + .brightness_get = &get_casper_brightness, + .color = LED_COLOR_ID_MULTI, + }, + .num_colors = 3, + .subled_info = drv->leds[i].subleds + }; + + ret = devm_led_classdev_multicolor_register(&drv->wdev->dev, + &drv->leds[i].mc_led); + if (ret) + return -ENODEV; + } + + // Setting leds to the default color + ret = casper_set(drv, CASPER_SET_LED, CASPER_ALL_KEYBOARD_LEDS, + CASPER_DEFAULT_COLOR); + if (ret) + return ret; + + ret = casper_set(drv, CASPER_SET_LED, CASPER_CORNER_LEDS, + CASPER_DEFAULT_COLOR); + return ret; +} + +static void casper_mutex_destroy(void *data) +{ + mutex_destroy((struct mutex *)data); +} + +static int casper_wmi_probe(struct wmi_device *wdev, const void *context) +{ + struct casper_quirk_entry *pp_quirk; + const struct dmi_system_id *dmi_id; + const struct x86_cpu_id *gen_id; + struct device *hwmon_dev; + struct casper_drv *drv; + int ret; + + drv = devm_kzalloc(&wdev->dev, sizeof(*drv), GFP_KERNEL); + if (!drv) + return -ENOMEM; + + drv->wdev = wdev; + dev_set_drvdata(&wdev->dev, drv); + + gen_id = x86_match_cpu(casper_gen); + if (!gen_id) + return -ENODEV; + + drv->quirk_applied = (struct casper_quirk_entry *)gen_id->driver_data; + + dmi_id = dmi_first_match(casper_quirks); + if (!dmi_id) + return -ENODEV; + + pp_quirk = (struct casper_quirk_entry *)dmi_id->driver_data; + drv->quirk_applied->no_power_profiles = pp_quirk->no_power_profiles; + + mutex_init(&drv->casper_mutex); + ret = devm_add_action_or_reset(&wdev->dev, casper_mutex_destroy, + &drv->casper_mutex); + if (ret) + return ret; + + ret = casper_multicolor_register(drv); + if (ret) + return ret; + + hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev, + "casper_wmi", wdev, + &casper_wmi_hwmon_chip_info, + NULL); + if (IS_ERR(hwmon_dev)) + return PTR_ERR(hwmon_dev); + + if (!drv->quirk_applied->no_power_profiles) { + ret = casper_platform_profile_register(drv); + if (ret) + return ret; + } + + return 0; +} + +static const struct wmi_device_id casper_wmi_id_table[] = { + { CASPER_WMI_GUID, NULL }, + { } +}; +MODULE_DEVICE_TABLE(wmi, casper_wmi_id_table); + +static struct wmi_driver casper_drv = { + .driver = { + .name = "casper-wmi", + }, + .id_table = casper_wmi_id_table, + .probe = casper_wmi_probe, + .no_singleton = true, +}; + +module_wmi_driver(casper_drv); + +MODULE_AUTHOR("Mustafa Ekşi <mustafa.eskieksi@gmail.com>"); +MODULE_DESCRIPTION("Casper Excalibur Laptop WMI driver"); +MODULE_LICENSE("GPL");
This wmi driver supports changing Casper Excalibur laptops' keyboard backlight brightness and color, reading fan speeds and changing power profiles. Multicolor led device is used for backlight, platform_profile for power management and hwmon for fan speeds. It supports both old (10th gen or older) and new (11th gen or newer) laptops. It uses x86_match_cpu to check if the laptop is old or new. Signed-off-by: Mustafa Ekşi <mustafa.eskieksi@gmail.com> --- Changes in v6: - Added "rgb" to zone names and changed kbd_zoned_backlight-corners to backlight. - Changed led structure to have 3 seperate subleds instead of one rgb subled. - Removed led_cache. - Removing platform_profile and destroying casper_mutex is managed by devm_add_action_or_reset now. - Removed casper_wmi_remove. - Reordered some variables. Changes in v5: - Added mutex_destroy to casper_wmi_probe error handling - casper_multicolor_register now sets all leds to CASPER_DEFAULT_COLOR - Some minor changes Changes in v4: - Renamed casper_driver to casper_drv - Moved all global variables to casper_drv struct. Devices access casper_drv via wdev's driver_data. - Removed struct led_cache, because only its u32 array was used. It is replaced with color_cache. - Added mutex_locks in casper_set and casper_query, so they now accept casper_drv instead of wmi_device as argument. - Changed endianess conversion in hwmon_read to something sparse doesn't complain about. - Moved registrations of multicolor leds and platform profile to their own functions. This makes casper_wmi_probe more readable. - Added .no_singleton to wmi_device. - Some minor changes. Changes in v3: - Replaced led_control attribute with multicolor led interface. - Added struct led_cache, instead of storing only last color change. - Added dmi list to prevent registering platform_profile driver in models that doesn't have this feature. - Added a x86_cpu_id to differentiate older laptops that are reporting fan speeds in big-endian. Also newer laptops have a different power profile scheme. I'm using x86_cpu_id because they don't have a difference in model names, only in cpu generations (the official driver download page makes you select your cpu's generation too). - Removed hwmon_pwm device in favor of platform_profile driver. It indirectly affects fans' speed but they also affect frequency and power consumption as well. - Replaced handwritten masks with GENMASK equivalents. - Replaced led_classdev_register with devm_led_classdev_multicolor_register. This should solve the bug where led_classdev remains registered even if casper_wmi_probe returns -ENODEV. - Removed select NEW_LEDS and LEDS_CLASS, because it creates recursive dependencies. - And some minor changes. Changes in v2: - Added masks for - Changed casper_set and casper_query returns Linux error code rather than acpi_status. - replaced complicated bit operations with FIELD_GET. - Fixed some indentation and spacing. - Broke fan speeds further. - Moved module metadata to the end of the file. --- MAINTAINERS | 6 + drivers/platform/x86/Kconfig | 14 + drivers/platform/x86/Makefile | 1 + drivers/platform/x86/casper-wmi.c | 656 ++++++++++++++++++++++++++++++ 4 files changed, 677 insertions(+) create mode 100644 drivers/platform/x86/casper-wmi.c