Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,278 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Lenovo X86 Platform Specific Drivers
#
config IDEAPAD_LAPTOP
tristate "Lenovo IdeaPad Laptop Extras"
depends on ACPI
depends on ACPI_BATTERY
depends on RFKILL && INPUT
depends on SERIO_I8042
depends on BACKLIGHT_CLASS_DEVICE
depends on ACPI_VIDEO || ACPI_VIDEO = n
depends on ACPI_WMI || ACPI_WMI = n
select ACPI_PLATFORM_PROFILE
select INPUT_SPARSEKMAP
select NEW_LEDS
select LEDS_CLASS
help
This is a driver for Lenovo IdeaPad netbooks contains drivers for
rfkill switch, hotkey, fan control and backlight control.
config LENOVO_WMI_HOTKEY_UTILITIES
tristate "Lenovo Hotkey Utility WMI extras driver"
depends on ACPI_WMI
select NEW_LEDS
select LEDS_CLASS
imply IDEAPAD_LAPTOP
help
This driver provides WMI support for Lenovo customized hotkeys function,
such as LED control for audio/mic mute event for Ideapad, YOGA, XiaoXin,
Gaming, ThinkBook and so on.
config LENOVO_WMI_CAMERA
tristate "Lenovo WMI Camera Button driver"
depends on ACPI_WMI
depends on INPUT
help
This driver provides support for Lenovo camera button. The Camera
button is a GPIO device. This driver receives ACPI notifications when
the camera button is switched on/off.
To compile this driver as a module, choose M here: the module
will be called lenovo-wmi-camera.
config LENOVO_YMC
tristate "Lenovo Yoga Tablet Mode Control"
depends on ACPI_WMI
depends on INPUT
depends on IDEAPAD_LAPTOP
select INPUT_SPARSEKMAP
help
This driver maps the Tablet Mode Control switch to SW_TABLET_MODE input
events for Lenovo Yoga notebooks.
config THINKPAD_ACPI
tristate "ThinkPad ACPI Laptop Extras"
depends on ACPI_EC
depends on ACPI_BATTERY
depends on INPUT
depends on RFKILL || RFKILL = n
depends on ACPI_VIDEO || ACPI_VIDEO = n
depends on BACKLIGHT_CLASS_DEVICE
depends on I2C
depends on DRM
select ACPI_PLATFORM_PROFILE
select DRM_PRIVACY_SCREEN
select HWMON
select NVRAM
select NEW_LEDS
select LEDS_CLASS
select INPUT_SPARSEKMAP
help
This is a driver for the IBM and Lenovo ThinkPad laptops. It adds
support for Fn-Fx key combinations, Bluetooth control, video
output switching, ThinkLight control, UltraBay eject and more.
For more information about this driver see
<file:Documentation/admin-guide/laptops/thinkpad-acpi.rst> and
<http://ibm-acpi.sf.net/> .
This driver was formerly known as ibm-acpi.
Extra functionality will be available if the rfkill (CONFIG_RFKILL)
and/or ALSA (CONFIG_SND) subsystems are available in the kernel.
Note that if you want ThinkPad-ACPI to be built-in instead of
modular, ALSA and rfkill will also have to be built-in.
If you have an IBM or Lenovo ThinkPad laptop, say Y or M here.
config THINKPAD_ACPI_ALSA_SUPPORT
bool "Console audio control ALSA interface"
depends on THINKPAD_ACPI
depends on SND
depends on SND = y || THINKPAD_ACPI = SND
default y
help
Enables monitoring of the built-in console audio output control
(headphone and speakers), which is operated by the mute and (in
some ThinkPad models) volume hotkeys.
If this option is enabled, ThinkPad-ACPI will export an ALSA card
with a single read-only mixer control, which should be used for
on-screen-display feedback purposes by the Desktop Environment.
Optionally, the driver will also allow software control (the
ALSA mixer will be made read-write). Please refer to the driver
documentation for details.
All IBM models have both volume and mute control. Newer Lenovo
models only have mute control (the volume hotkeys are just normal
keys and volume control is done through the main HDA mixer).
config THINKPAD_ACPI_DEBUGFACILITIES
bool "Maintainer debug facilities"
depends on THINKPAD_ACPI
help
Enables extra stuff in the thinkpad-acpi which is completely useless
for normal use. Read the driver source to find out what it does.
Say N here, unless you were told by a kernel maintainer to do
otherwise.
config THINKPAD_ACPI_DEBUG
bool "Verbose debug mode"
depends on THINKPAD_ACPI
help
Enables extra debugging information, at the expense of a slightly
increase in driver size.
If you are not sure, say N here.
config THINKPAD_ACPI_UNSAFE_LEDS
bool "Allow control of important LEDs (unsafe)"
depends on THINKPAD_ACPI
help
Overriding LED state on ThinkPads can mask important
firmware alerts (like critical battery condition), or misled
the user into damaging the hardware (undocking or ejecting
the bay while buses are still active), etc.
LED control on the ThinkPad is write-only (with very few
exceptions on very ancient models), which makes it
impossible to know beforehand if important information will
be lost when one changes LED state.
Users that know what they are doing can enable this option
and the driver will allow control of every LED, including
the ones on the dock stations.
Never enable this option on a distribution kernel.
Say N here, unless you are building a kernel for your own
use, and need to control the important firmware LEDs.
config THINKPAD_ACPI_VIDEO
bool "Video output control support"
depends on THINKPAD_ACPI
default y
help
Allows the thinkpad_acpi driver to provide an interface to control
the various video output ports.
This feature often won't work well, depending on ThinkPad model,
display state, video output devices in use, whether there is a X
server running, phase of the moon, and the current mood of
Schroedinger's cat. If you can use X.org's RandR to control
your ThinkPad's video output ports instead of this feature,
don't think twice: do it and say N here to save memory and avoid
bad interactions with X.org.
NOTE: access to this feature is limited to processes with the
CAP_SYS_ADMIN capability, to avoid local DoS issues in platforms
where it interacts badly with X.org.
If you are not sure, say Y here but do try to check if you could
be using X.org RandR instead.
config THINKPAD_ACPI_HOTKEY_POLL
bool "Support NVRAM polling for hot keys"
depends on THINKPAD_ACPI
default y
help
Some thinkpad models benefit from NVRAM polling to detect a few of
the hot key press events. If you know your ThinkPad model does not
need to do NVRAM polling to support any of the hot keys you use,
unselecting this option will save about 1kB of memory.
ThinkPads T40 and newer, R52 and newer, and X31 and newer are
unlikely to need NVRAM polling in their latest BIOS versions.
NVRAM polling can detect at most the following keys: ThinkPad/Access
IBM, Zoom, Switch Display (fn+F7), ThinkLight, Volume up/down/mute,
Brightness up/down, Display Expand (fn+F8), Hibernate (fn+F12).
If you are not sure, say Y here. The driver enables polling only if
it is strictly necessary to do so.
config THINKPAD_LMI
tristate "Lenovo WMI-based systems management driver"
depends on ACPI_WMI
depends on DMI
select FW_ATTR_CLASS
help
This driver allows changing BIOS settings on Lenovo machines whose
BIOS support the WMI interface.
To compile this driver as a module, choose M here: the module will
be called think-lmi.
config YOGABOOK
tristate "Lenovo Yoga Book tablet key driver"
depends on ACPI_WMI
depends on INPUT
depends on I2C
select LEDS_CLASS
select NEW_LEDS
help
Say Y here if you want to support the 'Pen' key and keyboard backlight
control on the Lenovo Yoga Book tablets.
To compile this driver as a module, choose M here: the module will
be called lenovo-yogabook.
config YT2_1380
tristate "Lenovo Yoga Tablet 2 1380 fast charge driver"
depends on SERIAL_DEV_BUS
depends on EXTCON
depends on ACPI
help
Say Y here to enable support for the custom fast charging protocol
found on the Lenovo Yoga Tablet 2 1380F / 1380L models.
To compile this driver as a module, choose M here: the module will
be called lenovo-yogabook.
config LENOVO_WMI_CAPDATA
tristate
depends on ACPI_WMI
depends on LENOVO_WMI_HELPERS
config LENOVO_WMI_EVENTS
tristate
depends on ACPI_WMI
config LENOVO_WMI_HELPERS
tristate
depends on ACPI_WMI
config LENOVO_WMI_GAMEZONE
tristate "Lenovo GameZone WMI Driver"
depends on ACPI_WMI
depends on DMI
select ACPI_PLATFORM_PROFILE
select LENOVO_WMI_EVENTS
select LENOVO_WMI_HELPERS
help
Say Y here if you have a WMI aware Lenovo Legion device and would like to use the
platform-profile firmware interface to manage power usage.
To compile this driver as a module, choose M here: the module will
be called lenovo-wmi-gamezone.
config LENOVO_WMI_TUNING
tristate "Lenovo Other Mode WMI Driver"
depends on ACPI_WMI
depends on ACPI_BATTERY
select HWMON
select FW_ATTR_CLASS
select LENOVO_WMI_CAPDATA
select LENOVO_WMI_EVENTS
select LENOVO_WMI_HELPERS
help
Say Y here if you have a WMI aware Lenovo Legion device and would like to use the
firmware_attributes API to control various tunable settings typically exposed by
Lenovo software in Windows.
To compile this driver as a module, choose M here: the module will
be called lenovo-wmi-other.
@@ -0,0 +1,28 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for linux/drivers/platform/x86/lenovo
# Lenovo x86 Platform Specific Drivers
#
obj-$(CONFIG_IDEAPAD_LAPTOP) += ideapad-laptop.o
obj-$(CONFIG_THINKPAD_LMI) += think-lmi.o
obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o
lenovo-target-$(CONFIG_LENOVO_WMI_HOTKEY_UTILITIES) += wmi-hotkey-utilities.o
lenovo-target-$(CONFIG_LENOVO_YMC) += ymc.o
lenovo-target-$(CONFIG_YOGABOOK) += yogabook.o
lenovo-target-$(CONFIG_YT2_1380) += yoga-tab2-pro-1380-fastcharger.o
lenovo-target-$(CONFIG_LENOVO_WMI_CAMERA) += wmi-camera.o
lenovo-target-$(CONFIG_LENOVO_WMI_CAPDATA) += wmi-capdata.o
lenovo-target-$(CONFIG_LENOVO_WMI_EVENTS) += wmi-events.o
lenovo-target-$(CONFIG_LENOVO_WMI_HELPERS) += wmi-helpers.o
lenovo-target-$(CONFIG_LENOVO_WMI_GAMEZONE) += wmi-gamezone.o
lenovo-target-$(CONFIG_LENOVO_WMI_TUNING) += wmi-other.o
# Add 'lenovo' prefix to each module listed in lenovo-target-*
define LENOVO_OBJ_TARGET
lenovo-$(1)-y := $(1).o
obj-$(2) += lenovo-$(1).o
endef
$(foreach target, $(basename $(lenovo-target-y)), $(eval $(call LENOVO_OBJ_TARGET,$(target),y)))
$(foreach target, $(basename $(lenovo-target-m)), $(eval $(call LENOVO_OBJ_TARGET,$(target),m)))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* ideapad-laptop.h - Lenovo IdeaPad ACPI Extras
*
* Copyright © 2010 Intel Corporation
* Copyright © 2010 David Woodhouse <dwmw2@infradead.org>
*/
#ifndef _IDEAPAD_LAPTOP_H_
#define _IDEAPAD_LAPTOP_H_
#include <linux/notifier.h>
enum ideapad_laptop_notifier_actions {
IDEAPAD_LAPTOP_YMC_EVENT,
};
int ideapad_laptop_register_notifier(struct notifier_block *nb);
int ideapad_laptop_unregister_notifier(struct notifier_block *nb);
void ideapad_laptop_call_notifier(unsigned long action, void *data);
#endif /* !_IDEAPAD_LAPTOP_H_ */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,140 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef _THINK_LMI_H_
#define _THINK_LMI_H_
#include <linux/types.h>
#include <linux/wmi.h>
#define TLMI_SETTINGS_COUNT 256
#define TLMI_SETTINGS_MAXLEN 512
#define TLMI_PWD_BUFSIZE 129
#define TLMI_LANG_MAXLEN 4
#define TLMI_INDEX_MAX 32
/* Possible error values */
struct tlmi_err_codes {
const char *err_str;
int err_code;
};
enum encoding_option {
TLMI_ENCODING_ASCII,
TLMI_ENCODING_SCANCODE,
};
enum level_option {
TLMI_LEVEL_USER,
TLMI_LEVEL_MASTER,
};
/*
* There are a limit on the number of WMI operations you can do if you use
* the default implementation of saving on every set. This is due to a
* limitation in EFI variable space used.
* Have a 'bulk save' mode where you can manually trigger the save, and can
* therefore set unlimited variables - for users that need it.
*/
enum save_mode {
TLMI_SAVE_SINGLE,
TLMI_SAVE_BULK,
TLMI_SAVE_SAVE,
};
/* GUIDs can differ between platforms */
struct tlmi_cert_guids {
const char *thumbprint;
const char *set_bios_setting;
const char *save_bios_setting;
const char *cert_to_password;
const char *clear_bios_cert;
const char *update_bios_cert;
const char *set_bios_cert;
};
/* password configuration details */
#define TLMI_PWDCFG_MODE_LEGACY 0
#define TLMI_PWDCFG_MODE_PASSWORD 1
#define TLMI_PWDCFG_MODE_MULTICERT 3
struct tlmi_pwdcfg_core {
uint32_t password_mode;
uint32_t password_state;
uint32_t min_length;
uint32_t max_length;
uint32_t supported_encodings;
uint32_t supported_keyboard;
};
struct tlmi_pwdcfg_ext {
uint32_t hdd_user_password;
uint32_t hdd_master_password;
uint32_t nvme_user_password;
uint32_t nvme_master_password;
};
struct tlmi_pwdcfg {
struct tlmi_pwdcfg_core core;
struct tlmi_pwdcfg_ext ext;
};
/* password setting details */
struct tlmi_pwd_setting {
struct kobject kobj;
bool pwd_enabled;
char password[TLMI_PWD_BUFSIZE];
const char *pwd_type;
const char *role;
int minlen;
int maxlen;
enum encoding_option encoding;
char kbdlang[TLMI_LANG_MAXLEN];
int index; /*Used for HDD and NVME auth */
enum level_option level;
bool cert_installed;
char *signature;
char *save_signature;
};
/* Attribute setting details */
struct tlmi_attr_setting {
struct kobject kobj;
struct wmi_device *wdev;
int index;
char name[TLMI_SETTINGS_MAXLEN];
char display_name[TLMI_SETTINGS_MAXLEN];
char *possible_values;
};
struct think_lmi {
struct wmi_device *wmi_device;
bool can_set_bios_settings;
bool can_get_bios_selections;
bool can_set_bios_password;
bool can_get_password_settings;
bool pending_changes;
bool can_debug_cmd;
bool opcode_support;
bool certificate_support;
enum save_mode save_mode;
bool save_required;
bool reboot_required;
bool thinkcenter_mode;
struct tlmi_attr_setting *setting[TLMI_SETTINGS_COUNT];
struct device *class_dev;
struct kset *attribute_kset;
struct kset *authentication_kset;
struct tlmi_pwdcfg pwdcfg;
struct tlmi_pwd_setting *pwd_admin;
struct tlmi_pwd_setting *pwd_power;
struct tlmi_pwd_setting *pwd_system;
struct tlmi_pwd_setting *pwd_hdd;
struct tlmi_pwd_setting *pwd_nvme;
const struct tlmi_cert_guids *cert_guid;
};
#endif /* !_THINK_LMI_H_ */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,147 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Lenovo WMI Camera Button Driver
*
* Author: Ai Chao <aichao@kylinos.cn>
* Copyright (C) 2024 KylinSoft Corporation.
*/
#include <linux/acpi.h>
#include <linux/device.h>
#include <linux/input.h>
#include <linux/types.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/wmi.h>
#include <linux/cleanup.h>
#define WMI_LENOVO_CAMERABUTTON_EVENT_GUID "50C76F1F-D8E4-D895-0A3D-62F4EA400013"
struct lenovo_wmi_priv {
struct input_dev *idev;
struct mutex notify_lock; /* lenovo WMI camera button notify lock */
};
enum {
SW_CAMERA_OFF = 0,
SW_CAMERA_ON = 1,
};
static int camera_shutter_input_setup(struct wmi_device *wdev, u8 camera_mode)
{
struct lenovo_wmi_priv *priv = dev_get_drvdata(&wdev->dev);
int err;
priv->idev = input_allocate_device();
if (!priv->idev)
return -ENOMEM;
priv->idev->name = "Lenovo WMI Camera Button";
priv->idev->phys = "wmi/input0";
priv->idev->id.bustype = BUS_HOST;
priv->idev->dev.parent = &wdev->dev;
input_set_capability(priv->idev, EV_SW, SW_CAMERA_LENS_COVER);
input_report_switch(priv->idev, SW_CAMERA_LENS_COVER,
camera_mode == SW_CAMERA_ON ? 0 : 1);
input_sync(priv->idev);
err = input_register_device(priv->idev);
if (err) {
input_free_device(priv->idev);
priv->idev = NULL;
}
return err;
}
static void lenovo_wmi_notify(struct wmi_device *wdev, union acpi_object *obj)
{
struct lenovo_wmi_priv *priv = dev_get_drvdata(&wdev->dev);
u8 camera_mode;
if (obj->type != ACPI_TYPE_BUFFER) {
dev_err(&wdev->dev, "Bad response type %u\n", obj->type);
return;
}
if (obj->buffer.length != 1) {
dev_err(&wdev->dev, "Invalid buffer length %u\n", obj->buffer.length);
return;
}
/*
* obj->buffer.pointer[0] is camera mode:
* 0 camera close
* 1 camera open
*/
camera_mode = obj->buffer.pointer[0];
if (camera_mode > SW_CAMERA_ON) {
dev_err(&wdev->dev, "Unknown camera mode %u\n", camera_mode);
return;
}
guard(mutex)(&priv->notify_lock);
if (!priv->idev) {
if (camera_shutter_input_setup(wdev, camera_mode))
dev_warn(&wdev->dev, "Failed to register input device\n");
return;
}
if (camera_mode == SW_CAMERA_ON)
input_report_switch(priv->idev, SW_CAMERA_LENS_COVER, 0);
else
input_report_switch(priv->idev, SW_CAMERA_LENS_COVER, 1);
input_sync(priv->idev);
}
static int lenovo_wmi_probe(struct wmi_device *wdev, const void *context)
{
struct lenovo_wmi_priv *priv;
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
dev_set_drvdata(&wdev->dev, priv);
mutex_init(&priv->notify_lock);
return 0;
}
static void lenovo_wmi_remove(struct wmi_device *wdev)
{
struct lenovo_wmi_priv *priv = dev_get_drvdata(&wdev->dev);
if (priv->idev)
input_unregister_device(priv->idev);
mutex_destroy(&priv->notify_lock);
}
static const struct wmi_device_id lenovo_wmi_id_table[] = {
{ .guid_string = WMI_LENOVO_CAMERABUTTON_EVENT_GUID },
{ }
};
MODULE_DEVICE_TABLE(wmi, lenovo_wmi_id_table);
static struct wmi_driver lenovo_wmi_driver = {
.driver = {
.name = "lenovo-wmi-camera",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
.id_table = lenovo_wmi_id_table,
.min_event_size = sizeof(u8),
.no_singleton = true,
.probe = lenovo_wmi_probe,
.notify = lenovo_wmi_notify,
.remove = lenovo_wmi_remove,
};
module_wmi_driver(lenovo_wmi_driver);
MODULE_AUTHOR("Ai Chao <aichao@kylinos.cn>");
MODULE_DESCRIPTION("Lenovo WMI Camera Button Driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,948 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Lenovo Capability Data WMI Data Block driver.
*
* Lenovo Capability Data provides information on tunable attributes used by
* the "Other Mode" WMI interface.
*
* Capability Data 00 includes if the attribute is supported by the hardware,
* and the default_value. All attributes are independent of thermal modes.
*
* Capability Data 01 includes if the attribute is supported by the hardware,
* and the default_value, max_value, min_value, and step increment. Each
* attribute has multiple pages, one for each of the thermal modes managed by
* the Gamezone interface.
*
* Fan Test Data includes the max/min fan speed RPM for each fan. This is
* reference data for self-test. If the fan is in good condition, it is capable
* to spin faster than max RPM or slower than min RPM.
*
* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
* - Initial implementation (formerly named lenovo-wmi-capdata01)
*
* Copyright (C) 2025 Rong Zhang <i@rong.moe>
* - Unified implementation
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/bug.h>
#include <linux/cleanup.h>
#include <linux/component.h>
#include <linux/container_of.h>
#include <linux/debugfs.h>
#include <linux/device.h>
#include <linux/dev_printk.h>
#include <linux/err.h>
#include <linux/export.h>
#include <linux/gfp_types.h>
#include <linux/limits.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/mutex_types.h>
#include <linux/notifier.h>
#include <linux/overflow.h>
#include <linux/seq_file.h>
#include <linux/stddef.h>
#include <linux/types.h>
#include <linux/wmi.h>
#include "wmi-capdata.h"
#include "wmi-helpers.h"
#define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
#define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21"
#define ACPI_AC_NOTIFY_STATUS 0x80
#define LWMI_FEATURE_ID_FAN_TEST 0x05
#define LWMI_ATTR_ID_FAN_TEST \
lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_TEST, \
LWMI_GZ_THERMAL_MODE_NONE, LWMI_TYPE_ID_NONE)
enum lwmi_cd_type {
LENOVO_CAPABILITY_DATA_00,
LENOVO_CAPABILITY_DATA_01,
LENOVO_FAN_TEST_DATA,
CD_TYPE_NONE = -1,
};
#define LWMI_CD_TABLE_ITEM(_type) \
[_type] = { \
.name = #_type, \
.type = _type, \
}
static const struct lwmi_cd_info {
const char *name;
enum lwmi_cd_type type;
} lwmi_cd_table[] = {
LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA),
};
struct lwmi_cd_priv {
struct notifier_block acpi_nb; /* ACPI events */
struct wmi_device *wdev;
struct cd_list *list;
struct dentry *debugfs_dir;
/*
* A capdata device may be a component master of another capdata device.
* E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan
* |- master |- component
* |- sub-master
* |- sub-component
*/
struct lwmi_cd_sub_master_priv {
struct device *master_dev;
cd_list_cb_t master_cb;
struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */
bool registered; /* Has the sub-master been registered? */
} *sub_master;
};
struct cd_list {
struct mutex list_mutex; /* list R/W mutex */
enum lwmi_cd_type type;
u8 count;
union {
DECLARE_FLEX_ARRAY(struct capdata00, cd00);
DECLARE_FLEX_ARRAY(struct capdata01, cd01);
DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan);
};
};
static struct wmi_driver lwmi_cd_driver;
/* ======== Device components ======== */
/**
* lwmi_cd_match() - Match rule for the master driver.
* @dev: Pointer to the capability data parent device.
* @type: Pointer to capability data type (enum lwmi_cd_type *) to match.
*
* Return: int.
*/
static int lwmi_cd_match(struct device *dev, void *type)
{
struct lwmi_cd_priv *priv;
if (dev->driver != &lwmi_cd_driver.driver)
return false;
priv = dev_get_drvdata(dev);
return priv->list->type == *(enum lwmi_cd_type *)type;
}
/**
* lwmi_cd_match_add_all() - Add all match rule for the master driver.
* @master: Pointer to the master device.
* @matchptr: Pointer to the returned component_match pointer.
*
* Adds all component matches to the list stored in @matchptr for the @master
* device. @matchptr must be initialized to NULL.
*/
void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr)
{
int i;
if (WARN_ON(*matchptr))
return;
for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
/* Skip sub-components. */
if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
continue;
component_match_add(master, matchptr, lwmi_cd_match,
(void *)&lwmi_cd_table[i].type);
if (IS_ERR(*matchptr))
return;
}
}
EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CAPDATA");
/**
* lwmi_cd_call_master_cb() - Call the master callback for the sub-component.
* @priv: Pointer to the capability data private data.
*
* Call the master callback and pass the sub-component list to it if the
* dependency chain (master <-> sub-master <-> sub-component) is complete.
*/
static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
{
struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
/*
* Call the callback only if the dependency chain is ready:
* - Binding between master and sub-master: fills master_dev and master_cb
* - Binding between sub-master and sub-component: fills sub_component_list
*
* If a binding has been unbound before the other binding is bound, the
* corresponding members filled by the former are guaranteed to be cleared.
*
* This function is only called in bind callbacks, and the component
* framework guarantees bind/unbind callbacks may never execute
* simultaneously, which implies that it's impossible to have a race
* condition.
*
* Hence, this check is sufficient to ensure that the callback is called
* at most once and with the correct state, without relying on a specific
* sequence of binding establishment.
*/
if (!sub_component_list ||
!priv->sub_master->master_dev ||
!priv->sub_master->master_cb)
return;
if (PTR_ERR(sub_component_list) == -ENODEV)
sub_component_list = NULL;
else if (WARN_ON(IS_ERR(sub_component_list)))
return;
priv->sub_master->master_cb(priv->sub_master->master_dev,
sub_component_list);
/*
* Userspace may unbind a device from its driver and bind it again
* through sysfs. Let's call this operation "reprobe" to distinguish it
* from component "rebind".
*
* When reprobing capdata00/01 or the master device, the master device
* is unbound from us with appropriate cleanup before we bind to it and
* call master_cb. Everything is fine in this case.
*
* When reprobing capdata_fan, the master device has never been unbound
* from us (hence no cleanup is done)[1], but we call master_cb the
* second time. To solve this issue, we clear master_cb and master_dev
* so we won't call master_cb twice while a binding is still complete.
*
* Note that we can't clear sub_component_list, otherwise reprobing
* capdata01 or the master device causes master_cb to be never called
* after we rebind to the master device.
*
* [1]: The master device does not need capdata_fan in run time, so
* losing capdata_fan will not break the binding to the master device.
*/
priv->sub_master->master_cb = NULL;
priv->sub_master->master_dev = NULL;
}
/**
* lwmi_cd_component_bind() - Bind component to master device.
* @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
* @om_dev: Pointer to the lenovo-wmi-other driver parent device.
* @data: lwmi_cd_binder object pointer used to return the capability data.
*
* On lenovo-wmi-other's master bind, provide a pointer to the local capdata
* list. This is used to call lwmi_cd*_get_data to look up attribute data
* from the lenovo-wmi-other driver.
*
* If cd_dev is a sub-master, try to call the master callback.
*
* Return: 0
*/
static int lwmi_cd_component_bind(struct device *cd_dev,
struct device *om_dev, void *data)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
struct lwmi_cd_binder *binder = data;
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
binder->cd00_list = priv->list;
priv->sub_master->master_dev = om_dev;
priv->sub_master->master_cb = binder->cd_fan_list_cb;
lwmi_cd_call_master_cb(priv);
break;
case LENOVO_CAPABILITY_DATA_01:
binder->cd01_list = priv->list;
break;
default:
return -EINVAL;
}
return 0;
}
/**
* lwmi_cd_component_unbind() - Unbind component to master device.
* @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
* @om_dev: Pointer to the lenovo-wmi-other driver parent device.
* @data: Unused.
*
* If cd_dev is a sub-master, clear the collected data from the master device to
* prevent the binding establishment between the sub-master and the sub-
* component (if it's about to happen) from calling the master callback.
*/
static void lwmi_cd_component_unbind(struct device *cd_dev,
struct device *om_dev, void *data)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
priv->sub_master->master_dev = NULL;
priv->sub_master->master_cb = NULL;
return;
default:
return;
}
}
static const struct component_ops lwmi_cd_component_ops = {
.bind = lwmi_cd_component_bind,
.unbind = lwmi_cd_component_unbind,
};
/**
* lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device
* @dev: The sub-master capdata basic device.
*
* Call component_bind_all to bind the sub-component device to the sub-master
* device. On success, collect the pointer to the sub-component list and try
* to call the master callback.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_cd_sub_master_bind(struct device *dev)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
struct cd_list *sub_component_list;
int ret;
ret = component_bind_all(dev, &sub_component_list);
if (ret)
return ret;
priv->sub_master->sub_component_list = sub_component_list;
lwmi_cd_call_master_cb(priv);
return 0;
}
/**
* lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device
* @dev: The sub-master capdata basic device
*
* Clear the collected pointer to the sub-component list to prevent the binding
* establishment between the sub-master and the sub-component (if it's about to
* happen) from calling the master callback. Then, call component_unbind_all to
* unbind the sub-component device from the sub-master device.
*/
static void lwmi_cd_sub_master_unbind(struct device *dev)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
priv->sub_master->sub_component_list = NULL;
component_unbind_all(dev, NULL);
}
static const struct component_master_ops lwmi_cd_sub_master_ops = {
.bind = lwmi_cd_sub_master_bind,
.unbind = lwmi_cd_sub_master_unbind,
};
/**
* lwmi_cd_sub_master_add() - Register a sub-master with its sub-component
* @priv: Pointer to the sub-master capdata device private data.
* @sub_component_type: Type of the sub-component.
*
* Match the sub-component type and register the current capdata device as a
* sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub-
* component as non-existent without registering sub-master.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
enum lwmi_cd_type sub_component_type)
{
struct component_match *master_match = NULL;
int ret;
priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
if (!priv->sub_master)
return -ENOMEM;
if (sub_component_type == CD_TYPE_NONE) {
/* The master callback will be called with NULL on bind. */
priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
priv->sub_master->registered = false;
return 0;
}
/*
* lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack
* data cannot be used here. Steal one from lwmi_cd_table.
*/
component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
(void *)&lwmi_cd_table[sub_component_type].type);
if (IS_ERR(master_match))
return PTR_ERR(master_match);
ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
master_match);
if (ret)
return ret;
priv->sub_master->registered = true;
return 0;
}
/**
* lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered
* @priv: Pointer to the sub-master capdata device private data.
*/
static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
{
if (!priv->sub_master->registered)
return;
component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
priv->sub_master->registered = false;
}
/**
* lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device.
* @sc_dev: Pointer to the sub-component capdata parent device.
* @sm_dev: Pointer to the sub-master capdata parent device.
* @data: Pointer used to return the capability data list pointer.
*
* On sub-master's bind, provide a pointer to the local capdata list.
* This is used by the sub-master to call the master callback.
*
* Return: 0
*/
static int lwmi_cd_sub_component_bind(struct device *sc_dev,
struct device *sm_dev, void *data)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
struct cd_list **listp = data;
*listp = priv->list;
return 0;
}
static const struct component_ops lwmi_cd_sub_component_ops = {
.bind = lwmi_cd_sub_component_bind,
};
/*
* lwmi_cd*_get_data - Get the data of the specified attribute
* @list: The lenovo-wmi-capdata pointer to its cd_list struct.
* @attribute_id: The capdata attribute ID to be found.
* @output: Pointer to a capdata* struct to return the data.
*
* Retrieves the capability data struct pointer for the given
* attribute.
*
* Return: 0 on success, or -EINVAL.
*/
#define DEF_LWMI_CDXX_GET_DATA(_cdxx, _cd_type, _output_t) \
int lwmi_##_cdxx##_get_data(struct cd_list *list, u32 attribute_id, _output_t *output) \
{ \
u8 idx; \
\
if (WARN_ON(list->type != _cd_type)) \
return -EINVAL; \
\
guard(mutex)(&list->list_mutex); \
for (idx = 0; idx < list->count; idx++) { \
if (list->_cdxx[idx].id != attribute_id) \
continue; \
memcpy(output, &list->_cdxx[idx], sizeof(list->_cdxx[idx])); \
return 0; \
} \
return -EINVAL; \
}
DEF_LWMI_CDXX_GET_DATA(cd00, LENOVO_CAPABILITY_DATA_00, struct capdata00);
EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CAPDATA");
DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CAPDATA");
DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan);
EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CAPDATA");
/* ======== debugfs ======== */
/**
* lwmi_cd00_show() - Dump capdata00
* @s: Pointer to seq_file where the capdata00 is dumped.
* @cd00: Pointer to a capdata00 struct to be dumped.
*/
static void lwmi_cd00_show(struct seq_file *s, struct capdata00 *cd00)
{
u8 dev = FIELD_GET(LWMI_ATTR_DEV_ID_MASK, cd00->id);
u8 feat = FIELD_GET(LWMI_ATTR_FEAT_ID_MASK, cd00->id);
u8 mode = FIELD_GET(LWMI_ATTR_MODE_ID_MASK, cd00->id);
u8 type = FIELD_GET(LWMI_ATTR_TYPE_ID_MASK, cd00->id);
bool extra = cd00->supported & ~(LWMI_SUPP_GET | LWMI_SUPP_SET | LWMI_SUPP_VALID);
bool get = cd00->supported & LWMI_SUPP_GET;
bool set = cd00->supported & LWMI_SUPP_SET;
bool valid = cd00->supported & LWMI_SUPP_VALID;
seq_printf(s, " id: 0x%08x [dev: %2u, feat: %2u, mode: %2u, type: %2u]\n",
cd00->id, dev, feat, mode, type);
seq_printf(s, " supported: 0x%08x [%c%c%c%c]\n", cd00->supported,
extra ? '+' : ' ',
get ? 'R' : ' ',
set ? 'W' : ' ',
valid ? 'V' : ' ');
seq_printf(s, " default_value: %u\n", cd00->default_value);
}
/**
* lwmi_cd01_show() - Dump capdata01
* @s: Pointer to seq_file where the capdata01 is dumped.
* @cd01: Pointer to a capdata01 struct to be dumped.
*/
static void lwmi_cd01_show(struct seq_file *s, struct capdata01 *cd01)
{
/* capdata01 is an extension to capdata00. */
lwmi_cd00_show(s, &cd01->cd00);
seq_printf(s, " step: %u\n", cd01->step);
seq_printf(s, " min_value: %u\n", cd01->min_value);
seq_printf(s, " max_value: %u\n", cd01->max_value);
}
/**
* lwmi_cd_fan_show() - Dump capdata_fan
* @s: Pointer to seq_file where the capdata_fan is dumped.
* @cd_fan: Pointer to a capdata_fan struct to be dumped.
*/
static void lwmi_cd_fan_show(struct seq_file *s, struct capdata_fan *cd_fan)
{
seq_printf(s, " id: %u\n", cd_fan->id);
seq_printf(s, " min_rpm: %u\n", cd_fan->min_rpm);
seq_printf(s, " max_rpm: %u\n", cd_fan->max_rpm);
}
/**
* lwmi_cd_debugfs_show() - Dump capability data to debugfs
* @s: Pointer to seq_file where the capability data is dumped.
* @data: unused.
*
* Return: 0
*/
static int lwmi_cd_debugfs_show(struct seq_file *s, void *data)
{
struct lwmi_cd_priv *priv = s->private;
u8 idx;
guard(mutex)(&priv->list->list_mutex);
/* lwmi_cd_alloc() ensured priv->list->type must be a valid type. */
for (idx = 0; idx < priv->list->count; idx++) {
seq_printf(s, "%s[%u]:\n", lwmi_cd_table[priv->list->type].name, idx);
if (priv->list->type == LENOVO_CAPABILITY_DATA_00)
lwmi_cd00_show(s, &priv->list->cd00[idx]);
else if (priv->list->type == LENOVO_CAPABILITY_DATA_01)
lwmi_cd01_show(s, &priv->list->cd01[idx]);
else if (priv->list->type == LENOVO_FAN_TEST_DATA)
lwmi_cd_fan_show(s, &priv->list->cd_fan[idx]);
}
return 0;
}
DEFINE_SHOW_ATTRIBUTE(lwmi_cd_debugfs);
/**
* lwmi_cd_debugfs_add() - Create debugfs directory and files for a device
* @priv: lenovo-wmi-capdata driver data.
*/
static void lwmi_cd_debugfs_add(struct lwmi_cd_priv *priv)
{
priv->debugfs_dir = lwmi_debugfs_create_dir(priv->wdev);
debugfs_create_file("capdata", 0444, priv->debugfs_dir, priv, &lwmi_cd_debugfs_fops);
}
/**
* lwmi_cd_debugfs_remove() - Remove debugfs directory for a device
* @priv: lenovo-wmi-capdata driver data.
*/
static void lwmi_cd_debugfs_remove(struct lwmi_cd_priv *priv)
{
debugfs_remove_recursive(priv->debugfs_dir);
priv->debugfs_dir = NULL;
}
/* ======== WMI interface ======== */
/**
* lwmi_cd_cache() - Cache all WMI data block information
* @priv: lenovo-wmi-capdata driver data.
*
* Loop through each WMI data block and cache the data.
*
* Return: 0 on success, or an error.
*/
static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
{
size_t size;
int idx;
void *p;
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
p = &priv->list->cd00[0];
size = sizeof(priv->list->cd00[0]);
break;
case LENOVO_CAPABILITY_DATA_01:
p = &priv->list->cd01[0];
size = sizeof(priv->list->cd01[0]);
break;
case LENOVO_FAN_TEST_DATA:
/* Done by lwmi_cd_alloc() => lwmi_cd_fan_list_alloc_cache(). */
return 0;
default:
return -EINVAL;
}
guard(mutex)(&priv->list->list_mutex);
for (idx = 0; idx < priv->list->count; idx++, p += size) {
union acpi_object *ret_obj __free(kfree) = NULL;
ret_obj = wmidev_block_query(priv->wdev, idx);
if (!ret_obj)
return -ENODEV;
if (ret_obj->type != ACPI_TYPE_BUFFER ||
ret_obj->buffer.length < size)
continue;
memcpy(p, ret_obj->buffer.pointer, size);
}
return 0;
}
/**
* lwmi_cd_fan_list_alloc_cache() - Alloc and cache Fan Test Data list
* @priv: lenovo-wmi-capdata driver data.
* @listptr: Pointer to returned cd_list pointer.
*
* Return: count of fans found, or an error.
*/
static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr)
{
struct cd_list *list;
size_t size;
u32 count;
int idx;
/* Emit unaligned access to u8 buffer with __packed. */
struct cd_fan_block {
u32 nr;
u32 data[]; /* id[nr], max_rpm[nr], min_rpm[nr] */
} __packed * block;
union acpi_object *ret_obj __free(kfree) = wmidev_block_query(priv->wdev, 0);
if (!ret_obj)
return -ENODEV;
if (ret_obj->type == ACPI_TYPE_BUFFER) {
block = (struct cd_fan_block *)ret_obj->buffer.pointer;
size = ret_obj->buffer.length;
count = size >= sizeof(*block) ? block->nr : 0;
if (size < struct_size(block, data, count * 3)) {
dev_warn(&priv->wdev->dev,
"incomplete fan test data block: %zu < %zu, ignoring\n",
size, struct_size(block, data, count * 3));
count = 0;
} else if (count > U8_MAX) {
dev_warn(&priv->wdev->dev,
"too many fans reported: %u > %u, truncating\n",
count, U8_MAX);
count = U8_MAX;
}
} else {
/*
* This is usually caused by a dummy ACPI method. Do not return an error
* as failing to probe this device will result in sub-master device being
* unbound. This behavior aligns with lwmi_cd_cache().
*/
count = 0;
}
list = devm_kzalloc(&priv->wdev->dev, struct_size(list, cd_fan, count), GFP_KERNEL);
if (!list)
return -ENOMEM;
for (idx = 0; idx < count; idx++) {
/* Do not calculate array index using count, as it may be truncated. */
list->cd_fan[idx] = (struct capdata_fan) {
.id = block->data[idx],
.max_rpm = block->data[idx + block->nr],
.min_rpm = block->data[idx + (2 * block->nr)],
};
}
*listptr = list;
return count;
}
/**
* lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
* @priv: lenovo-wmi-capdata driver data.
* @type: The type of capability data.
*
* Allocate a cd_list struct large enough to contain data from all WMI data
* blocks provided by the interface.
*
* Return: 0 on success, or an error.
*/
static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
{
struct cd_list *list;
size_t list_size;
int count, ret;
count = wmidev_instance_count(priv->wdev);
switch (type) {
case LENOVO_CAPABILITY_DATA_00:
list_size = struct_size(list, cd00, count);
break;
case LENOVO_CAPABILITY_DATA_01:
list_size = struct_size(list, cd01, count);
break;
case LENOVO_FAN_TEST_DATA:
count = lwmi_cd_fan_list_alloc_cache(priv, &list);
if (count < 0)
return count;
goto got_list;
default:
return -EINVAL;
}
list = devm_kzalloc(&priv->wdev->dev, list_size, GFP_KERNEL);
if (!list)
return -ENOMEM;
got_list:
ret = devm_mutex_init(&priv->wdev->dev, &list->list_mutex);
if (ret)
return ret;
list->type = type;
list->count = count;
priv->list = list;
return 0;
}
/**
* lwmi_cd_setup() - Cache all WMI data block information
* @priv: lenovo-wmi-capdata driver data.
* @type: The type of capability data.
*
* Allocate a cd_list struct large enough to contain data from all WMI data
* blocks provided by the interface. Then loop through each data block and
* cache the data.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_cd_setup(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
{
int ret;
ret = lwmi_cd_alloc(priv, type);
if (ret)
return ret;
return lwmi_cd_cache(priv);
}
/**
* lwmi_cd01_notifier_call() - Call method for cd01 notifier.
* block call chain.
* @nb: The notifier_block registered to lenovo-wmi-events driver.
* @action: Unused.
* @data: The ACPI event.
*
* For LWMI_EVENT_THERMAL_MODE, set current_mode and notify platform_profile
* of a change.
*
* Return: notifier_block status.
*/
static int lwmi_cd01_notifier_call(struct notifier_block *nb, unsigned long action,
void *data)
{
struct acpi_bus_event *event = data;
struct lwmi_cd_priv *priv;
int ret;
if (strcmp(event->device_class, ACPI_AC_CLASS) != 0)
return NOTIFY_DONE;
priv = container_of(nb, struct lwmi_cd_priv, acpi_nb);
switch (event->type) {
case ACPI_AC_NOTIFY_STATUS:
ret = lwmi_cd_cache(priv);
if (ret)
return NOTIFY_BAD;
return NOTIFY_OK;
default:
return NOTIFY_DONE;
}
}
/**
* lwmi_cd01_unregister() - Unregister the cd01 ACPI notifier_block.
* @data: The ACPI event notifier_block to unregister.
*/
static void lwmi_cd01_unregister(void *data)
{
struct notifier_block *acpi_nb = data;
unregister_acpi_notifier(acpi_nb);
}
static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
{
const struct lwmi_cd_info *info = context;
struct lwmi_cd_priv *priv;
int ret;
if (!info)
return -EINVAL;
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->wdev = wdev;
dev_set_drvdata(&wdev->dev, priv);
ret = lwmi_cd_setup(priv, info->type);
if (ret)
goto out;
switch (info->type) {
case LENOVO_CAPABILITY_DATA_00: {
enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
struct capdata00 capdata00;
ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
sub_component_type = CD_TYPE_NONE;
}
/* Sub-master (capdata00) <-> sub-component (capdata_fan) */
ret = lwmi_cd_sub_master_add(priv, sub_component_type);
if (ret)
goto out;
/* Master (lenovo-wmi-other) <-> sub-master (capdata00) */
ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
if (ret)
lwmi_cd_sub_master_del(priv);
goto out;
}
case LENOVO_CAPABILITY_DATA_01:
priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
ret = register_acpi_notifier(&priv->acpi_nb);
if (ret)
goto out;
ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister,
&priv->acpi_nb);
if (ret)
goto out;
ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
goto out;
case LENOVO_FAN_TEST_DATA:
ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
goto out;
default:
return -EINVAL;
}
out:
if (ret) {
dev_err(&wdev->dev, "failed to register %s: %d\n",
info->name, ret);
} else {
lwmi_cd_debugfs_add(priv);
dev_dbg(&wdev->dev, "registered %s with %u items\n",
info->name, priv->list->count);
}
return ret;
}
static void lwmi_cd_remove(struct wmi_device *wdev)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev);
lwmi_cd_debugfs_remove(priv);
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
lwmi_cd_sub_master_del(priv);
fallthrough;
case LENOVO_CAPABILITY_DATA_01:
component_del(&wdev->dev, &lwmi_cd_component_ops);
break;
case LENOVO_FAN_TEST_DATA:
component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
break;
default:
WARN_ON(1);
}
}
#define LWMI_CD_WDEV_ID(_type) \
.guid_string = _type##_GUID, \
.context = &lwmi_cd_table[_type],
static const struct wmi_device_id lwmi_cd_id_table[] = {
{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_00) },
{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
{ LWMI_CD_WDEV_ID(LENOVO_FAN_TEST_DATA) },
{}
};
static struct wmi_driver lwmi_cd_driver = {
.driver = {
.name = "lenovo_wmi_capdata",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
.id_table = lwmi_cd_id_table,
.probe = lwmi_cd_probe,
.remove = lwmi_cd_remove,
.no_singleton = true,
};
module_wmi_driver(lwmi_cd_driver);
MODULE_IMPORT_NS("LENOVO_WMI_HELPERS");
MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table);
MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,91 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com> */
#ifndef _LENOVO_WMI_CAPDATA_H_
#define _LENOVO_WMI_CAPDATA_H_
#include <linux/bits.h>
#include <linux/bitfield.h>
#include <linux/types.h>
#define LWMI_SUPP_VALID BIT(0)
#define LWMI_SUPP_GET BIT(1)
#define LWMI_SUPP_SET BIT(2)
#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)
enum lwmi_device_id {
LWMI_DEVICE_ID_CPU = 0x01,
LWMI_DEVICE_ID_GPU = 0x02,
LWMI_DEVICE_ID_PSU = 0x03,
LWMI_DEVICE_ID_FAN = 0x04,
};
#define LWMI_TYPE_ID_NONE 0x00
struct component_match;
struct device;
struct cd_list;
struct capdata00 {
u32 id;
u32 supported;
u32 default_value;
};
struct capdata01 {
union {
struct capdata00;
struct capdata00 cd00;
};
u32 step;
u32 min_value;
u32 max_value;
};
struct capdata_fan {
u32 id;
u32 min_rpm;
u32 max_rpm;
};
typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list);
struct lwmi_cd_binder {
struct cd_list *cd00_list;
struct cd_list *cd01_list;
/*
* May be called during or after the bind callback.
* Will be called with NULL if capdata_fan does not exist.
* The pointer is only valid in the callback; never keep it for later use!
*/
cd_list_cb_t cd_fan_list_cb;
};
/**
* lwmi_attr_id() - Formats a capability data attribute ID
* @dev_id: The u8 corresponding to the device ID.
* @feat_id: The u8 corresponding to the feature ID on the device.
* @mode_id: The u8 corresponding to the wmi-gamezone mode for set/get.
* @type_id: The u8 corresponding to the sub-device.
*
* Return: encoded capability data attribute ID.
*/
static inline u32 lwmi_attr_id(u8 dev_id, u8 feat_id, u8 mode_id, u8 type_id)
{
return (FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, dev_id) |
FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, feat_id) |
FIELD_PREP(LWMI_ATTR_MODE_ID_MASK, mode_id) |
FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, type_id));
}
void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
int lwmi_cd00_get_data(struct cd_list *list, u32 attribute_id, struct capdata00 *output);
int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
int lwmi_cd_fan_get_data(struct cd_list *list, u32 attribute_id, struct capdata_fan *output);
#endif /* !_LENOVO_WMI_CAPDATA_H_ */
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Lenovo WMI Events driver. Lenovo WMI interfaces provide various
* hardware triggered events that many drivers need to have propagated.
* This driver provides a uniform entrypoint for these events so that
* any driver that needs to respond to these events can subscribe to a
* notifier chain.
*
* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
*/
#include <linux/acpi.h>
#include <linux/export.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/types.h>
#include <linux/wmi.h>
#include "wmi-events.h"
#include "wmi-helpers.h"
#define THERMAL_MODE_EVENT_GUID "D320289E-8FEA-41E0-86F9-911D83151B5F"
#define LWMI_EVENT_DEVICE(guid, type) \
.guid_string = (guid), .context = &(enum lwmi_events_type) \
{ \
type \
}
static BLOCKING_NOTIFIER_HEAD(events_chain_head);
struct lwmi_events_priv {
struct wmi_device *wdev;
enum lwmi_events_type type;
};
/**
* lwmi_events_register_notifier() - Add a notifier to the notifier chain.
* @nb: The notifier_block struct to register
*
* Call blocking_notifier_chain_register to register the notifier block to the
* lenovo-wmi-events driver blocking notifier chain.
*
* Return: 0 on success, %-EEXIST on error.
*/
int lwmi_events_register_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&events_chain_head, nb);
}
EXPORT_SYMBOL_NS_GPL(lwmi_events_register_notifier, "LENOVO_WMI_EVENTS");
/**
* lwmi_events_unregister_notifier() - Remove a notifier from the notifier
* chain.
* @nb: The notifier_block struct to unregister
*
* Call blocking_notifier_chain_unregister to unregister the notifier block
* from the lenovo-wmi-events driver blocking notifier chain.
*
* Return: 0 on success, %-ENOENT on error.
*/
int lwmi_events_unregister_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_unregister(&events_chain_head, nb);
}
EXPORT_SYMBOL_NS_GPL(lwmi_events_unregister_notifier, "LENOVO_WMI_EVENTS");
/**
* devm_lwmi_events_unregister_notifier() - Remove a notifier from the notifier
* chain.
* @data: Void pointer to the notifier_block struct to unregister.
*
* Call lwmi_events_unregister_notifier to unregister the notifier block from
* the lenovo-wmi-events driver blocking notifier chain.
*
* Return: 0 on success, %-ENOENT on error.
*/
static void devm_lwmi_events_unregister_notifier(void *data)
{
struct notifier_block *nb = data;
lwmi_events_unregister_notifier(nb);
}
/**
* devm_lwmi_events_register_notifier() - Add a notifier to the notifier chain.
* @dev: The parent device of the notifier_block struct.
* @nb: The notifier_block struct to register
*
* Call lwmi_events_register_notifier to register the notifier block to the
* lenovo-wmi-events driver blocking notifier chain. Then add, as a device
* managed action, unregister_notifier to automatically unregister the
* notifier block upon its parent device removal.
*
* Return: 0 on success, or an error code.
*/
int devm_lwmi_events_register_notifier(struct device *dev,
struct notifier_block *nb)
{
int ret;
ret = lwmi_events_register_notifier(nb);
if (ret < 0)
return ret;
return devm_add_action_or_reset(dev, devm_lwmi_events_unregister_notifier, nb);
}
EXPORT_SYMBOL_NS_GPL(devm_lwmi_events_register_notifier, "LENOVO_WMI_EVENTS");
/**
* lwmi_events_notify() - Call functions for the notifier call chain.
* @wdev: The parent WMI device of the driver.
* @obj: ACPI object passed by the registered WMI Event.
*
* Validate WMI event data and notify all registered drivers of the event and
* its output.
*
* Return: 0 on success, or an error code.
*/
static void lwmi_events_notify(struct wmi_device *wdev, union acpi_object *obj)
{
struct lwmi_events_priv *priv = dev_get_drvdata(&wdev->dev);
int sel_prof;
int ret;
switch (priv->type) {
case LWMI_EVENT_THERMAL_MODE:
if (obj->type != ACPI_TYPE_INTEGER)
return;
sel_prof = obj->integer.value;
switch (sel_prof) {
case LWMI_GZ_THERMAL_MODE_QUIET:
case LWMI_GZ_THERMAL_MODE_BALANCED:
case LWMI_GZ_THERMAL_MODE_PERFORMANCE:
case LWMI_GZ_THERMAL_MODE_EXTREME:
case LWMI_GZ_THERMAL_MODE_CUSTOM:
ret = blocking_notifier_call_chain(&events_chain_head,
LWMI_EVENT_THERMAL_MODE,
&sel_prof);
if (ret == NOTIFY_BAD)
dev_err(&wdev->dev,
"Failed to send notification to call chain for WMI Events\n");
return;
default:
dev_err(&wdev->dev, "Got invalid thermal mode: %x",
sel_prof);
return;
}
break;
default:
return;
}
}
static int lwmi_events_probe(struct wmi_device *wdev, const void *context)
{
struct lwmi_events_priv *priv;
if (!context)
return -EINVAL;
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->wdev = wdev;
priv->type = *(enum lwmi_events_type *)context;
dev_set_drvdata(&wdev->dev, priv);
return 0;
}
static const struct wmi_device_id lwmi_events_id_table[] = {
{ LWMI_EVENT_DEVICE(THERMAL_MODE_EVENT_GUID, LWMI_EVENT_THERMAL_MODE) },
{}
};
static struct wmi_driver lwmi_events_driver = {
.driver = {
.name = "lenovo_wmi_events",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
.id_table = lwmi_events_id_table,
.min_event_size = sizeof(u32),
.probe = lwmi_events_probe,
.notify = lwmi_events_notify,
.no_singleton = true,
};
module_wmi_driver(lwmi_events_driver);
MODULE_DEVICE_TABLE(wmi, lwmi_events_id_table);
MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
MODULE_DESCRIPTION("Lenovo WMI Events Driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,20 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com> */
#ifndef _LENOVO_WMI_EVENTS_H_
#define _LENOVO_WMI_EVENTS_H_
struct device;
struct notifier_block;
enum lwmi_events_type {
LWMI_EVENT_THERMAL_MODE = 1,
};
int lwmi_events_register_notifier(struct notifier_block *nb);
int lwmi_events_unregister_notifier(struct notifier_block *nb);
int devm_lwmi_events_register_notifier(struct device *dev,
struct notifier_block *nb);
#endif /* !_LENOVO_WMI_EVENTS_H_ */
@@ -0,0 +1,409 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Lenovo GameZone WMI interface driver.
*
* The GameZone WMI interface provides platform profile and fan curve settings
* for devices that fall under the "Gaming Series" of Lenovo Legion devices.
*
* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
*/
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/export.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/platform_profile.h>
#include <linux/spinlock.h>
#include <linux/spinlock_types.h>
#include <linux/types.h>
#include <linux/wmi.h>
#include "wmi-events.h"
#include "wmi-helpers.h"
#define LENOVO_GAMEZONE_GUID "887B54E3-DDDC-4B2C-8B88-68A26A8835D0"
#define LWMI_GZ_METHOD_ID_SMARTFAN_SUP 43
#define LWMI_GZ_METHOD_ID_SMARTFAN_SET 44
#define LWMI_GZ_METHOD_ID_SMARTFAN_GET 45
struct lwmi_gz_priv {
enum thermal_mode current_mode;
struct notifier_block event_nb;
struct notifier_block mode_nb;
spinlock_t gz_mode_lock; /* current_mode lock */
struct wmi_device *wdev;
int extreme_supported;
struct device *ppdev;
};
struct quirk_entry {
bool extreme_supported;
};
static struct quirk_entry quirk_no_extreme_bug = {
.extreme_supported = false,
};
/**
* lwmi_gz_mode_call() - Call method for lenovo-wmi-other driver notifier.
*
* @nb: The notifier_block registered to lenovo-wmi-other driver.
* @cmd: The event type.
* @data: Thermal mode enum pointer pointer for returning the thermal mode.
*
* For LWMI_GZ_GET_THERMAL_MODE, retrieve the current thermal mode.
*
* Return: Notifier_block status.
*/
static int lwmi_gz_mode_call(struct notifier_block *nb, unsigned long cmd,
void *data)
{
enum thermal_mode **mode = data;
struct lwmi_gz_priv *priv;
priv = container_of(nb, struct lwmi_gz_priv, mode_nb);
switch (cmd) {
case LWMI_GZ_GET_THERMAL_MODE:
scoped_guard(spinlock, &priv->gz_mode_lock) {
**mode = priv->current_mode;
}
return NOTIFY_OK;
default:
return NOTIFY_DONE;
}
}
/**
* lwmi_gz_event_call() - Call method for lenovo-wmi-events driver notifier.
* block call chain.
* @nb: The notifier_block registered to lenovo-wmi-events driver.
* @cmd: The event type.
* @data: The data to be updated by the event.
*
* For LWMI_EVENT_THERMAL_MODE, set current_mode and notify platform_profile
* of a change.
*
* Return: notifier_block status.
*/
static int lwmi_gz_event_call(struct notifier_block *nb, unsigned long cmd,
void *data)
{
enum thermal_mode *mode = data;
struct lwmi_gz_priv *priv;
priv = container_of(nb, struct lwmi_gz_priv, event_nb);
switch (cmd) {
case LWMI_EVENT_THERMAL_MODE:
scoped_guard(spinlock, &priv->gz_mode_lock) {
priv->current_mode = *mode;
}
platform_profile_notify(priv->ppdev);
return NOTIFY_STOP;
default:
return NOTIFY_DONE;
}
}
/**
* lwmi_gz_thermal_mode_supported() - Get the version of the WMI
* interface to determine the support level.
* @wdev: The Gamezone WMI device.
* @supported: Pointer to return the support level with.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_gz_thermal_mode_supported(struct wmi_device *wdev,
int *supported)
{
return lwmi_dev_evaluate_int(wdev, 0x0, LWMI_GZ_METHOD_ID_SMARTFAN_SUP,
NULL, 0, supported);
}
/**
* lwmi_gz_thermal_mode_get() - Get the current thermal mode.
* @wdev: The Gamezone interface WMI device.
* @mode: Pointer to return the thermal mode with.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_gz_thermal_mode_get(struct wmi_device *wdev,
enum thermal_mode *mode)
{
return lwmi_dev_evaluate_int(wdev, 0x0, LWMI_GZ_METHOD_ID_SMARTFAN_GET,
NULL, 0, mode);
}
/**
* lwmi_gz_profile_get() - Get the current platform profile.
* @dev: the Gamezone interface parent device.
* @profile: Pointer to provide the current platform profile with.
*
* Call lwmi_gz_thermal_mode_get and convert the thermal mode into a platform
* profile based on the support level of the interface.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_gz_profile_get(struct device *dev,
enum platform_profile_option *profile)
{
struct lwmi_gz_priv *priv = dev_get_drvdata(dev);
enum thermal_mode mode;
int ret;
ret = lwmi_gz_thermal_mode_get(priv->wdev, &mode);
if (ret)
return ret;
switch (mode) {
case LWMI_GZ_THERMAL_MODE_QUIET:
*profile = PLATFORM_PROFILE_LOW_POWER;
break;
case LWMI_GZ_THERMAL_MODE_BALANCED:
*profile = PLATFORM_PROFILE_BALANCED;
break;
case LWMI_GZ_THERMAL_MODE_PERFORMANCE:
*profile = PLATFORM_PROFILE_PERFORMANCE;
break;
case LWMI_GZ_THERMAL_MODE_EXTREME:
*profile = PLATFORM_PROFILE_MAX_POWER;
break;
case LWMI_GZ_THERMAL_MODE_CUSTOM:
*profile = PLATFORM_PROFILE_CUSTOM;
break;
default:
return -EINVAL;
}
guard(spinlock)(&priv->gz_mode_lock);
priv->current_mode = mode;
return 0;
}
/**
* lwmi_gz_profile_set() - Set the current platform profile.
* @dev: The Gamezone interface parent device.
* @profile: Pointer to the desired platform profile.
*
* Convert the given platform profile into a thermal mode based on the support
* level of the interface, then call the WMI method to set the thermal mode.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_gz_profile_set(struct device *dev,
enum platform_profile_option profile)
{
struct lwmi_gz_priv *priv = dev_get_drvdata(dev);
struct wmi_method_args_32 args = {};
enum thermal_mode mode;
int ret;
switch (profile) {
case PLATFORM_PROFILE_LOW_POWER:
mode = LWMI_GZ_THERMAL_MODE_QUIET;
break;
case PLATFORM_PROFILE_BALANCED:
mode = LWMI_GZ_THERMAL_MODE_BALANCED;
break;
case PLATFORM_PROFILE_PERFORMANCE:
mode = LWMI_GZ_THERMAL_MODE_PERFORMANCE;
break;
case PLATFORM_PROFILE_MAX_POWER:
mode = LWMI_GZ_THERMAL_MODE_EXTREME;
break;
case PLATFORM_PROFILE_CUSTOM:
mode = LWMI_GZ_THERMAL_MODE_CUSTOM;
break;
default:
return -EOPNOTSUPP;
}
args.arg0 = mode;
ret = lwmi_dev_evaluate_int(priv->wdev, 0x0,
LWMI_GZ_METHOD_ID_SMARTFAN_SET,
(u8 *)&args, sizeof(args), NULL);
if (ret)
return ret;
guard(spinlock)(&priv->gz_mode_lock);
priv->current_mode = mode;
return 0;
}
static const struct dmi_system_id fwbug_list[] = {
{
.ident = "Legion Go 8APU1",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8APU1"),
},
.driver_data = &quirk_no_extreme_bug,
},
{
.ident = "Legion Go S 8APU1",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8APU1"),
},
.driver_data = &quirk_no_extreme_bug,
},
{
.ident = "Legion Go S 8ARP1",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8ARP1"),
},
.driver_data = &quirk_no_extreme_bug,
},
{
.ident = "Legion Go 8ASP2",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8ASP2"),
},
.driver_data = &quirk_no_extreme_bug,
},
{
.ident = "Legion Go 8AHP2",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8AHP2"),
},
.driver_data = &quirk_no_extreme_bug,
},
{},
};
/**
* lwmi_gz_extreme_supported() - Evaluate if a device supports extreme thermal mode.
* @profile_support_ver: Version of the WMI interface.
*
* Determine if the extreme thermal mode is supported by the hardware.
* Anything version 5 or lower does not. For devices with a version 6 or
* greater do a DMI check, as some devices report a version that supports
* extreme mode but have an incomplete entry in the BIOS. To ensure this
* cannot be set, quirk them to prevent assignment.
*
* Return: bool.
*/
static bool lwmi_gz_extreme_supported(int profile_support_ver)
{
const struct dmi_system_id *dmi_id;
struct quirk_entry *quirks;
if (profile_support_ver < 6)
return false;
dmi_id = dmi_first_match(fwbug_list);
if (!dmi_id)
return true;
quirks = dmi_id->driver_data;
return quirks->extreme_supported;
}
/**
* lwmi_gz_platform_profile_probe - Enable and set up the platform profile
* device.
* @drvdata: Driver data for the interface.
* @choices: Container for enabled platform profiles.
*
* Determine if thermal mode is supported, and if so to what feature level.
* Then enable all supported platform profiles.
*
* Return: 0 on success, or an error code.
*/
static int lwmi_gz_platform_profile_probe(void *drvdata, unsigned long *choices)
{
struct lwmi_gz_priv *priv = drvdata;
int profile_support_ver;
int ret;
ret = lwmi_gz_thermal_mode_supported(priv->wdev, &profile_support_ver);
if (ret)
return ret;
if (profile_support_ver < 1)
return -ENODEV;
set_bit(PLATFORM_PROFILE_LOW_POWER, choices);
set_bit(PLATFORM_PROFILE_BALANCED, choices);
set_bit(PLATFORM_PROFILE_PERFORMANCE, choices);
set_bit(PLATFORM_PROFILE_CUSTOM, choices);
priv->extreme_supported = lwmi_gz_extreme_supported(profile_support_ver);
if (priv->extreme_supported)
set_bit(PLATFORM_PROFILE_MAX_POWER, choices);
return 0;
}
static const struct platform_profile_ops lwmi_gz_platform_profile_ops = {
.probe = lwmi_gz_platform_profile_probe,
.profile_get = lwmi_gz_profile_get,
.profile_set = lwmi_gz_profile_set,
};
static int lwmi_gz_probe(struct wmi_device *wdev, const void *context)
{
struct lwmi_gz_priv *priv;
int ret;
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->wdev = wdev;
dev_set_drvdata(&wdev->dev, priv);
priv->ppdev = devm_platform_profile_register(&wdev->dev, "lenovo-wmi-gamezone",
priv, &lwmi_gz_platform_profile_ops);
if (IS_ERR(priv->ppdev))
return -ENODEV;
spin_lock_init(&priv->gz_mode_lock);
ret = lwmi_gz_thermal_mode_get(wdev, &priv->current_mode);
if (ret)
return ret;
priv->event_nb.notifier_call = lwmi_gz_event_call;
ret = devm_lwmi_events_register_notifier(&wdev->dev, &priv->event_nb);
if (ret)
return ret;
priv->mode_nb.notifier_call = lwmi_gz_mode_call;
return devm_lwmi_tm_register_notifier(&wdev->dev, &priv->mode_nb);
}
static const struct wmi_device_id lwmi_gz_id_table[] = {
{ LENOVO_GAMEZONE_GUID, NULL },
{}
};
static struct wmi_driver lwmi_gz_driver = {
.driver = {
.name = "lenovo_wmi_gamezone",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
.id_table = lwmi_gz_id_table,
.probe = lwmi_gz_probe,
.no_singleton = true,
};
module_wmi_driver(lwmi_gz_driver);
MODULE_IMPORT_NS("LENOVO_WMI_EVENTS");
MODULE_IMPORT_NS("LENOVO_WMI_HELPERS");
MODULE_DEVICE_TABLE(wmi, lwmi_gz_id_table);
MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
MODULE_DESCRIPTION("Lenovo GameZone WMI Driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,224 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Lenovo Legion WMI helpers driver.
*
* The Lenovo Legion WMI interface is broken up into multiple GUID interfaces
* that require cross-references between GUID's for some functionality. The
* "Custom Mode" interface is a legacy interface for managing and displaying
* CPU & GPU power and hwmon settings and readings. The "Other Mode" interface
* is a modern interface that replaces or extends the "Custom Mode" interface
* methods. The "Gamezone" interface adds advanced features such as fan
* profiles and overclocking. The "Lighting" interface adds control of various
* status lights related to different hardware components. Each of these
* drivers uses a common procedure to get data from the WMI interface,
* enumerated here.
*
* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
*/
#include <linux/acpi.h>
#include <linux/cleanup.h>
#include <linux/debugfs.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/export.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/unaligned.h>
#include <linux/wmi.h>
#include "wmi-helpers.h"
/* Thermal mode notifier chain. */
static BLOCKING_NOTIFIER_HEAD(tm_chain_head);
/**
* lwmi_dev_evaluate_int() - Helper function for calling WMI methods that
* return an integer.
* @wdev: Pointer to the WMI device to be called.
* @instance: Instance of the called method.
* @method_id: WMI Method ID for the method to be called.
* @buf: Buffer of all arguments for the given method_id.
* @size: Length of the buffer.
* @retval: Pointer for the return value to be assigned.
*
* Calls wmidev_evaluate_method for Lenovo WMI devices that return an ACPI
* integer. Validates the return value type and assigns the value to the
* retval pointer.
*
* Return: 0 on success, or an error code.
*/
int lwmi_dev_evaluate_int(struct wmi_device *wdev, u8 instance, u32 method_id,
unsigned char *buf, size_t size, u32 *retval)
{
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
struct acpi_buffer input = { size, buf };
acpi_status status;
status = wmidev_evaluate_method(wdev, instance, method_id, &input,
&output);
if (ACPI_FAILURE(status))
return -EIO;
union acpi_object *ret_obj __free(kfree) = output.pointer;
if (retval) {
if (!ret_obj)
return -ENODATA;
switch (ret_obj->type) {
/*
* The ACPI method may simply return a buffer when a u32
* is expected. This is valid on Windows as its WMI-ACPI
* driver converts everything to a common buffer.
*/
case ACPI_TYPE_BUFFER:
if (ret_obj->buffer.length < sizeof(u32))
return -ENXIO;
*retval = get_unaligned_le32(ret_obj->buffer.pointer);
return 0;
case ACPI_TYPE_INTEGER:
*retval = (u32)ret_obj->integer.value;
return 0;
default:
return -ENXIO;
}
}
return 0;
};
EXPORT_SYMBOL_NS_GPL(lwmi_dev_evaluate_int, "LENOVO_WMI_HELPERS");
/**
* lwmi_tm_register_notifier() - Add a notifier to the blocking notifier chain
* @nb: The notifier_block struct to register
*
* Call blocking_notifier_chain_register to register the notifier block to the
* thermal mode notifier chain.
*
* Return: 0 on success, %-EEXIST on error.
*/
int lwmi_tm_register_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&tm_chain_head, nb);
}
EXPORT_SYMBOL_NS_GPL(lwmi_tm_register_notifier, "LENOVO_WMI_HELPERS");
/**
* lwmi_tm_unregister_notifier() - Remove a notifier from the blocking notifier
* chain.
* @nb: The notifier_block struct to register
*
* Call blocking_notifier_chain_unregister to unregister the notifier block from the
* thermal mode notifier chain.
*
* Return: 0 on success, %-ENOENT on error.
*/
int lwmi_tm_unregister_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_unregister(&tm_chain_head, nb);
}
EXPORT_SYMBOL_NS_GPL(lwmi_tm_unregister_notifier, "LENOVO_WMI_HELPERS");
/**
* devm_lwmi_tm_unregister_notifier() - Remove a notifier from the blocking
* notifier chain.
* @data: Void pointer to the notifier_block struct to register.
*
* Call lwmi_tm_unregister_notifier to unregister the notifier block from the
* thermal mode notifier chain.
*
* Return: 0 on success, %-ENOENT on error.
*/
static void devm_lwmi_tm_unregister_notifier(void *data)
{
struct notifier_block *nb = data;
lwmi_tm_unregister_notifier(nb);
}
/**
* devm_lwmi_tm_register_notifier() - Add a notifier to the blocking notifier
* chain.
* @dev: The parent device of the notifier_block struct.
* @nb: The notifier_block struct to register
*
* Call lwmi_tm_register_notifier to register the notifier block to the
* thermal mode notifier chain. Then add devm_lwmi_tm_unregister_notifier
* as a device managed action to automatically unregister the notifier block
* upon parent device removal.
*
* Return: 0 on success, or an error code.
*/
int devm_lwmi_tm_register_notifier(struct device *dev,
struct notifier_block *nb)
{
int ret;
ret = lwmi_tm_register_notifier(nb);
if (ret < 0)
return ret;
return devm_add_action_or_reset(dev, devm_lwmi_tm_unregister_notifier,
nb);
}
EXPORT_SYMBOL_NS_GPL(devm_lwmi_tm_register_notifier, "LENOVO_WMI_HELPERS");
/**
* lwmi_tm_notifier_call() - Call functions for the notifier call chain.
* @mode: Pointer to a thermal mode enum to retrieve the data from.
*
* Call blocking_notifier_call_chain to retrieve the thermal mode from the
* lenovo-wmi-gamezone driver.
*
* Return: 0 on success, or an error code.
*/
int lwmi_tm_notifier_call(enum thermal_mode *mode)
{
int ret;
ret = blocking_notifier_call_chain(&tm_chain_head,
LWMI_GZ_GET_THERMAL_MODE, &mode);
if ((ret & ~NOTIFY_STOP_MASK) != NOTIFY_OK)
return -EINVAL;
return 0;
}
EXPORT_SYMBOL_NS_GPL(lwmi_tm_notifier_call, "LENOVO_WMI_HELPERS");
static struct dentry *lwmi_debugfs_dir;
/**
* lwmi_debugfs_create_dir() - Helper function for creating a debugfs directory
* for a device.
* @wdev: Pointer to the WMI device to be called.
*
* Caller must remove the directory with debugfs_remove_recursive() on device
* removal.
*
* Return: Pointer to the created directory.
*/
struct dentry *lwmi_debugfs_create_dir(struct wmi_device *wdev)
{
return debugfs_create_dir(dev_name(&wdev->dev), lwmi_debugfs_dir);
}
EXPORT_SYMBOL_NS_GPL(lwmi_debugfs_create_dir, "LENOVO_WMI_HELPERS");
static int __init lwmi_helpers_init(void)
{
lwmi_debugfs_dir = debugfs_create_dir("lenovo_wmi", NULL);
return 0;
}
subsys_initcall(lwmi_helpers_init)
static void __exit lwmi_helpers_exit(void)
{
debugfs_remove_recursive(lwmi_debugfs_dir);
}
module_exit(lwmi_helpers_exit)
MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
MODULE_DESCRIPTION("Lenovo WMI Helpers Driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com> */
#ifndef _LENOVO_WMI_HELPERS_H_
#define _LENOVO_WMI_HELPERS_H_
#include <linux/types.h>
struct device;
struct notifier_block;
struct wmi_device;
struct wmi_method_args_32 {
u32 arg0;
u32 arg1;
};
struct dentry *lwmi_debugfs_create_dir(struct wmi_device *wdev);
enum lwmi_event_type {
LWMI_GZ_GET_THERMAL_MODE = 0x01,
};
enum thermal_mode {
LWMI_GZ_THERMAL_MODE_NONE = 0x00,
LWMI_GZ_THERMAL_MODE_QUIET = 0x01,
LWMI_GZ_THERMAL_MODE_BALANCED = 0x02,
LWMI_GZ_THERMAL_MODE_PERFORMANCE = 0x03,
LWMI_GZ_THERMAL_MODE_EXTREME = 0xE0, /* Ver 6+ */
LWMI_GZ_THERMAL_MODE_CUSTOM = 0xFF,
};
int lwmi_dev_evaluate_int(struct wmi_device *wdev, u8 instance, u32 method_id,
unsigned char *buf, size_t size, u32 *retval);
int lwmi_tm_register_notifier(struct notifier_block *nb);
int lwmi_tm_unregister_notifier(struct notifier_block *nb);
int devm_lwmi_tm_register_notifier(struct device *dev,
struct notifier_block *nb);
int lwmi_tm_notifier_call(enum thermal_mode *mode);
#endif /* !_LENOVO_WMI_HELPERS_H_ */
@@ -0,0 +1,224 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Lenovo Super Hotkey Utility WMI extras driver for Ideapad laptop
*
* Copyright (C) 2025 Lenovo
*/
#include <linux/cleanup.h>
#include <linux/dev_printk.h>
#include <linux/device.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/wmi.h>
/* Lenovo Super Hotkey WMI GUIDs */
#define LUD_WMI_METHOD_GUID "CE6C0974-0407-4F50-88BA-4FC3B6559AD8"
/* Lenovo Utility Data WMI method_id */
#define WMI_LUD_GET_SUPPORT 1
#define WMI_LUD_SET_FEATURE 2
#define WMI_LUD_GET_MICMUTE_LED_VER 20
#define WMI_LUD_GET_AUDIOMUTE_LED_VER 26
#define WMI_LUD_SUPPORT_MICMUTE_LED_VER 25
#define WMI_LUD_SUPPORT_AUDIOMUTE_LED_VER 27
/* Input parameters to mute/unmute audio LED and Mic LED */
struct wmi_led_args {
u8 id;
u8 subid;
u16 value;
};
/* Values of input parameters to SetFeature of audio LED and Mic LED */
enum hotkey_set_feature {
MIC_MUTE_LED_ON = 1,
MIC_MUTE_LED_OFF = 2,
AUDIO_MUTE_LED_ON = 4,
AUDIO_MUTE_LED_OFF = 5,
};
#define LSH_ACPI_LED_MAX 2
struct lenovo_super_hotkey_wmi_private {
struct led_classdev cdev[LSH_ACPI_LED_MAX];
struct wmi_device *led_wdev;
};
enum mute_led_type {
MIC_MUTE,
AUDIO_MUTE,
};
static int lsh_wmi_mute_led_set(enum mute_led_type led_type, struct led_classdev *led_cdev,
enum led_brightness brightness)
{
struct lenovo_super_hotkey_wmi_private *wpriv = container_of(led_cdev,
struct lenovo_super_hotkey_wmi_private, cdev[led_type]);
struct wmi_led_args led_arg = {0, 0, 0};
struct acpi_buffer input;
acpi_status status;
switch (led_type) {
case MIC_MUTE:
led_arg.id = brightness == LED_ON ? MIC_MUTE_LED_ON : MIC_MUTE_LED_OFF;
break;
case AUDIO_MUTE:
led_arg.id = brightness == LED_ON ? AUDIO_MUTE_LED_ON : AUDIO_MUTE_LED_OFF;
break;
default:
return -EINVAL;
}
input.length = sizeof(led_arg);
input.pointer = &led_arg;
status = wmidev_evaluate_method(wpriv->led_wdev, 0, WMI_LUD_SET_FEATURE, &input, NULL);
if (ACPI_FAILURE(status))
return -EIO;
return 0;
}
static int lsh_wmi_audiomute_led_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
return lsh_wmi_mute_led_set(AUDIO_MUTE, led_cdev, brightness);
}
static int lsh_wmi_micmute_led_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
return lsh_wmi_mute_led_set(MIC_MUTE, led_cdev, brightness);
}
static int lenovo_super_hotkey_wmi_led_init(enum mute_led_type led_type, struct device *dev)
{
struct lenovo_super_hotkey_wmi_private *wpriv = dev_get_drvdata(dev);
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
struct acpi_buffer input;
int led_version, err = 0;
unsigned int wmiarg;
acpi_status status;
switch (led_type) {
case MIC_MUTE:
wmiarg = WMI_LUD_GET_MICMUTE_LED_VER;
break;
case AUDIO_MUTE:
wmiarg = WMI_LUD_GET_AUDIOMUTE_LED_VER;
break;
default:
return -EINVAL;
}
input.length = sizeof(wmiarg);
input.pointer = &wmiarg;
status = wmidev_evaluate_method(wpriv->led_wdev, 0, WMI_LUD_GET_SUPPORT, &input, &output);
if (ACPI_FAILURE(status))
return -EIO;
union acpi_object *obj __free(kfree) = output.pointer;
if (!obj || obj->type != ACPI_TYPE_INTEGER)
return -EIO;
led_version = obj->integer.value;
/*
* Output parameters define: 0 means mute LED is not supported, Non-zero means
* mute LED can be supported.
*/
if (led_version == 0)
return 0;
switch (led_type) {
case MIC_MUTE:
if (led_version != WMI_LUD_SUPPORT_MICMUTE_LED_VER) {
pr_warn("The MIC_MUTE LED of this device isn't supported.\n");
return 0;
}
wpriv->cdev[led_type].name = "platform::micmute";
wpriv->cdev[led_type].brightness_set_blocking = &lsh_wmi_micmute_led_set;
wpriv->cdev[led_type].default_trigger = "audio-micmute";
break;
case AUDIO_MUTE:
if (led_version != WMI_LUD_SUPPORT_AUDIOMUTE_LED_VER) {
pr_warn("The AUDIO_MUTE LED of this device isn't supported.\n");
return 0;
}
wpriv->cdev[led_type].name = "platform::mute";
wpriv->cdev[led_type].brightness_set_blocking = &lsh_wmi_audiomute_led_set;
wpriv->cdev[led_type].default_trigger = "audio-mute";
break;
default:
dev_err(dev, "Unknown LED type %d\n", led_type);
return -EINVAL;
}
wpriv->cdev[led_type].max_brightness = LED_ON;
wpriv->cdev[led_type].flags = LED_CORE_SUSPENDRESUME;
err = devm_led_classdev_register(dev, &wpriv->cdev[led_type]);
if (err < 0) {
dev_err(dev, "Could not register mute LED %d : %d\n", led_type, err);
return err;
}
return 0;
}
static int lenovo_super_hotkey_wmi_leds_setup(struct device *dev)
{
int err;
err = lenovo_super_hotkey_wmi_led_init(MIC_MUTE, dev);
if (err)
return err;
err = lenovo_super_hotkey_wmi_led_init(AUDIO_MUTE, dev);
if (err)
return err;
return 0;
}
static int lenovo_super_hotkey_wmi_probe(struct wmi_device *wdev, const void *context)
{
struct lenovo_super_hotkey_wmi_private *wpriv;
wpriv = devm_kzalloc(&wdev->dev, sizeof(*wpriv), GFP_KERNEL);
if (!wpriv)
return -ENOMEM;
dev_set_drvdata(&wdev->dev, wpriv);
wpriv->led_wdev = wdev;
return lenovo_super_hotkey_wmi_leds_setup(&wdev->dev);
}
static const struct wmi_device_id lenovo_super_hotkey_wmi_id_table[] = {
{ LUD_WMI_METHOD_GUID, NULL }, /* Utility data */
{ }
};
MODULE_DEVICE_TABLE(wmi, lenovo_super_hotkey_wmi_id_table);
static struct wmi_driver lenovo_wmi_hotkey_utilities_driver = {
.driver = {
.name = "lenovo_wmi_hotkey_utilities",
.probe_type = PROBE_PREFER_ASYNCHRONOUS
},
.id_table = lenovo_super_hotkey_wmi_id_table,
.probe = lenovo_super_hotkey_wmi_probe,
.no_singleton = true,
};
module_wmi_driver(lenovo_wmi_hotkey_utilities_driver);
MODULE_AUTHOR("Jackie Dong <dongeg1@lenovo.com>");
MODULE_DESCRIPTION("Lenovo Super Hotkey Utility WMI extras driver");
MODULE_LICENSE("GPL");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* lenovo-ymc.c - Lenovo Yoga Mode Control driver
*
* Copyright © 2022 Gergo Koteles <soyer@irl.hu>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
#include <linux/wmi.h>
#include "ideapad-laptop.h"
#define LENOVO_YMC_EVENT_GUID "06129D99-6083-4164-81AD-F092F9D773A6"
#define LENOVO_YMC_QUERY_GUID "09B0EE6E-C3FD-4243-8DA1-7911FF80BB8C"
#define LENOVO_YMC_QUERY_INSTANCE 0
#define LENOVO_YMC_QUERY_METHOD 0x01
static bool force;
module_param(force, bool, 0444);
MODULE_PARM_DESC(force, "Force loading on boards without a convertible DMI chassis-type");
static const struct dmi_system_id allowed_chasis_types_dmi_table[] = {
{
.matches = {
DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "31" /* Convertible */),
},
},
{
.matches = {
DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "32" /* Detachable */),
},
},
{ }
};
struct lenovo_ymc_private {
struct input_dev *input_dev;
};
static const struct key_entry lenovo_ymc_keymap[] = {
/* Ignore the uninitialized state */
{ KE_IGNORE, 0x00 },
/* Laptop */
{ KE_SW, 0x01, { .sw = { SW_TABLET_MODE, 0 } } },
/* Tablet */
{ KE_SW, 0x02, { .sw = { SW_TABLET_MODE, 1 } } },
/* Drawing Board */
{ KE_SW, 0x03, { .sw = { SW_TABLET_MODE, 1 } } },
/* Tent */
{ KE_SW, 0x04, { .sw = { SW_TABLET_MODE, 1 } } },
{ KE_END },
};
static void lenovo_ymc_notify(struct wmi_device *wdev, union acpi_object *data)
{
struct lenovo_ymc_private *priv = dev_get_drvdata(&wdev->dev);
u32 input_val = 0;
struct acpi_buffer input = { sizeof(input_val), &input_val };
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
union acpi_object *obj;
acpi_status status;
int code;
status = wmi_evaluate_method(LENOVO_YMC_QUERY_GUID,
LENOVO_YMC_QUERY_INSTANCE,
LENOVO_YMC_QUERY_METHOD,
&input, &output);
if (ACPI_FAILURE(status)) {
dev_warn(&wdev->dev,
"Failed to evaluate query method: %s\n",
acpi_format_exception(status));
return;
}
obj = output.pointer;
if (obj->type != ACPI_TYPE_INTEGER) {
dev_warn(&wdev->dev,
"WMI event data is not an integer\n");
goto free_obj;
}
code = obj->integer.value;
if (!sparse_keymap_report_event(priv->input_dev, code, 1, true))
dev_warn(&wdev->dev, "Unknown key %d pressed\n", code);
free_obj:
kfree(obj);
ideapad_laptop_call_notifier(IDEAPAD_LAPTOP_YMC_EVENT, &code);
}
static int lenovo_ymc_probe(struct wmi_device *wdev, const void *ctx)
{
struct lenovo_ymc_private *priv;
struct input_dev *input_dev;
int err;
if (!dmi_check_system(allowed_chasis_types_dmi_table)) {
if (force)
dev_info(&wdev->dev, "Force loading Lenovo YMC support\n");
else
return -ENODEV;
}
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
input_dev = devm_input_allocate_device(&wdev->dev);
if (!input_dev)
return -ENOMEM;
input_dev->name = "Lenovo Yoga Tablet Mode Control switch";
input_dev->phys = LENOVO_YMC_EVENT_GUID "/input0";
input_dev->id.bustype = BUS_HOST;
input_dev->dev.parent = &wdev->dev;
err = sparse_keymap_setup(input_dev, lenovo_ymc_keymap, NULL);
if (err) {
dev_err(&wdev->dev,
"Could not set up input device keymap: %d\n", err);
return err;
}
err = input_register_device(input_dev);
if (err) {
dev_err(&wdev->dev,
"Could not register input device: %d\n", err);
return err;
}
priv->input_dev = input_dev;
dev_set_drvdata(&wdev->dev, priv);
/* Report the state for the first time on probe */
lenovo_ymc_notify(wdev, NULL);
return 0;
}
static const struct wmi_device_id lenovo_ymc_wmi_id_table[] = {
{ .guid_string = LENOVO_YMC_EVENT_GUID },
{ }
};
MODULE_DEVICE_TABLE(wmi, lenovo_ymc_wmi_id_table);
static struct wmi_driver lenovo_ymc_driver = {
.driver = {
.name = "lenovo-ymc",
},
.id_table = lenovo_ymc_wmi_id_table,
.min_event_size = sizeof(u32),
.probe = lenovo_ymc_probe,
.notify = lenovo_ymc_notify,
};
module_wmi_driver(lenovo_ymc_driver);
MODULE_AUTHOR("Gergo Koteles <soyer@irl.hu>");
MODULE_DESCRIPTION("Lenovo Yoga Mode Control driver");
MODULE_LICENSE("GPL");
MODULE_IMPORT_NS("IDEAPAD_LAPTOP");
@@ -0,0 +1,333 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Support for the custom fast charging protocol found on the Lenovo Yoga
* Tablet 2 1380F / 1380L models.
*
* Copyright (C) 2024 Hans de Goede <hansg@kernel.org>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/delay.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/extcon.h>
#include <linux/gpio/consumer.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/pinctrl/consumer.h>
#include <linux/pinctrl/machine.h>
#include <linux/platform_device.h>
#include <linux/serdev.h>
#include <linux/time.h>
#include <linux/types.h>
#include <linux/workqueue.h>
#include "../serdev_helpers.h"
#define YT2_1380_FC_PDEV_NAME "lenovo-yoga-tab2-pro-1380-fastcharger"
#define YT2_1380_FC_SERDEV_CTRL "serial0"
#define YT2_1380_FC_SERDEV_NAME "serial0-0"
#define YT2_1380_FC_EXTCON_NAME "i2c-lc824206xa"
#define YT2_1380_FC_MAX_TRIES 5
#define YT2_1380_FC_PIN_SW_DELAY_US (10 * USEC_PER_MSEC)
#define YT2_1380_FC_UART_DRAIN_DELAY_US (50 * USEC_PER_MSEC)
#define YT2_1380_FC_VOLT_SW_DELAY_US (1000 * USEC_PER_MSEC)
struct yt2_1380_fc {
struct device *dev;
struct pinctrl *pinctrl;
struct pinctrl_state *gpio_state;
struct pinctrl_state *uart_state;
struct gpio_desc *uart3_txd;
struct gpio_desc *uart3_rxd;
struct extcon_dev *extcon;
struct notifier_block nb;
struct work_struct work;
bool fast_charging;
};
static int yt2_1380_fc_set_gpio_mode(struct yt2_1380_fc *fc, bool enable)
{
struct pinctrl_state *state = enable ? fc->gpio_state : fc->uart_state;
int ret;
ret = pinctrl_select_state(fc->pinctrl, state);
if (ret) {
dev_err(fc->dev, "Error %d setting pinctrl state\n", ret);
return ret;
}
fsleep(YT2_1380_FC_PIN_SW_DELAY_US);
return 0;
}
static bool yt2_1380_fc_dedicated_charger_connected(struct yt2_1380_fc *fc)
{
return extcon_get_state(fc->extcon, EXTCON_CHG_USB_DCP) > 0;
}
static bool yt2_1380_fc_fast_charger_connected(struct yt2_1380_fc *fc)
{
return extcon_get_state(fc->extcon, EXTCON_CHG_USB_FAST) > 0;
}
static void yt2_1380_fc_worker(struct work_struct *work)
{
struct yt2_1380_fc *fc = container_of(work, struct yt2_1380_fc, work);
int i, ret;
/* Do nothing if already fast charging */
if (yt2_1380_fc_fast_charger_connected(fc))
return;
for (i = 0; i < YT2_1380_FC_MAX_TRIES; i++) {
/* Set pins to UART mode (for charger disconnect and retries) */
ret = yt2_1380_fc_set_gpio_mode(fc, false);
if (ret)
return;
/* Only try 12V charging if a dedicated charger is detected */
if (!yt2_1380_fc_dedicated_charger_connected(fc))
return;
/* Send the command to switch to 12V charging */
ret = serdev_device_write_buf(to_serdev_device(fc->dev), "SC", strlen("SC"));
if (ret != strlen("SC")) {
dev_err(fc->dev, "Error %d writing to uart\n", ret);
return;
}
fsleep(YT2_1380_FC_UART_DRAIN_DELAY_US);
/* Re-check a charger is still connected */
if (!yt2_1380_fc_dedicated_charger_connected(fc))
return;
/*
* Now switch the lines to GPIO (output, high). The charger
* expects the lines being driven high after the command.
* Presumably this is used to detect the tablet getting
* unplugged (to switch back to 5V output on unplug).
*/
ret = yt2_1380_fc_set_gpio_mode(fc, true);
if (ret)
return;
fsleep(YT2_1380_FC_VOLT_SW_DELAY_US);
if (yt2_1380_fc_fast_charger_connected(fc))
return; /* Success */
}
dev_dbg(fc->dev, "Failed to switch to 12V charging (not the original charger?)\n");
/* Failed to enable 12V fast charging, reset pins to default UART mode */
yt2_1380_fc_set_gpio_mode(fc, false);
}
static int yt2_1380_fc_extcon_evt(struct notifier_block *nb,
unsigned long event, void *param)
{
struct yt2_1380_fc *fc = container_of(nb, struct yt2_1380_fc, nb);
schedule_work(&fc->work);
return NOTIFY_OK;
}
static size_t yt2_1380_fc_receive(struct serdev_device *serdev, const u8 *data, size_t len)
{
/*
* Since the USB data lines are shorted for DCP detection, echos of
* the "SC" command send in yt2_1380_fc_worker() will be received.
*/
dev_dbg(&serdev->dev, "recv: %*ph\n", (int)len, data);
return len;
}
static const struct serdev_device_ops yt2_1380_fc_serdev_ops = {
.receive_buf = yt2_1380_fc_receive,
.write_wakeup = serdev_device_write_wakeup,
};
static int yt2_1380_fc_serdev_probe(struct serdev_device *serdev)
{
struct device *dev = &serdev->dev;
struct yt2_1380_fc *fc;
int ret;
fc = devm_kzalloc(dev, sizeof(*fc), GFP_KERNEL);
if (!fc)
return -ENOMEM;
fc->dev = dev;
fc->nb.notifier_call = yt2_1380_fc_extcon_evt;
INIT_WORK(&fc->work, yt2_1380_fc_worker);
/*
* Do this first since it may return -EPROBE_DEFER.
* There is no extcon_put(), so there is no need to free this.
*/
fc->extcon = extcon_get_extcon_dev(YT2_1380_FC_EXTCON_NAME);
if (IS_ERR(fc->extcon))
return dev_err_probe(dev, PTR_ERR(fc->extcon), "getting extcon\n");
fc->pinctrl = devm_pinctrl_get(dev);
if (IS_ERR(fc->pinctrl))
return dev_err_probe(dev, PTR_ERR(fc->pinctrl), "getting pinctrl\n");
/*
* To switch the UART3 pins connected to the USB data lines between
* UART and GPIO modes.
*/
fc->gpio_state = pinctrl_lookup_state(fc->pinctrl, "uart3_gpio");
fc->uart_state = pinctrl_lookup_state(fc->pinctrl, "uart3_uart");
if (IS_ERR(fc->gpio_state) || IS_ERR(fc->uart_state))
return dev_err_probe(dev, -EINVAL, "getting pinctrl states\n");
ret = yt2_1380_fc_set_gpio_mode(fc, true);
if (ret)
return ret;
fc->uart3_txd = devm_gpiod_get(dev, "uart3_txd", GPIOD_OUT_HIGH);
if (IS_ERR(fc->uart3_txd))
return dev_err_probe(dev, PTR_ERR(fc->uart3_txd), "getting uart3_txd gpio\n");
fc->uart3_rxd = devm_gpiod_get(dev, "uart3_rxd", GPIOD_OUT_HIGH);
if (IS_ERR(fc->uart3_rxd))
return dev_err_probe(dev, PTR_ERR(fc->uart3_rxd), "getting uart3_rxd gpio\n");
ret = yt2_1380_fc_set_gpio_mode(fc, false);
if (ret)
return ret;
serdev_device_set_drvdata(serdev, fc);
serdev_device_set_client_ops(serdev, &yt2_1380_fc_serdev_ops);
ret = devm_serdev_device_open(dev, serdev);
if (ret)
return dev_err_probe(dev, ret, "opening UART device\n");
serdev_device_set_baudrate(serdev, 600);
serdev_device_set_flow_control(serdev, false);
ret = devm_extcon_register_notifier_all(dev, fc->extcon, &fc->nb);
if (ret)
return dev_err_probe(dev, ret, "registering extcon notifier\n");
/* In case the extcon already has detected a DCP charger */
schedule_work(&fc->work);
return 0;
}
static struct serdev_device_driver yt2_1380_fc_serdev_driver = {
.probe = yt2_1380_fc_serdev_probe,
.driver = {
.name = KBUILD_MODNAME,
},
};
static const struct pinctrl_map yt2_1380_fc_pinctrl_map[] = {
PIN_MAP_MUX_GROUP(YT2_1380_FC_SERDEV_NAME, "uart3_uart",
"INT33FC:00", "uart3_grp", "uart"),
PIN_MAP_MUX_GROUP(YT2_1380_FC_SERDEV_NAME, "uart3_gpio",
"INT33FC:00", "uart3_grp_gpio", "gpio"),
};
static int yt2_1380_fc_pdev_probe(struct platform_device *pdev)
{
struct serdev_device *serdev;
struct device *ctrl_dev;
int ret;
/* Register pinctrl mappings for setting the UART3 pins mode */
ret = devm_pinctrl_register_mappings(&pdev->dev, yt2_1380_fc_pinctrl_map,
ARRAY_SIZE(yt2_1380_fc_pinctrl_map));
if (ret)
return ret;
/* And create the serdev to talk to the charger over the UART3 pins */
ctrl_dev = get_serdev_controller("PNP0501", "1", 0, YT2_1380_FC_SERDEV_CTRL);
if (IS_ERR(ctrl_dev))
return PTR_ERR(ctrl_dev);
serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev));
put_device(ctrl_dev);
if (!serdev)
return -ENOMEM;
/* Propagate pdev-fwnode set by x86-android-tablets to serdev */
device_set_node(&serdev->dev, dev_fwnode(&pdev->dev));
/* The fwnode is a managed node, so it will be auto-put on serdev_device_put() */
fwnode_handle_get(dev_fwnode(&serdev->dev));
ret = serdev_device_add(serdev);
if (ret) {
serdev_device_put(serdev);
return dev_err_probe(&pdev->dev, ret, "adding serdev\n");
}
/*
* serdev device <-> driver matching relies on OF or ACPI matches and
* neither is available here, manually bind the driver.
*/
ret = device_driver_attach(&yt2_1380_fc_serdev_driver.driver, &serdev->dev);
if (ret) {
/* device_driver_attach() maps EPROBE_DEFER to EAGAIN, map it back */
serdev_device_remove(serdev);
return dev_err_probe(&pdev->dev,
(ret == -EAGAIN) ? -EPROBE_DEFER : ret,
"attaching serdev driver\n");
}
/* So that yt2_1380_fc_pdev_remove() can remove the serdev */
platform_set_drvdata(pdev, serdev);
return 0;
}
static void yt2_1380_fc_pdev_remove(struct platform_device *pdev)
{
struct serdev_device *serdev = platform_get_drvdata(pdev);
serdev_device_remove(serdev);
}
static struct platform_driver yt2_1380_fc_pdev_driver = {
.probe = yt2_1380_fc_pdev_probe,
.remove = yt2_1380_fc_pdev_remove,
.driver = {
.name = YT2_1380_FC_PDEV_NAME,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
static int __init yt2_1380_fc_module_init(void)
{
int ret;
/*
* serdev driver MUST be registered first because pdev driver calls
* device_driver_attach() on the serdev, serdev-driver pair.
*/
ret = serdev_device_driver_register(&yt2_1380_fc_serdev_driver);
if (ret)
return ret;
ret = platform_driver_register(&yt2_1380_fc_pdev_driver);
if (ret)
serdev_device_driver_unregister(&yt2_1380_fc_serdev_driver);
return ret;
}
module_init(yt2_1380_fc_module_init);
static void __exit yt2_1380_fc_module_exit(void)
{
platform_driver_unregister(&yt2_1380_fc_pdev_driver);
serdev_device_driver_unregister(&yt2_1380_fc_serdev_driver);
}
module_exit(yt2_1380_fc_module_exit);
MODULE_ALIAS("platform:" YT2_1380_FC_PDEV_NAME);
MODULE_DESCRIPTION("Lenovo Yoga Tablet 2 1380 fast charge driver");
MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,575 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Platform driver for Lenovo Yoga Book YB1-X90F/L tablets (Android model)
* WMI driver for Lenovo Yoga Book YB1-X91F/L tablets (Windows model)
*
* The keyboard half of the YB1 models can function as both a capacitive
* touch keyboard or as a Wacom digitizer, but not at the same time.
*
* This driver takes care of switching between the 2 functions.
*
* Copyright 2023 Hans de Goede <hansg@kernel.org>
*/
#include <linux/acpi.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio/machine.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/wmi.h>
#include <linux/workqueue.h>
#define YB_MBTN_EVENT_GUID "243FEC1D-1963-41C1-8100-06A9D82A94B4"
#define YB_KBD_BL_DEFAULT 128
#define YB_KBD_BL_MAX 255
#define YB_KBD_BL_PWM_PERIOD 13333
#define YB_PDEV_NAME "yogabook-touch-kbd-digitizer-switch"
/* flags */
enum {
YB_KBD_IS_ON,
YB_DIGITIZER_IS_ON,
YB_DIGITIZER_MODE,
YB_TABLET_MODE,
YB_SUSPENDED,
};
struct yogabook_data {
struct device *dev;
struct acpi_device *kbd_adev;
struct acpi_device *dig_adev;
struct device *kbd_dev;
struct device *dig_dev;
struct led_classdev *pen_led;
struct gpio_desc *pen_touch_event;
struct gpio_desc *kbd_bl_led_enable;
struct gpio_desc *backside_hall_gpio;
struct pwm_device *kbd_bl_pwm;
int (*set_kbd_backlight)(struct yogabook_data *data, uint8_t level);
int pen_touch_irq;
int backside_hall_irq;
struct work_struct work;
struct led_classdev kbd_bl_led;
unsigned long flags;
u8 brightness;
};
static void yogabook_work(struct work_struct *work)
{
struct yogabook_data *data = container_of(work, struct yogabook_data, work);
bool kbd_on, digitizer_on;
int r;
if (test_bit(YB_SUSPENDED, &data->flags))
return;
if (test_bit(YB_TABLET_MODE, &data->flags)) {
kbd_on = false;
digitizer_on = false;
} else if (test_bit(YB_DIGITIZER_MODE, &data->flags)) {
digitizer_on = true;
kbd_on = false;
} else {
kbd_on = true;
digitizer_on = false;
}
if (!kbd_on && test_bit(YB_KBD_IS_ON, &data->flags)) {
/*
* Must be done before releasing the keyboard touchscreen driver,
* so that the keyboard touchscreen dev is still in D0.
*/
data->set_kbd_backlight(data, 0);
device_release_driver(data->kbd_dev);
clear_bit(YB_KBD_IS_ON, &data->flags);
}
if (!digitizer_on && test_bit(YB_DIGITIZER_IS_ON, &data->flags)) {
led_set_brightness(data->pen_led, LED_OFF);
device_release_driver(data->dig_dev);
clear_bit(YB_DIGITIZER_IS_ON, &data->flags);
}
if (kbd_on && !test_bit(YB_KBD_IS_ON, &data->flags)) {
r = device_reprobe(data->kbd_dev);
if (r)
dev_warn(data->dev, "Reprobe of keyboard touchscreen failed: %d\n", r);
data->set_kbd_backlight(data, data->brightness);
set_bit(YB_KBD_IS_ON, &data->flags);
}
if (digitizer_on && !test_bit(YB_DIGITIZER_IS_ON, &data->flags)) {
r = device_reprobe(data->dig_dev);
if (r)
dev_warn(data->dev, "Reprobe of digitizer failed: %d\n", r);
led_set_brightness(data->pen_led, LED_FULL);
set_bit(YB_DIGITIZER_IS_ON, &data->flags);
}
}
static void yogabook_toggle_digitizer_mode(struct yogabook_data *data)
{
if (test_bit(YB_SUSPENDED, &data->flags))
return;
if (test_bit(YB_DIGITIZER_MODE, &data->flags))
clear_bit(YB_DIGITIZER_MODE, &data->flags);
else
set_bit(YB_DIGITIZER_MODE, &data->flags);
/*
* We are called from the ACPI core and the driver [un]binding which is
* done also needs ACPI functions, use a workqueue to avoid deadlocking.
*/
schedule_work(&data->work);
}
static irqreturn_t yogabook_backside_hall_irq(int irq, void *_data)
{
struct yogabook_data *data = _data;
if (gpiod_get_value(data->backside_hall_gpio))
set_bit(YB_TABLET_MODE, &data->flags);
else
clear_bit(YB_TABLET_MODE, &data->flags);
schedule_work(&data->work);
return IRQ_HANDLED;
}
#define kbd_led_to_yogabook(cdev) container_of(cdev, struct yogabook_data, kbd_bl_led)
static enum led_brightness kbd_brightness_get(struct led_classdev *cdev)
{
struct yogabook_data *data = kbd_led_to_yogabook(cdev);
return data->brightness;
}
static int kbd_brightness_set(struct led_classdev *cdev,
enum led_brightness value)
{
struct yogabook_data *data = kbd_led_to_yogabook(cdev);
if ((value < 0) || (value > YB_KBD_BL_MAX))
return -EINVAL;
data->brightness = value;
if (!test_bit(YB_KBD_IS_ON, &data->flags))
return 0;
return data->set_kbd_backlight(data, data->brightness);
}
static struct gpiod_lookup_table yogabook_gpios = {
.table = {
GPIO_LOOKUP("INT33FF:02", 18, "backside_hall_sw", GPIO_ACTIVE_LOW),
{}
},
};
static struct led_lookup_data yogabook_pen_led = {
.provider = "platform::indicator",
.con_id = "pen-icon-led",
};
static int yogabook_probe(struct device *dev, struct yogabook_data *data,
const char *kbd_bl_led_name)
{
int r;
data->dev = dev;
data->brightness = YB_KBD_BL_DEFAULT;
set_bit(YB_KBD_IS_ON, &data->flags);
set_bit(YB_DIGITIZER_IS_ON, &data->flags);
INIT_WORK(&data->work, yogabook_work);
yogabook_pen_led.dev_id = dev_name(dev);
led_add_lookup(&yogabook_pen_led);
data->pen_led = devm_led_get(dev, "pen-icon-led");
led_remove_lookup(&yogabook_pen_led);
if (IS_ERR(data->pen_led))
return dev_err_probe(dev, PTR_ERR(data->pen_led), "Getting pen icon LED\n");
yogabook_gpios.dev_id = dev_name(dev);
gpiod_add_lookup_table(&yogabook_gpios);
data->backside_hall_gpio = devm_gpiod_get(dev, "backside_hall_sw", GPIOD_IN);
gpiod_remove_lookup_table(&yogabook_gpios);
if (IS_ERR(data->backside_hall_gpio))
return dev_err_probe(dev, PTR_ERR(data->backside_hall_gpio),
"Getting backside_hall_sw GPIO\n");
r = gpiod_to_irq(data->backside_hall_gpio);
if (r < 0)
return dev_err_probe(dev, r, "Getting backside_hall_sw IRQ\n");
data->backside_hall_irq = r;
/* Set default brightness before enabling the IRQ */
data->set_kbd_backlight(data, YB_KBD_BL_DEFAULT);
r = request_irq(data->backside_hall_irq, yogabook_backside_hall_irq,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"backside_hall_sw", data);
if (r)
return dev_err_probe(dev, r, "Requesting backside_hall_sw IRQ\n");
schedule_work(&data->work);
data->kbd_bl_led.name = kbd_bl_led_name;
data->kbd_bl_led.brightness_set_blocking = kbd_brightness_set;
data->kbd_bl_led.brightness_get = kbd_brightness_get;
data->kbd_bl_led.max_brightness = YB_KBD_BL_MAX;
r = devm_led_classdev_register(dev, &data->kbd_bl_led);
if (r < 0) {
dev_err_probe(dev, r, "Registering backlight LED device\n");
goto error_free_irq;
}
dev_set_drvdata(dev, data);
return 0;
error_free_irq:
free_irq(data->backside_hall_irq, data);
cancel_work_sync(&data->work);
return r;
}
static void yogabook_remove(struct yogabook_data *data)
{
int r = 0;
free_irq(data->backside_hall_irq, data);
cancel_work_sync(&data->work);
if (!test_bit(YB_KBD_IS_ON, &data->flags))
r |= device_reprobe(data->kbd_dev);
if (!test_bit(YB_DIGITIZER_IS_ON, &data->flags))
r |= device_reprobe(data->dig_dev);
if (r)
dev_warn(data->dev, "Reprobe of devices failed\n");
}
static int yogabook_suspend(struct device *dev)
{
struct yogabook_data *data = dev_get_drvdata(dev);
set_bit(YB_SUSPENDED, &data->flags);
flush_work(&data->work);
if (test_bit(YB_KBD_IS_ON, &data->flags))
data->set_kbd_backlight(data, 0);
return 0;
}
static int yogabook_resume(struct device *dev)
{
struct yogabook_data *data = dev_get_drvdata(dev);
if (test_bit(YB_KBD_IS_ON, &data->flags))
data->set_kbd_backlight(data, data->brightness);
clear_bit(YB_SUSPENDED, &data->flags);
/* Check for YB_TABLET_MODE changes made during suspend */
schedule_work(&data->work);
return 0;
}
static DEFINE_SIMPLE_DEV_PM_OPS(yogabook_pm_ops, yogabook_suspend, yogabook_resume);
/********** WMI driver code **********/
/*
* To control keyboard backlight, call the method KBLC() of the TCS1 ACPI
* device (Goodix touchpad acts as virtual sensor keyboard).
*/
static int yogabook_wmi_set_kbd_backlight(struct yogabook_data *data,
uint8_t level)
{
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
struct acpi_object_list input;
union acpi_object param;
acpi_status status;
dev_dbg(data->dev, "Set KBLC level to %u\n", level);
/* Ensure keyboard touchpad is on before we call KBLC() */
acpi_device_set_power(data->kbd_adev, ACPI_STATE_D0);
input.count = 1;
input.pointer = &param;
param.type = ACPI_TYPE_INTEGER;
param.integer.value = YB_KBD_BL_MAX - level;
status = acpi_evaluate_object(acpi_device_handle(data->kbd_adev), "KBLC",
&input, &output);
if (ACPI_FAILURE(status)) {
dev_err(data->dev, "Failed to call KBLC method: 0x%x\n", status);
return status;
}
kfree(output.pointer);
return 0;
}
static int yogabook_wmi_probe(struct wmi_device *wdev, const void *context)
{
struct device *dev = &wdev->dev;
struct yogabook_data *data;
int r;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->kbd_adev = acpi_dev_get_first_match_dev("GDIX1001", NULL, -1);
if (!data->kbd_adev)
return dev_err_probe(dev, -ENODEV,
"Cannot find the touchpad device in ACPI tables\n");
data->dig_adev = acpi_dev_get_first_match_dev("WCOM0019", NULL, -1);
if (!data->dig_adev) {
r = dev_err_probe(dev, -ENODEV,
"Cannot find the digitizer device in ACPI tables\n");
goto error_put_devs;
}
data->kbd_dev = get_device(acpi_get_first_physical_node(data->kbd_adev));
if (!data->kbd_dev || !data->kbd_dev->driver) {
r = -EPROBE_DEFER;
goto error_put_devs;
}
data->dig_dev = get_device(acpi_get_first_physical_node(data->dig_adev));
if (!data->dig_dev || !data->dig_dev->driver) {
r = -EPROBE_DEFER;
goto error_put_devs;
}
data->set_kbd_backlight = yogabook_wmi_set_kbd_backlight;
r = yogabook_probe(dev, data, "ybwmi::kbd_backlight");
if (r)
goto error_put_devs;
return 0;
error_put_devs:
put_device(data->dig_dev);
put_device(data->kbd_dev);
acpi_dev_put(data->dig_adev);
acpi_dev_put(data->kbd_adev);
return r;
}
static void yogabook_wmi_remove(struct wmi_device *wdev)
{
struct yogabook_data *data = dev_get_drvdata(&wdev->dev);
yogabook_remove(data);
put_device(data->dig_dev);
put_device(data->kbd_dev);
acpi_dev_put(data->dig_adev);
acpi_dev_put(data->kbd_adev);
}
static void yogabook_wmi_notify(struct wmi_device *wdev, union acpi_object *dummy)
{
yogabook_toggle_digitizer_mode(dev_get_drvdata(&wdev->dev));
}
static const struct wmi_device_id yogabook_wmi_id_table[] = {
{
.guid_string = YB_MBTN_EVENT_GUID,
},
{ } /* Terminating entry */
};
MODULE_DEVICE_TABLE(wmi, yogabook_wmi_id_table);
static struct wmi_driver yogabook_wmi_driver = {
.driver = {
.name = "yogabook-wmi",
.pm = pm_sleep_ptr(&yogabook_pm_ops),
},
.id_table = yogabook_wmi_id_table,
.min_event_size = 0,
.probe = yogabook_wmi_probe,
.remove = yogabook_wmi_remove,
.notify = yogabook_wmi_notify,
};
/********** platform driver code **********/
static struct gpiod_lookup_table yogabook_pdev_gpios = {
.dev_id = YB_PDEV_NAME,
.table = {
GPIO_LOOKUP("INT33FF:00", 95, "pen_touch_event", GPIO_ACTIVE_HIGH),
GPIO_LOOKUP("INT33FF:03", 52, "enable_keyboard_led", GPIO_ACTIVE_HIGH),
{}
},
};
static int yogabook_pdev_set_kbd_backlight(struct yogabook_data *data, u8 level)
{
struct pwm_state state = {
.period = YB_KBD_BL_PWM_PERIOD,
.duty_cycle = YB_KBD_BL_PWM_PERIOD * level / YB_KBD_BL_MAX,
.enabled = level,
};
pwm_apply_might_sleep(data->kbd_bl_pwm, &state);
gpiod_set_value(data->kbd_bl_led_enable, level ? 1 : 0);
return 0;
}
static irqreturn_t yogabook_pen_touch_irq(int irq, void *data)
{
yogabook_toggle_digitizer_mode(data);
return IRQ_HANDLED;
}
static int yogabook_pdev_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct yogabook_data *data;
int r;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->kbd_dev = bus_find_device_by_name(&i2c_bus_type, NULL, "i2c-goodix_ts");
if (!data->kbd_dev || !data->kbd_dev->driver) {
r = -EPROBE_DEFER;
goto error_put_devs;
}
data->dig_dev = bus_find_device_by_name(&i2c_bus_type, NULL, "i2c-wacom");
if (!data->dig_dev || !data->dig_dev->driver) {
r = -EPROBE_DEFER;
goto error_put_devs;
}
gpiod_add_lookup_table(&yogabook_pdev_gpios);
data->pen_touch_event = devm_gpiod_get(dev, "pen_touch_event", GPIOD_IN);
data->kbd_bl_led_enable = devm_gpiod_get(dev, "enable_keyboard_led", GPIOD_OUT_HIGH);
gpiod_remove_lookup_table(&yogabook_pdev_gpios);
if (IS_ERR(data->pen_touch_event)) {
r = dev_err_probe(dev, PTR_ERR(data->pen_touch_event),
"Getting pen_touch_event GPIO\n");
goto error_put_devs;
}
if (IS_ERR(data->kbd_bl_led_enable)) {
r = dev_err_probe(dev, PTR_ERR(data->kbd_bl_led_enable),
"Getting enable_keyboard_led GPIO\n");
goto error_put_devs;
}
data->kbd_bl_pwm = devm_pwm_get(dev, "pwm_soc_lpss_2");
if (IS_ERR(data->kbd_bl_pwm)) {
r = dev_err_probe(dev, PTR_ERR(data->kbd_bl_pwm),
"Getting keyboard backlight PWM\n");
goto error_put_devs;
}
r = gpiod_to_irq(data->pen_touch_event);
if (r < 0) {
dev_err_probe(dev, r, "Getting pen_touch_event IRQ\n");
goto error_put_devs;
}
data->pen_touch_irq = r;
r = request_irq(data->pen_touch_irq, yogabook_pen_touch_irq, IRQF_TRIGGER_FALLING,
"pen_touch_event", data);
if (r) {
dev_err_probe(dev, r, "Requesting pen_touch_event IRQ\n");
goto error_put_devs;
}
data->set_kbd_backlight = yogabook_pdev_set_kbd_backlight;
r = yogabook_probe(dev, data, "yogabook::kbd_backlight");
if (r)
goto error_free_irq;
return 0;
error_free_irq:
free_irq(data->pen_touch_irq, data);
cancel_work_sync(&data->work);
error_put_devs:
put_device(data->dig_dev);
put_device(data->kbd_dev);
return r;
}
static void yogabook_pdev_remove(struct platform_device *pdev)
{
struct yogabook_data *data = platform_get_drvdata(pdev);
yogabook_remove(data);
free_irq(data->pen_touch_irq, data);
cancel_work_sync(&data->work);
put_device(data->dig_dev);
put_device(data->kbd_dev);
}
static struct platform_driver yogabook_pdev_driver = {
.probe = yogabook_pdev_probe,
.remove = yogabook_pdev_remove,
.driver = {
.name = YB_PDEV_NAME,
.pm = pm_sleep_ptr(&yogabook_pm_ops),
},
};
static int __init yogabook_module_init(void)
{
int r;
r = wmi_driver_register(&yogabook_wmi_driver);
if (r)
return r;
r = platform_driver_register(&yogabook_pdev_driver);
if (r)
wmi_driver_unregister(&yogabook_wmi_driver);
return r;
}
static void __exit yogabook_module_exit(void)
{
platform_driver_unregister(&yogabook_pdev_driver);
wmi_driver_unregister(&yogabook_wmi_driver);
}
module_init(yogabook_module_init);
module_exit(yogabook_module_exit);
MODULE_ALIAS("platform:" YB_PDEV_NAME);
MODULE_AUTHOR("Yauhen Kharuzhy");
MODULE_DESCRIPTION("Lenovo Yoga Book driver");
MODULE_LICENSE("GPL v2");