On Sun, 2 Nov 2025, Armin Wolf wrote: > Add a new driver for Uniwill laptops. The driver uses a ACPI > interface to talk with the embedded controller, but relies on a > ACPI WMI interface for receiving event notifications. > > The driver is reverse-engineered based on the following information: > - OEM software from intel > - https://github.com/pobrn/qc71_laptop > - https://gitlab.com/tuxedocomputers/development/packages/tuxedo-drivers > - https://github.com/tuxedocomputers/tuxedo-control-center > > The underlying EC supports various features, including hwmon sensors, > battery charge limiting, a RGB lightbar and keyboard-related controls. > > Reported-by: cyear > Closes: https://github.com/lm-sensors/lm-sensors/issues/508 > Closes: https://github.com/Wer-Wolf/uniwill-laptop/issues/3 > Tested-by: Werner Sembach > Signed-off-by: Armin Wolf > --- > .../ABI/testing/sysfs-driver-uniwill-laptop | 53 + > Documentation/wmi/devices/uniwill-laptop.rst | 198 +++ > MAINTAINERS | 10 + > drivers/platform/x86/Kconfig | 2 + > drivers/platform/x86/Makefile | 3 + > drivers/platform/x86/uniwill/Kconfig | 38 + > drivers/platform/x86/uniwill/Makefile | 8 + > drivers/platform/x86/uniwill/uniwill-acpi.c | 1550 +++++++++++++++++ > drivers/platform/x86/uniwill/uniwill-wmi.c | 92 + > drivers/platform/x86/uniwill/uniwill-wmi.h | 127 ++ > 10 files changed, 2081 insertions(+) > create mode 100644 Documentation/ABI/testing/sysfs-driver-uniwill-laptop > create mode 100644 Documentation/wmi/devices/uniwill-laptop.rst > create mode 100644 drivers/platform/x86/uniwill/Kconfig > create mode 100644 drivers/platform/x86/uniwill/Makefile > create mode 100644 drivers/platform/x86/uniwill/uniwill-acpi.c > create mode 100644 drivers/platform/x86/uniwill/uniwill-wmi.c > create mode 100644 drivers/platform/x86/uniwill/uniwill-wmi.h > > diff --git a/Documentation/ABI/testing/sysfs-driver-uniwill-laptop b/Documentation/ABI/testing/sysfs-driver-uniwill-laptop > new file mode 100644 > index 000000000000..eaeb659793d2 > --- /dev/null > +++ b/Documentation/ABI/testing/sysfs-driver-uniwill-laptop > @@ -0,0 +1,53 @@ > +What: /sys/bus/platform/devices/INOU0000:XX/fn_lock_toggle_enable > +Date: November 2025 > +KernelVersion: 6.19 > +Contact: Armin Wolf > +Description: > + Allows userspace applications to enable/disable the FN lock feature > + of the integrated keyboard by writing "1"/"0" into this file. > + > + Reading this file returns the current enable status of the FN lock functionality. > + > +What: /sys/bus/platform/devices/INOU0000:XX/super_key_toggle_enable > +Date: November 2025 > +KernelVersion: 6.19 > +Contact: Armin Wolf > +Description: > + Allows userspace applications to enable/disable the super key functionality > + of the integrated keyboard by writing "1"/"0" into this file. > + > + Reading this file returns the current enable status of the super key functionality. > + > +What: /sys/bus/platform/devices/INOU0000:XX/touchpad_toggle_enable > +Date: November 2025 > +KernelVersion: 6.19 > +Contact: Armin Wolf > +Description: > + Allows userspace applications to enable/disable the touchpad toggle functionality > + of the integrated touchpad by writing "1"/"0" into this file. > + > + Reading this file returns the current enable status of the touchpad toggle > + functionality. > + > +What: /sys/bus/platform/devices/INOU0000:XX/rainbow_animation > +Date: November 2025 > +KernelVersion: 6.19 > +Contact: Armin Wolf > +Description: > + Forces the integrated lightbar to display a rainbow animation when the machine > + is not suspended. Writing "1"/"0" into this file enables/disables this > + functionality. > + > + Reading this file returns the current status of the rainbow animation functionality. > + > +What: /sys/bus/platform/devices/INOU0000:XX/breathing_in_suspend > +Date: November 2025 > +KernelVersion: 6.19 > +Contact: Armin Wolf > +Description: > + Causes the integrated lightbar to display a breathing animation when the machine > + has been suspended and is running on AC power. Writing "1"/"0" into this file > + enables/disables this functionality. > + > + Reading this file returns the current status of the breathing animation > + functionality. > diff --git a/Documentation/wmi/devices/uniwill-laptop.rst b/Documentation/wmi/devices/uniwill-laptop.rst > new file mode 100644 > index 000000000000..e246bf293450 > --- /dev/null > +++ b/Documentation/wmi/devices/uniwill-laptop.rst > @@ -0,0 +1,198 @@ > +.. SPDX-License-Identifier: GPL-2.0-or-later > + > +======================================== > +Uniwill Notebook driver (uniwill-laptop) > +======================================== > + > +Introduction > +============ > + > +Many notebooks manufactured by Uniwill (either directly or as ODM) provide a EC interface > +for controlling various platform settings like sensors and fan control. This interface is > +used by the ``uniwill-laptop`` driver to map those features onto standard kernel interfaces. > + > +EC WMI interface description > +============================ > + > +The EC WMI interface description can be decoded from the embedded binary MOF (bmof) > +data using the `bmfdec `_ utility: > + > +:: > + > + [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), > + Description("Class used to operate methods on a ULong"), > + guid("{ABBC0F6F-8EA1-11d1-00A0-C90629100000}")] > + class AcpiTest_MULong { > + [key, read] string InstanceName; > + [read] boolean Active; > + > + [WmiMethodId(1), Implemented, read, write, Description("Return the contents of a ULong")] > + void GetULong([out, Description("Ulong Data")] uint32 Data); > + > + [WmiMethodId(2), Implemented, read, write, Description("Set the contents of a ULong")] > + void SetULong([in, Description("Ulong Data")] uint32 Data); > + > + [WmiMethodId(3), Implemented, read, write, > + Description("Generate an event containing ULong data")] > + void FireULong([in, Description("WMI requires a parameter")] uint32 Hack); > + > + [WmiMethodId(4), Implemented, read, write, Description("Get and Set the contents of a ULong")] > + void GetSetULong([in, Description("Ulong Data")] uint64 Data, > + [out, Description("Ulong Data")] uint32 Return); > + > + [WmiMethodId(5), Implemented, read, write, > + Description("Get and Set the contents of a ULong for Dollby button")] > + void GetButton([in, Description("Ulong Data")] uint64 Data, > + [out, Description("Ulong Data")] uint32 Return); > + }; > + > +Most of the WMI-related code was copied from the Windows driver samples, which unfortunately means > +that the WMI-GUID is not unique. This makes the WMI-GUID unusable for autoloading. > + > +WMI method GetULong() > +--------------------- > + > +This WMI method was copied from the Windows driver samples and has no function. > + > +WMI method SetULong() > +--------------------- > + > +This WMI method was copied from the Windows driver samples and has no function. > + > +WMI method FireULong() > +---------------------- > + > +This WMI method allows to inject a WMI event with a 32-bit payload. Its primary purpose seems > +to be debugging. > + > +WMI method GetSetULong() > +------------------------ > + > +This WMI method is used to communicate with the EC. The ``Data`` argument holds the following > +information (starting with the least significant byte): > + > +1. 16-bit address > +2. 16-bit data (set to ``0x0000`` when reading) > +3. 16-bit operation (``0x0100`` for reading and ``0x0000`` for writing) > +4. 16-bit reserved (set to ``0x0000``) > + > +The first 8 bits of the ``Return`` value contain the data returned by the EC when reading. > +The special value ``0xFEFEFEFE`` is used to indicate a communication failure with the EC. > + > +WMI method GetButton() > +---------------------- > + > +This WMI method is not implemented on all machines and has an unknown purpose. > + > +Reverse-Engineering the EC WMI interface > +======================================== > + > +.. warning:: Randomly poking the EC can potentially cause damage to the machine and other unwanted > + side effects, please be careful. > + > +The EC behind the ``GetSetULong`` method is used by the OEM software supplied by the manufacturer. > +Reverse-engineering of this software is difficult since it uses an obfuscator, however some parts > +are not obfuscated. In this case `dnSpy `_ could also be helpful. > + > +The EC can be accessed under Windows using powershell (requires admin privileges): > + > +:: > + > + > $obj = Get-CimInstance -Namespace root/wmi -ClassName AcpiTest_MULong | Select-Object -First 1 > + > Invoke-CimMethod -InputObject $obj -MethodName GetSetULong -Arguments @{Data = } > + > +WMI event interface description > +=============================== > + > +The WMI interface description can also be decoded from the embedded binary MOF (bmof) > +data: > + > +:: > + > + [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), > + Description("Class containing event generated ULong data"), > + guid("{ABBC0F72-8EA1-11d1-00A0-C90629100000}")] > + class AcpiTest_EventULong : WmiEvent { > + [key, read] string InstanceName; > + [read] boolean Active; > + > + [WmiDataId(1), read, write, Description("ULong Data")] uint32 ULong; > + }; > + > +Most of the WMI-related code was again copied from the Windows driver samples, causing this WMI > +interface to suffer from the same restrictions as the EC WMI interface described above. > + > +WMI event data > +-------------- > + > +The WMI event data contains a single 32-bit value which is used to indicate various platform events. > + > +Reverse-Engineering the Uniwill WMI event interface > +=================================================== > + > +The driver logs debug messages when receiving a WMI event. Thus enabling debug messages will be > +useful for finding unknown event codes. > + > +EC ACPI interface description > +============================= > + > +The ``INOU0000`` ACPI device is a virtual device used to access various hardware registers > +available on notebooks manufactured by Uniwill. Reading and writing those registers happens > +by calling ACPI control methods. The ``uniwill-laptop`` driver uses this device to communicate > +with the EC because the ACPI control methods are faster than the WMI methods described above. > + > +ACPI control methods used for reading registers take a single ACPI integer containing the address > +of the register to read and return a ACPI integer containing the data inside said register. ACPI > +control methods used for writing registers however take two ACPI integers, with the additional > +ACPI integer containing the data to be written into the register. Such ACPI control methods return > +nothing. > + > +System memory > +------------- > + > +System memory can be accessed with a granularity of either a single byte (``MMRB`` for reading and > +``MMWB`` for writing) or four bytes (``MMRD`` for reading and ``MMWD`` for writing). Those ACPI > +control methods are unused because they provide no benefit when compared to the native memory > +access functions provided by the kernel. > + > +EC RAM > +------ > + > +The internal RAM of the EC can be accessed with a granularity of a single byte using the ``ECRR`` > +(read) and ``ECRW`` (write) ACPI control methods, with the maximum register address being ``0xFFF``. > +The OEM software waits 6 ms after calling one of those ACPI control methods, likely to avoid > +overwhelming the EC when being connected over LPC. > + > +PCI config space > +---------------- > + > +The PCI config space can be accessed with a granularity of four bytes using the ``PCRD`` (read) and > +``PCWD`` (write) ACPI control methods. The exact address format is unknown, and poking random PCI > +devices might confuse the PCI subsystem. Because of this those ACPI control methods are not used. > + > +IO ports > +-------- > + > +IO ports can be accessed with a granularity of four bytes using the ``IORD`` (read) and ``IOWD`` > +(write) ACPI control methods. Those ACPI control methods are unused because they provide no benefit > +when compared to the native IO port access functions provided by the kernel. > + > +CMOS RAM > +-------- > + > +The CMOS RAM can be accessed with a granularity of a single byte using the ``RCMS`` (read) and > +``WCMS`` ACPI control methods. Using those ACPI methods might interfere with the native CMOS RAM > +access functions provided by the kernel due to the usage of indexed IO, so they are unused. > + > +Indexed IO > +---------- > + > +Indexed IO with IO ports with a granularity of a single byte can be performed using the ``RIOP`` > +(read) and ``WIOP`` (write) ACPI control methods. Those ACPI methods are unused because they > +provide no benifit when compared to the native IO port access functions provided by the kernel. > + > +Special thanks go to github user `pobrn` which developed the > +`qc71_laptop `_ driver on which this driver is partly based. > +The same is true for Tuxedo Computers, which developed the > +`tuxedo-drivers `_ package > +which also served as a foundation for this driver. > diff --git a/MAINTAINERS b/MAINTAINERS > index 46126ce2f968..8fce9b5e9fd7 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -26376,6 +26376,16 @@ L: linux-scsi@vger.kernel.org > S: Maintained > F: drivers/ufs/host/ufs-renesas.c > > +UNIWILL LAPTOP DRIVER > +M: Armin Wolf > +L: platform-driver-x86@vger.kernel.org > +S: Maintained > +F: Documentation/ABI/testing/sysfs-driver-uniwill-laptop > +F: Documentation/wmi/devices/uniwill-laptop.rst > +F: drivers/platform/x86/uniwill/uniwill-acpi.c > +F: drivers/platform/x86/uniwill/uniwill-wmi.c > +F: drivers/platform/x86/uniwill/uniwill-wmi.h > + > UNSORTED BLOCK IMAGES (UBI) > M: Richard Weinberger > R: Zhihao Cheng > diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig > index 46e62feeda3c..1e9b84f1098f 100644 > --- a/drivers/platform/x86/Kconfig > +++ b/drivers/platform/x86/Kconfig > @@ -74,6 +74,8 @@ config HUAWEI_WMI > To compile this driver as a module, choose M here: the module > will be called huawei-wmi. > > +source "drivers/platform/x86/uniwill/Kconfig" > + > config UV_SYSFS > tristate "Sysfs structure for UV systems" > depends on X86_UV > diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile > index c7db2a88c11a..d722e244a4a7 100644 > --- a/drivers/platform/x86/Makefile > +++ b/drivers/platform/x86/Makefile > @@ -110,6 +110,9 @@ obj-$(CONFIG_TOSHIBA_WMI) += toshiba-wmi.o > # before toshiba_acpi initializes > obj-$(CONFIG_ACPI_TOSHIBA) += toshiba_acpi.o > > +# Uniwill > +obj-y += uniwill/ > + > # Inspur > obj-$(CONFIG_INSPUR_PLATFORM_PROFILE) += inspur_platform_profile.o > > diff --git a/drivers/platform/x86/uniwill/Kconfig b/drivers/platform/x86/uniwill/Kconfig > new file mode 100644 > index 000000000000..d07cc8440188 > --- /dev/null > +++ b/drivers/platform/x86/uniwill/Kconfig > @@ -0,0 +1,38 @@ > +# SPDX-License-Identifier: GPL-2.0-or-later > +# > +# Uniwill X86 Platform Specific Drivers > +# > + > +menuconfig X86_PLATFORM_DRIVERS_UNIWILL > + bool "Uniwill X86 Platform Specific Device Drivers" > + depends on X86_PLATFORM_DEVICES > + help > + Say Y here to see options for device drivers for various > + Uniwill x86 platforms, including many OEM laptops originally > + manufactured by Uniwill. > + This option alone does not add any kernel code. > + > + If you say N, all options in this submenu will be skipped and disabled. > + > +if X86_PLATFORM_DRIVERS_UNIWILL > + > +config UNIWILL_LAPTOP > + tristate "Uniwill Laptop Extras" > + default m > + depends on ACPI > + depends on ACPI_WMI > + depends on ACPI_BATTERY > + depends on HWMON > + depends on INPUT > + depends on LEDS_CLASS_MULTICOLOR > + depends on DMI > + select REGMAP > + select INPUT_SPARSEKMAP > + help > + This driver adds support for various extra features found on Uniwill laptops, > + like the lightbar, hwmon sensors and hotkeys. It also supports many OEM laptops > + originally manufactured by Uniwill. > + > + If you have such a laptop, say Y or M here. > + > +endif > diff --git a/drivers/platform/x86/uniwill/Makefile b/drivers/platform/x86/uniwill/Makefile > new file mode 100644 > index 000000000000..05cd1747a240 > --- /dev/null > +++ b/drivers/platform/x86/uniwill/Makefile > @@ -0,0 +1,8 @@ > +# SPDX-License-Identifier: GPL-2.0-or-later > +# > +# Makefile for linux/drivers/platform/x86/uniwill > +# Uniwill X86 Platform Specific Drivers > +# > + > +obj-$(CONFIG_UNIWILL_LAPTOP) += uniwill-laptop.o > +uniwill-laptop-y := uniwill-acpi.o uniwill-wmi.o > diff --git a/drivers/platform/x86/uniwill/uniwill-acpi.c b/drivers/platform/x86/uniwill/uniwill-acpi.c > new file mode 100644 > index 000000000000..014960d16211 > --- /dev/null > +++ b/drivers/platform/x86/uniwill/uniwill-acpi.c > @@ -0,0 +1,1550 @@ > +// SPDX-License-Identifier: GPL-2.0-or-later > +/* > + * Linux driver for Uniwill notebooks. > + * > + * Special thanks go to Pőcze Barnabás, Christoffer Sandberg and Werner Sembach > + * for supporting the development of this driver either through prior work or > + * by answering questions regarding the underlying ACPI and WMI interfaces. > + * > + * Copyright (C) 2025 Armin Wolf > + */ > + > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt > + > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +#include > + > +#include "uniwill-wmi.h" > + > +#define EC_ADDR_BAT_POWER_UNIT_1 0x0400 > + > +#define EC_ADDR_BAT_POWER_UNIT_2 0x0401 > + > +#define EC_ADDR_BAT_DESIGN_CAPACITY_1 0x0402 > + > +#define EC_ADDR_BAT_DESIGN_CAPACITY_2 0x0403 > + > +#define EC_ADDR_BAT_FULL_CAPACITY_1 0x0404 > + > +#define EC_ADDR_BAT_FULL_CAPACITY_2 0x0405 > + > +#define EC_ADDR_BAT_DESIGN_VOLTAGE_1 0x0408 > + > +#define EC_ADDR_BAT_DESIGN_VOLTAGE_2 0x0409 > + > +#define EC_ADDR_BAT_STATUS_1 0x0432 > +#define BAT_DISCHARGING BIT(0) > + > +#define EC_ADDR_BAT_STATUS_2 0x0433 > + > +#define EC_ADDR_BAT_CURRENT_1 0x0434 > + > +#define EC_ADDR_BAT_CURRENT_2 0x0435 > + > +#define EC_ADDR_BAT_REMAIN_CAPACITY_1 0x0436 > + > +#define EC_ADDR_BAT_REMAIN_CAPACITY_2 0x0437 > + > +#define EC_ADDR_BAT_VOLTAGE_1 0x0438 > + > +#define EC_ADDR_BAT_VOLTAGE_2 0x0439 > + > +#define EC_ADDR_CPU_TEMP 0x043E > + > +#define EC_ADDR_GPU_TEMP 0x044F > + > +#define EC_ADDR_MAIN_FAN_RPM_1 0x0464 > + > +#define EC_ADDR_MAIN_FAN_RPM_2 0x0465 > + > +#define EC_ADDR_SECOND_FAN_RPM_1 0x046C > + > +#define EC_ADDR_SECOND_FAN_RPM_2 0x046D > + > +#define EC_ADDR_DEVICE_STATUS 0x047B > +#define WIFI_STATUS_ON BIT(7) > +/* BIT(5) is also unset depending on the rfkill state (bluetooth?) */ > + > +#define EC_ADDR_BAT_ALERT 0x0494 > + > +#define EC_ADDR_BAT_CYCLE_COUNT_1 0x04A6 > + > +#define EC_ADDR_BAT_CYCLE_COUNT_2 0x04A7 > + > +#define EC_ADDR_PROJECT_ID 0x0740 > + > +#define EC_ADDR_AP_OEM 0x0741 > +#define ENABLE_MANUAL_CTRL BIT(0) > +#define ITE_KBD_EFFECT_REACTIVE BIT(3) > +#define FAN_ABNORMAL BIT(5) > + > +#define EC_ADDR_SUPPORT_5 0x0742 > +#define FAN_TURBO_SUPPORTED BIT(4) > +#define FAN_SUPPORT BIT(5) > + > +#define EC_ADDR_CTGP_DB_CTRL 0x0743 > +#define CTGP_DB_GENERAL_ENABLE BIT(0) > +#define CTGP_DB_DB_ENABLE BIT(1) > +#define CTGP_DB_CTGP_ENABLE BIT(2) > + > +#define EC_ADDR_CTGP_OFFSET 0x0744 > + > +#define EC_ADDR_TPP_OFFSET 0x0745 > + > +#define EC_ADDR_MAX_TGP 0x0746 > + > +#define EC_ADDR_LIGHTBAR_AC_CTRL 0x0748 > +#define LIGHTBAR_APP_EXISTS BIT(0) > +#define LIGHTBAR_POWER_SAVE BIT(1) > +#define LIGHTBAR_S0_OFF BIT(2) > +#define LIGHTBAR_S3_OFF BIT(3) // Breathing animation when suspended > +#define LIGHTBAR_WELCOME BIT(7) // Rainbow animation > + > +#define EC_ADDR_LIGHTBAR_AC_RED 0x0749 > + > +#define EC_ADDR_LIGHTBAR_AC_GREEN 0x074A > + > +#define EC_ADDR_LIGHTBAR_AC_BLUE 0x074B > + > +#define EC_ADDR_BIOS_OEM 0x074E > +#define FN_LOCK_STATUS BIT(4) > + > +#define EC_ADDR_MANUAL_FAN_CTRL 0x0751 > +#define FAN_LEVEL_MASK GENMASK(2, 0) > +#define FAN_MODE_TURBO BIT(4) > +#define FAN_MODE_HIGH BIT(5) > +#define FAN_MODE_BOOST BIT(6) > +#define FAN_MODE_USER BIT(7) > + > +#define EC_ADDR_PWM_1 0x075B > + > +#define EC_ADDR_PWM_2 0x075C > + > +/* Unreliable */ > +#define EC_ADDR_SUPPORT_1 0x0765 > +#define AIRPLANE_MODE BIT(0) > +#define GPS_SWITCH BIT(1) > +#define OVERCLOCK BIT(2) > +#define MACRO_KEY BIT(3) > +#define SHORTCUT_KEY BIT(4) > +#define SUPER_KEY_LOCK BIT(5) > +#define LIGHTBAR BIT(6) > +#define FAN_BOOST BIT(7) > + > +#define EC_ADDR_SUPPORT_2 0x0766 > +#define SILENT_MODE BIT(0) > +#define USB_CHARGING BIT(1) > +#define RGB_KEYBOARD BIT(2) > +#define CHINA_MODE BIT(5) > +#define MY_BATTERY BIT(6) > + > +#define EC_ADDR_TRIGGER 0x0767 > +#define TRIGGER_SUPER_KEY_LOCK BIT(0) > +#define TRIGGER_LIGHTBAR BIT(1) > +#define TRIGGER_FAN_BOOST BIT(2) > +#define TRIGGER_SILENT_MODE BIT(3) > +#define TRIGGER_USB_CHARGING BIT(4) > +#define RGB_APPLY_COLOR BIT(5) > +#define RGB_LOGO_EFFECT BIT(6) > +#define RGB_RAINBOW_EFFECT BIT(7) > + > +#define EC_ADDR_SWITCH_STATUS 0x0768 > +#define SUPER_KEY_LOCK_STATUS BIT(0) > +#define LIGHTBAR_STATUS BIT(1) > +#define FAN_BOOST_STATUS BIT(2) > +#define MACRO_KEY_STATUS BIT(3) > +#define MY_BAT_POWER_BAT_STATUS BIT(4) > + > +#define EC_ADDR_RGB_RED 0x0769 > + > +#define EC_ADDR_RGB_GREEN 0x076A > + > +#define EC_ADDR_RGB_BLUE 0x076B > + > +#define EC_ADDR_ROMID_START 0x0770 > +#define ROMID_LENGTH 14 > + > +#define EC_ADDR_ROMID_EXTRA_1 0x077E > + > +#define EC_ADDR_ROMID_EXTRA_2 0x077F > + > +#define EC_ADDR_BIOS_OEM_2 0x0782 > +#define FAN_V2_NEW BIT(0) > +#define FAN_QKEY BIT(1) > +#define FAN_TABLE_OFFICE_MODE BIT(2) > +#define FAN_V3 BIT(3) > +#define DEFAULT_MODE BIT(4) > + > +#define EC_ADDR_PL1_SETTING 0x0783 > + > +#define EC_ADDR_PL2_SETTING 0x0784 > + > +#define EC_ADDR_PL4_SETTING 0x0785 > + > +#define EC_ADDR_FAN_DEFAULT 0x0786 > +#define FAN_CURVE_LENGTH 5 > + > +#define EC_ADDR_KBD_STATUS 0x078C > +#define KBD_WHITE_ONLY BIT(0) // ~single color > +#define KBD_SINGLE_COLOR_OFF BIT(1) > +#define KBD_TURBO_LEVEL_MASK GENMASK(3, 2) > +#define KBD_APPLY BIT(4) > +#define KBD_BRIGHTNESS GENMASK(7, 5) > + > +#define EC_ADDR_FAN_CTRL 0x078E > +#define FAN3P5 BIT(1) > +#define CHARGING_PROFILE BIT(3) > +#define UNIVERSAL_FAN_CTRL BIT(6) > + > +#define EC_ADDR_BIOS_OEM_3 0x07A3 > +#define FAN_REDUCED_DURY_CYCLE BIT(5) > +#define FAN_ALWAYS_ON BIT(6) > + > +#define EC_ADDR_BIOS_BYTE 0x07A4 > +#define FN_LOCK_SWITCH BIT(3) > + > +#define EC_ADDR_OEM_3 0x07A5 > +#define POWER_LED_MASK GENMASK(1, 0) > +#define POWER_LED_LEFT 0x00 > +#define POWER_LED_BOTH 0x01 > +#define POWER_LED_NONE 0x02 > +#define FAN_QUIET BIT(2) > +#define OVERBOOST BIT(4) > +#define HIGH_POWER BIT(7) > + > +#define EC_ADDR_OEM_4 0x07A6 > +#define OVERBOOST_DYN_TEMP_OFF BIT(1) > +#define TOUCHPAD_TOGGLE_OFF BIT(6) > + > +#define EC_ADDR_CHARGE_CTRL 0x07B9 > +#define CHARGE_CTRL_MASK GENMASK(6, 0) > +#define CHARGE_CTRL_REACHED BIT(7) > + > +#define EC_ADDR_UNIVERSAL_FAN_CTRL 0x07C5 > +#define SPLIT_TABLES BIT(7) > + > +#define EC_ADDR_AP_OEM_6 0x07C6 > +#define ENABLE_UNIVERSAL_FAN_CTRL BIT(2) > +#define BATTERY_CHARGE_FULL_OVER_24H BIT(3) > +#define BATTERY_ERM_STATUS_REACHED BIT(4) > + > +#define EC_ADDR_CHARGE_PRIO 0x07CC > +#define CHARGING_PERFORMANCE BIT(7) > + > +/* Same bits as EC_ADDR_LIGHTBAR_AC_CTRL except LIGHTBAR_S3_OFF */ > +#define EC_ADDR_LIGHTBAR_BAT_CTRL 0x07E2 > + > +#define EC_ADDR_LIGHTBAR_BAT_RED 0x07E3 > + > +#define EC_ADDR_LIGHTBAR_BAT_GREEN 0x07E4 > + > +#define EC_ADDR_LIGHTBAR_BAT_BLUE 0x07E5 > + > +#define EC_ADDR_CPU_TEMP_END_TABLE 0x0F00 > + > +#define EC_ADDR_CPU_TEMP_START_TABLE 0x0F10 > + > +#define EC_ADDR_CPU_FAN_SPEED_TABLE 0x0F20 > + > +#define EC_ADDR_GPU_TEMP_END_TABLE 0x0F30 > + > +#define EC_ADDR_GPU_TEMP_START_TABLE 0x0F40 > + > +#define EC_ADDR_GPU_FAN_SPEED_TABLE 0x0F50 > + > +/* > + * Those two registers technically allow for manual fan control, > + * but are unstable on some models and are likely not meant to > + * be used by applications as they are only accessible when using > + * the WMI interface. > + */ > +#define EC_ADDR_PWM_1_WRITEABLE 0x1804 > + > +#define EC_ADDR_PWM_2_WRITEABLE 0x1809 > + > +#define DRIVER_NAME "uniwill" > + > +/* > + * The OEM software always sleeps up to 6 ms after reading/writing EC > + * registers, so we emulate this behaviour for maximum compatibility. > + */ > +#define UNIWILL_EC_DELAY_US 6000 > + > +#define PWM_MAX 200 > +#define FAN_TABLE_LENGTH 16 > + > +#define LED_CHANNELS 3 > +#define LED_MAX_BRIGHTNESS 200 > + > +#define UNIWILL_FEATURE_FN_LOCK_TOGGLE BIT(0) > +#define UNIWILL_FEATURE_SUPER_KEY_TOGGLE BIT(1) > +#define UNIWILL_FEATURE_TOUCHPAD_TOGGLE BIT(2) > +#define UNIWILL_FEATURE_LIGHTBAR BIT(3) > +#define UNIWILL_FEATURE_BATTERY BIT(4) > +#define UNIWILL_FEATURE_HWMON BIT(5) > + > +struct uniwill_data { > + struct device *dev; > + acpi_handle handle; > + struct regmap *regmap; > + struct acpi_battery_hook hook; > + unsigned int last_charge_ctrl; > + struct mutex battery_lock; /* Protects the list of currently registered batteries */ > + unsigned int last_switch_status; > + struct mutex super_key_lock; /* Protects the toggling of the super key lock state */ > + struct list_head batteries; > + struct mutex led_lock; /* Protects writes to the lightbar registers */ > + struct led_classdev_mc led_mc_cdev; > + struct mc_subled led_mc_subled_info[LED_CHANNELS]; > + struct mutex input_lock; /* Protects input sequence during notify */ > + struct input_dev *input_device; > + struct notifier_block nb; > +}; > + > +struct uniwill_battery_entry { > + struct list_head head; > + struct power_supply *battery; > +}; > + > +static bool force; > +module_param_unsafe(force, bool, 0); > +MODULE_PARM_DESC(force, "Force loading without checking for supported devices\n"); > + > +/* Feature bitmask since the associated registers are not reliable */ > +static unsigned int supported_features; > + > +static const char * const uniwill_temp_labels[] = { > + "CPU", > + "GPU", > +}; > + > +static const char * const uniwill_fan_labels[] = { > + "Main", > + "Secondary", > +}; > + > +static const struct key_entry uniwill_keymap[] = { > + /* Reported via keyboard controller */ > + { KE_IGNORE, UNIWILL_OSD_CAPSLOCK, { KEY_CAPSLOCK }}, > + { KE_IGNORE, UNIWILL_OSD_NUMLOCK, { KEY_NUMLOCK }}, > + > + /* Reported when the user locks/unlocks the super key */ > + { KE_IGNORE, UNIWILL_OSD_SUPER_KEY_LOCK_ENABLE, { KEY_UNKNOWN }}, > + { KE_IGNORE, UNIWILL_OSD_SUPER_KEY_LOCK_DISABLE, { KEY_UNKNOWN }}, > + /* Optional, might not be reported by all devices */ > + { KE_IGNORE, UNIWILL_OSD_SUPER_KEY_LOCK_CHANGED, { KEY_UNKNOWN }}, > + > + /* Reported in manual mode when toggling the airplane mode status */ > + { KE_KEY, UNIWILL_OSD_RFKILL, { KEY_RFKILL }}, > + > + /* Reported when user wants to cycle the platform profile */ > + { KE_IGNORE, UNIWILL_OSD_PERFORMANCE_MODE_TOGGLE, { KEY_UNKNOWN }}, > + > + /* Reported when the user wants to adjust the brightness of the keyboard */ > + { KE_KEY, UNIWILL_OSD_KBDILLUMDOWN, { KEY_KBDILLUMDOWN }}, > + { KE_KEY, UNIWILL_OSD_KBDILLUMUP, { KEY_KBDILLUMUP }}, > + > + /* Reported when the user wants to toggle the microphone mute status */ > + { KE_KEY, UNIWILL_OSD_MIC_MUTE, { KEY_MICMUTE }}, > + > + /* Reported when the user locks/unlocks the Fn key */ > + { KE_IGNORE, UNIWILL_OSD_FN_LOCK, { KEY_FN_ESC }}, > + > + /* Reported when the user wants to toggle the brightness of the keyboard */ > + { KE_KEY, UNIWILL_OSD_KBDILLUMTOGGLE, { KEY_KBDILLUMTOGGLE }}, > + > + /* FIXME: find out the exact meaning of those events */ > + { KE_IGNORE, UNIWILL_OSD_BAT_CHARGE_FULL_24_H, { KEY_UNKNOWN }}, > + { KE_IGNORE, UNIWILL_OSD_BAT_ERM_UPDATE, { KEY_UNKNOWN }}, > + > + /* Reported when the user wants to toggle the benchmark mode status */ > + { KE_IGNORE, UNIWILL_OSD_BENCHMARK_MODE_TOGGLE, { KEY_UNKNOWN }}, > + > + { KE_END } > +}; > + > +static int uniwill_ec_reg_write(void *context, unsigned int reg, unsigned int val) > +{ > + union acpi_object params[2] = { > + { > + .integer = { > + .type = ACPI_TYPE_INTEGER, > + .value = reg, > + }, > + }, > + { > + .integer = { > + .type = ACPI_TYPE_INTEGER, > + .value = val, > + }, > + }, > + }; > + struct uniwill_data *data = context; > + struct acpi_object_list input = { > + .count = ARRAY_SIZE(params), > + .pointer = params, > + }; > + acpi_status status; > + > + status = acpi_evaluate_object(data->handle, "ECRW", &input, NULL); > + if (ACPI_FAILURE(status)) > + return -EIO; > + > + usleep_range(UNIWILL_EC_DELAY_US, UNIWILL_EC_DELAY_US * 2); > + > + return 0; > +} > + > +static int uniwill_ec_reg_read(void *context, unsigned int reg, unsigned int *val) > +{ > + union acpi_object params[1] = { > + { > + .integer = { > + .type = ACPI_TYPE_INTEGER, > + .value = reg, > + }, > + }, > + }; > + struct uniwill_data *data = context; > + struct acpi_object_list input = { > + .count = ARRAY_SIZE(params), > + .pointer = params, > + }; > + unsigned long long output; > + acpi_status status; > + > + status = acpi_evaluate_integer(data->handle, "ECRR", &input, &output); > + if (ACPI_FAILURE(status)) > + return -EIO; > + > + if (output > U8_MAX) > + return -ENXIO; > + > + usleep_range(UNIWILL_EC_DELAY_US, UNIWILL_EC_DELAY_US * 2); > + > + *val = output; > + > + return 0; > +} > + > +static const struct regmap_bus uniwill_ec_bus = { > + .reg_write = uniwill_ec_reg_write, > + .reg_read = uniwill_ec_reg_read, > + .reg_format_endian_default = REGMAP_ENDIAN_LITTLE, > + .val_format_endian_default = REGMAP_ENDIAN_LITTLE, > +}; > + > +static bool uniwill_writeable_reg(struct device *dev, unsigned int reg) > +{ > + switch (reg) { > + case EC_ADDR_AP_OEM: > + case EC_ADDR_LIGHTBAR_AC_CTRL: > + case EC_ADDR_LIGHTBAR_AC_RED: > + case EC_ADDR_LIGHTBAR_AC_GREEN: > + case EC_ADDR_LIGHTBAR_AC_BLUE: > + case EC_ADDR_BIOS_OEM: > + case EC_ADDR_TRIGGER: > + case EC_ADDR_OEM_4: > + case EC_ADDR_CHARGE_CTRL: > + case EC_ADDR_LIGHTBAR_BAT_CTRL: > + case EC_ADDR_LIGHTBAR_BAT_RED: > + case EC_ADDR_LIGHTBAR_BAT_GREEN: > + case EC_ADDR_LIGHTBAR_BAT_BLUE: > + return true; > + default: > + return false; > + } > +} > + > +static bool uniwill_readable_reg(struct device *dev, unsigned int reg) > +{ > + switch (reg) { > + case EC_ADDR_CPU_TEMP: > + case EC_ADDR_GPU_TEMP: > + case EC_ADDR_MAIN_FAN_RPM_1: > + case EC_ADDR_MAIN_FAN_RPM_2: > + case EC_ADDR_SECOND_FAN_RPM_1: > + case EC_ADDR_SECOND_FAN_RPM_2: > + case EC_ADDR_BAT_ALERT: > + case EC_ADDR_PROJECT_ID: > + case EC_ADDR_AP_OEM: > + case EC_ADDR_LIGHTBAR_AC_CTRL: > + case EC_ADDR_LIGHTBAR_AC_RED: > + case EC_ADDR_LIGHTBAR_AC_GREEN: > + case EC_ADDR_LIGHTBAR_AC_BLUE: > + case EC_ADDR_BIOS_OEM: > + case EC_ADDR_PWM_1: > + case EC_ADDR_PWM_2: > + case EC_ADDR_TRIGGER: > + case EC_ADDR_SWITCH_STATUS: > + case EC_ADDR_OEM_4: > + case EC_ADDR_CHARGE_CTRL: > + case EC_ADDR_LIGHTBAR_BAT_CTRL: > + case EC_ADDR_LIGHTBAR_BAT_RED: > + case EC_ADDR_LIGHTBAR_BAT_GREEN: > + case EC_ADDR_LIGHTBAR_BAT_BLUE: > + return true; > + default: > + return false; > + } > +} > + > +static bool uniwill_volatile_reg(struct device *dev, unsigned int reg) > +{ > + switch (reg) { > + case EC_ADDR_CPU_TEMP: > + case EC_ADDR_GPU_TEMP: > + case EC_ADDR_MAIN_FAN_RPM_1: > + case EC_ADDR_MAIN_FAN_RPM_2: > + case EC_ADDR_SECOND_FAN_RPM_1: > + case EC_ADDR_SECOND_FAN_RPM_2: > + case EC_ADDR_BAT_ALERT: > + case EC_ADDR_PWM_1: > + case EC_ADDR_PWM_2: > + case EC_ADDR_TRIGGER: > + case EC_ADDR_SWITCH_STATUS: > + case EC_ADDR_CHARGE_CTRL: > + return true; > + default: > + return false; > + } > +} > + > +static const struct regmap_config uniwill_ec_config = { > + .reg_bits = 16, > + .val_bits = 8, > + .writeable_reg = uniwill_writeable_reg, > + .readable_reg = uniwill_readable_reg, > + .volatile_reg = uniwill_volatile_reg, > + .can_sleep = true, > + .max_register = 0xFFF, > + .cache_type = REGCACHE_MAPLE, > + .use_single_read = true, > + .use_single_write = true, > +}; > + > +static ssize_t fn_lock_toggle_enable_store(struct device *dev, struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + bool enable; > + int ret; > + > + ret = kstrtobool(buf, &enable); > + if (ret < 0) > + return ret; > + > + if (enable) > + value = FN_LOCK_STATUS; > + else > + value = 0; > + > + ret = regmap_update_bits(data->regmap, EC_ADDR_BIOS_OEM, FN_LOCK_STATUS, value); > + if (ret < 0) > + return ret; > + > + return count; > +} > + > +static ssize_t fn_lock_toggle_enable_show(struct device *dev, struct device_attribute *attr, > + char *buf) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + int ret; > + > + ret = regmap_read(data->regmap, EC_ADDR_BIOS_OEM, &value); > + if (ret < 0) > + return ret; > + > + return sysfs_emit(buf, "%d\n", !!(value & FN_LOCK_STATUS)); > +} > + > +static DEVICE_ATTR_RW(fn_lock_toggle_enable); > + > +static ssize_t super_key_toggle_enable_store(struct device *dev, struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + bool enable; > + int ret; > + > + ret = kstrtobool(buf, &enable); > + if (ret < 0) > + return ret; > + > + guard(mutex)(&data->super_key_lock); > + > + ret = regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &value); > + if (ret < 0) > + return ret; > + > + /* > + * We can only toggle the super key lock, so we return early if the setting > + * is already in the correct state. > + */ > + if (enable == !(value & SUPER_KEY_LOCK_STATUS)) > + return count; > + > + ret = regmap_write_bits(data->regmap, EC_ADDR_TRIGGER, TRIGGER_SUPER_KEY_LOCK, > + TRIGGER_SUPER_KEY_LOCK); > + if (ret < 0) > + return ret; > + > + return count; > +} > + > +static ssize_t super_key_toggle_enable_show(struct device *dev, struct device_attribute *attr, > + char *buf) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + int ret; > + > + ret = regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &value); > + if (ret < 0) > + return ret; > + > + return sysfs_emit(buf, "%d\n", !(value & SUPER_KEY_LOCK_STATUS)); > +} > + > +static DEVICE_ATTR_RW(super_key_toggle_enable); > + > +static ssize_t touchpad_toggle_enable_store(struct device *dev, struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + bool enable; > + int ret; > + > + ret = kstrtobool(buf, &enable); > + if (ret < 0) > + return ret; > + > + if (enable) > + value = 0; > + else > + value = TOUCHPAD_TOGGLE_OFF; > + > + ret = regmap_update_bits(data->regmap, EC_ADDR_OEM_4, TOUCHPAD_TOGGLE_OFF, value); > + if (ret < 0) > + return ret; > + > + return count; > +} > + > +static ssize_t touchpad_toggle_enable_show(struct device *dev, struct device_attribute *attr, > + char *buf) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + int ret; > + > + ret = regmap_read(data->regmap, EC_ADDR_OEM_4, &value); > + if (ret < 0) > + return ret; > + > + return sysfs_emit(buf, "%d\n", !(value & TOUCHPAD_TOGGLE_OFF)); > +} > + > +static DEVICE_ATTR_RW(touchpad_toggle_enable); > + > +static ssize_t rainbow_animation_store(struct device *dev, struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + bool enable; > + int ret; > + > + ret = kstrtobool(buf, &enable); > + if (ret < 0) > + return ret; > + > + if (enable) > + value = LIGHTBAR_WELCOME; > + else > + value = 0; > + > + guard(mutex)(&data->led_lock); > + > + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_WELCOME, value); > + if (ret < 0) > + return ret; > + > + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_WELCOME, value); > + if (ret < 0) > + return ret; > + > + return count; > +} > + > +static ssize_t rainbow_animation_show(struct device *dev, struct device_attribute *attr, char *buf) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + int ret; > + > + ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value); > + if (ret < 0) > + return ret; > + > + return sysfs_emit(buf, "%d\n", !!(value & LIGHTBAR_WELCOME)); > +} > + > +static DEVICE_ATTR_RW(rainbow_animation); > + > +static ssize_t breathing_in_suspend_store(struct device *dev, struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + bool enable; > + int ret; > + > + ret = kstrtobool(buf, &enable); > + if (ret < 0) > + return ret; > + > + if (enable) > + value = 0; > + else > + value = LIGHTBAR_S3_OFF; > + > + /* We only access a single register here, so we do not need to use data->led_lock */ > + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_S3_OFF, value); > + if (ret < 0) > + return ret; > + > + return count; > +} > + > +static ssize_t breathing_in_suspend_show(struct device *dev, struct device_attribute *attr, > + char *buf) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + int ret; > + > + ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value); > + if (ret < 0) > + return ret; > + > + return sysfs_emit(buf, "%d\n", !(value & LIGHTBAR_S3_OFF)); > +} > + > +static DEVICE_ATTR_RW(breathing_in_suspend); > + > +static struct attribute *uniwill_attrs[] = { > + /* Keyboard-related */ > + &dev_attr_fn_lock_toggle_enable.attr, > + &dev_attr_super_key_toggle_enable.attr, > + &dev_attr_touchpad_toggle_enable.attr, > + /* Lightbar-related */ > + &dev_attr_rainbow_animation.attr, > + &dev_attr_breathing_in_suspend.attr, > + NULL > +}; > + > +static umode_t uniwill_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) > +{ > + if (attr == &dev_attr_fn_lock_toggle_enable.attr) { > + if (supported_features & UNIWILL_FEATURE_FN_LOCK_TOGGLE) > + return attr->mode; > + } > + > + if (attr == &dev_attr_super_key_toggle_enable.attr) { > + if (supported_features & UNIWILL_FEATURE_SUPER_KEY_TOGGLE) > + return attr->mode; > + } > + > + if (attr == &dev_attr_touchpad_toggle_enable.attr) { > + if (supported_features & UNIWILL_FEATURE_TOUCHPAD_TOGGLE) > + return attr->mode; > + } > + > + if (attr == &dev_attr_rainbow_animation.attr || > + attr == &dev_attr_breathing_in_suspend.attr) { > + if (supported_features & UNIWILL_FEATURE_LIGHTBAR) > + return attr->mode; > + } > + > + return 0; > +} > + > +static const struct attribute_group uniwill_group = { > + .is_visible = uniwill_attr_is_visible, > + .attrs = uniwill_attrs, > +}; > + > +static const struct attribute_group *uniwill_groups[] = { > + &uniwill_group, > + NULL > +}; > + > +static int uniwill_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, > + long *val) > +{ > + struct uniwill_data *data = dev_get_drvdata(dev); > + unsigned int value; > + __be16 rpm; > + int ret; > + > + switch (type) { > + case hwmon_temp: > + switch (channel) { > + case 0: > + ret = regmap_read(data->regmap, EC_ADDR_CPU_TEMP, &value); > + break; > + case 1: > + ret = regmap_read(data->regmap, EC_ADDR_GPU_TEMP, &value); > + break; > + default: > + return -EOPNOTSUPP; > + } > + > + if (ret < 0) > + return ret; > + > + *val = value * MILLIDEGREE_PER_DEGREE; > + return 0; > + case hwmon_fan: > + switch (channel) { > + case 0: > + ret = regmap_bulk_read(data->regmap, EC_ADDR_MAIN_FAN_RPM_1, &rpm, > + sizeof(rpm)); > + break; > + case 1: > + ret = regmap_bulk_read(data->regmap, EC_ADDR_SECOND_FAN_RPM_1, &rpm, > + sizeof(rpm)); > + break; > + default: > + return -EOPNOTSUPP; > + } > + > + if (ret < 0) > + return ret; > + > + *val = be16_to_cpu(rpm); > + return 0; > + case hwmon_pwm: > + switch (channel) { > + case 0: > + ret = regmap_read(data->regmap, EC_ADDR_PWM_1, &value); > + break; > + case 1: > + ret = regmap_read(data->regmap, EC_ADDR_PWM_2, &value); > + break; > + default: > + return -EOPNOTSUPP; > + } > + > + if (ret < 0) > + return ret; > + > + *val = fixp_linear_interpolate(0, 0, PWM_MAX, U8_MAX, value); > + return 0; > + default: > + return -EOPNOTSUPP; > + } > +} > + > +static int uniwill_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, > + int channel, const char **str) > +{ > + switch (type) { > + case hwmon_temp: > + *str = uniwill_temp_labels[channel]; > + return 0; > + case hwmon_fan: > + *str = uniwill_fan_labels[channel]; > + return 0; > + default: > + return -EOPNOTSUPP; > + } > +} > + > +static const struct hwmon_ops uniwill_ops = { > + .visible = 0444, > + .read = uniwill_read, > + .read_string = uniwill_read_string, > +}; > + > +static const struct hwmon_channel_info * const uniwill_info[] = { > + HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ), > + HWMON_CHANNEL_INFO(temp, > + HWMON_T_INPUT | HWMON_T_LABEL, > + HWMON_T_INPUT | HWMON_T_LABEL), > + HWMON_CHANNEL_INFO(fan, > + HWMON_F_INPUT | HWMON_F_LABEL, > + HWMON_F_INPUT | HWMON_F_LABEL), > + HWMON_CHANNEL_INFO(pwm, > + HWMON_PWM_INPUT, > + HWMON_PWM_INPUT), > + NULL > +}; > + > +static const struct hwmon_chip_info uniwill_chip_info = { > + .ops = &uniwill_ops, > + .info = uniwill_info, > +}; > + > +static int uniwill_hwmon_init(struct uniwill_data *data) > +{ > + struct device *hdev; > + > + if (!(supported_features & UNIWILL_FEATURE_HWMON)) > + return 0; > + > + hdev = devm_hwmon_device_register_with_info(data->dev, "uniwill", data, > + &uniwill_chip_info, NULL); > + > + return PTR_ERR_OR_ZERO(hdev); > +} > + > +static const unsigned int uniwill_led_channel_to_bat_reg[LED_CHANNELS] = { > + EC_ADDR_LIGHTBAR_BAT_RED, > + EC_ADDR_LIGHTBAR_BAT_GREEN, > + EC_ADDR_LIGHTBAR_BAT_BLUE, > +}; > + > +static const unsigned int uniwill_led_channel_to_ac_reg[LED_CHANNELS] = { > + EC_ADDR_LIGHTBAR_AC_RED, > + EC_ADDR_LIGHTBAR_AC_GREEN, > + EC_ADDR_LIGHTBAR_AC_BLUE, > +}; > + > +static int uniwill_led_brightness_set(struct led_classdev *led_cdev, enum led_brightness brightness) > +{ > + struct led_classdev_mc *led_mc_cdev = lcdev_to_mccdev(led_cdev); > + struct uniwill_data *data = container_of(led_mc_cdev, struct uniwill_data, led_mc_cdev); > + unsigned int value; > + int ret; > + > + ret = led_mc_calc_color_components(led_mc_cdev, brightness); > + if (ret < 0) > + return ret; > + > + guard(mutex)(&data->led_lock); > + > + for (int i = 0; i < LED_CHANNELS; i++) { > + /* Prevent the brightness values from overflowing */ > + value = min(LED_MAX_BRIGHTNESS, data->led_mc_subled_info[i].brightness); > + ret = regmap_write(data->regmap, uniwill_led_channel_to_ac_reg[i], value); > + if (ret < 0) > + return ret; > + > + ret = regmap_write(data->regmap, uniwill_led_channel_to_bat_reg[i], value); > + if (ret < 0) > + return ret; > + } > + > + if (brightness) > + value = 0; > + else > + value = LIGHTBAR_S0_OFF; > + > + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_S0_OFF, value); > + if (ret < 0) > + return ret; > + > + return regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_S0_OFF, value); > +} > + > +#define LIGHTBAR_MASK (LIGHTBAR_APP_EXISTS | LIGHTBAR_S0_OFF | LIGHTBAR_S3_OFF | LIGHTBAR_WELCOME) > + > +static int uniwill_led_init(struct uniwill_data *data) > +{ > + struct led_init_data init_data = { > + .devicename = DRIVER_NAME, > + .default_label = "multicolor:" LED_FUNCTION_STATUS, > + .devname_mandatory = true, > + }; > + unsigned int color_indices[3] = { > + LED_COLOR_ID_RED, > + LED_COLOR_ID_GREEN, > + LED_COLOR_ID_BLUE, > + }; > + unsigned int value; > + int ret; > + > + if (!(supported_features & UNIWILL_FEATURE_LIGHTBAR)) > + return 0; > + > + ret = devm_mutex_init(data->dev, &data->led_lock); > + if (ret < 0) > + return ret; > + > + /* > + * The EC has separate lightbar settings for AC and battery mode, > + * so we have to ensure that both settings are the same. > + */ > + ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value); > + if (ret < 0) > + return ret; > + > + value |= LIGHTBAR_APP_EXISTS; > + ret = regmap_write(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, value); > + if (ret < 0) > + return ret; > + > + /* > + * The breathing animation during suspend is not supported when > + * running on battery power. > + */ > + value |= LIGHTBAR_S3_OFF; > + ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_MASK, value); > + if (ret < 0) > + return ret; > + > + data->led_mc_cdev.led_cdev.color = LED_COLOR_ID_MULTI; > + data->led_mc_cdev.led_cdev.max_brightness = LED_MAX_BRIGHTNESS; > + data->led_mc_cdev.led_cdev.flags = LED_REJECT_NAME_CONFLICT; > + data->led_mc_cdev.led_cdev.brightness_set_blocking = uniwill_led_brightness_set; > + > + if (value & LIGHTBAR_S0_OFF) > + data->led_mc_cdev.led_cdev.brightness = 0; > + else > + data->led_mc_cdev.led_cdev.brightness = LED_MAX_BRIGHTNESS; > + > + for (int i = 0; i < LED_CHANNELS; i++) { > + data->led_mc_subled_info[i].color_index = color_indices[i]; > + > + ret = regmap_read(data->regmap, uniwill_led_channel_to_ac_reg[i], &value); > + if (ret < 0) > + return ret; > + > + /* > + * Make sure that the initial intensity value is not greater than > + * the maximum brightness. > + */ > + value = min(LED_MAX_BRIGHTNESS, value); > + ret = regmap_write(data->regmap, uniwill_led_channel_to_ac_reg[i], value); > + if (ret < 0) > + return ret; > + > + ret = regmap_write(data->regmap, uniwill_led_channel_to_bat_reg[i], value); > + if (ret < 0) > + return ret; > + > + data->led_mc_subled_info[i].intensity = value; > + data->led_mc_subled_info[i].channel = i; > + } > + > + data->led_mc_cdev.subled_info = data->led_mc_subled_info; > + data->led_mc_cdev.num_colors = LED_CHANNELS; > + > + return devm_led_classdev_multicolor_register_ext(data->dev, &data->led_mc_cdev, > + &init_data); > +} > + > +static int uniwill_get_property(struct power_supply *psy, const struct power_supply_ext *ext, > + void *drvdata, enum power_supply_property psp, > + union power_supply_propval *val) > +{ > + struct uniwill_data *data = drvdata; > + union power_supply_propval prop; > + unsigned int regval; > + int ret; > + > + switch (psp) { > + case POWER_SUPPLY_PROP_HEALTH: > + ret = power_supply_get_property_direct(psy, POWER_SUPPLY_PROP_PRESENT, &prop); > + if (ret < 0) > + return ret; > + > + if (!prop.intval) { > + val->intval = POWER_SUPPLY_HEALTH_NO_BATTERY; > + return 0; > + } > + > + ret = power_supply_get_property_direct(psy, POWER_SUPPLY_PROP_STATUS, &prop); > + if (ret < 0) > + return ret; > + > + if (prop.intval == POWER_SUPPLY_STATUS_UNKNOWN) { > + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; > + return 0; > + } > + > + ret = regmap_read(data->regmap, EC_ADDR_BAT_ALERT, ®val); > + if (ret < 0) > + return ret; > + > + if (regval) { > + /* Charging issue */ > + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; > + return 0; > + } > + > + val->intval = POWER_SUPPLY_HEALTH_GOOD; > + return 0; > + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: > + ret = regmap_read(data->regmap, EC_ADDR_CHARGE_CTRL, ®val); > + if (ret < 0) > + return ret; > + > + val->intval = clamp_val(FIELD_GET(CHARGE_CTRL_MASK, regval), 0, 100); Hmm, this seems to trigger an error from sparse: CHECK drivers/platform/x86/uniwill/uniwill-acpi.c drivers/platform/x86/uniwill/uniwill-acpi.c:1125:31: error: too long token expansion I guess they do some crazy type validation inside those which expands like crazy. Based on the message and the code, it looks non-error though, just some stupid limitation perhaps. > + return 0; > + default: > + return -EINVAL; > + } > +} -- i.