diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst index d7928b8dfb4b5..f7564b23bb7f0 100644 --- a/Documentation/wmi/devices/lenovo-wmi-other.rst +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst @@ -31,13 +31,32 @@ under the following path: /sys/class/firmware-attributes/lenovo-wmi-other/attributes// +Additionally, this driver also exports attributes to HWMON. + +LENOVO_CAPABILITY_DATA_00 +------------------------- + +WMI GUID ``362A3AFE-3D96-4665-8530-96DAD5BB300E`` + +The LENOVO_CAPABILITY_DATA_00 interface provides various information that +does not rely on the gamezone thermal mode. + +The following HWMON attributes are implemented: + - fanX_div: internal RPM divisor + - fanX_input: current RPM + - fanX_target: target RPM (tunable, 0=auto) + +Due to the internal RPM divisor, the current/target RPMs are rounded down to +its nearest multiple. The divisor itself is not necessary to be a power of two. + LENOVO_CAPABILITY_DATA_01 ------------------------- WMI GUID ``7A8F5407-CB67-4D6E-B547-39B3BE018154`` -The LENOVO_CAPABILITY_DATA_01 interface provides information on various -power limits of integrated CPU and GPU components. +The LENOVO_CAPABILITY_DATA_01 interface provides various information that +relies on the gamezone thermal mode, including power limits of integrated +CPU and GPU components. Each attribute has the following properties: - current_value @@ -48,11 +67,43 @@ Each attribute has the following properties: - scalar_increment - type -The following attributes are implemented: +The following firmware-attributes are implemented: + - cpu_oc_stat: CPU Overlocking Status + - cpu_temp: CPU Thermal Load Limit + - dgpu_boost_clk: Dedicated GPU Boost Clock + - dgpu_enable: Dedicated GPU Enabled Status + - gpu_didvid: GPU Device Identifier and Vendor Identifier + - gpu_mode: GPU Mode by Power Limit + - gpu_nv_ac_offset: Nvidia GPU AC Total Processing Power Baseline Offset + - gpu_nv_bpl: Nvidia GPU Base Power Limit + - gpu_nv_cpu_boost: Nvidia GPU to CPU Dynamic Boost Limit + - gpu_nv_ctgp: Nvidia GPU Configurable Total Graphics Power + - gpu_nv_ppab: Nvidia GPU Power Performance Aware Boost Limit + - gpu_oc_stat: GPU Overclocking Status + - gpu_temp: GPU Thermal Load Limit + - ppt_cpu_cl: CPU Cross Loading Power Limit + - ppt_pl1_apu_spl: Platform Profile Tracking APU Sustained Power Limit - ppt_pl1_spl: Platform Profile Tracking Sustained Power Limit + - ppt_pl1_spl_cl: Platform Profile Tracking Cross Loading Sustained Power Limit + - ppt_pl1_tau: Exceed Duration for Platform Profile Tracking Sustained Power Limit - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking + - ppt_pl2_sppt_cl: Platform Profile Tracking Cross Loading Slow Package Tracking - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking + - ppt_pl3_fppt_cl: Platform Profile Tracking Cross Loading Fast Package Power Tracking + - ppt_pl4_ipl: Platform Profile Trakcing Instantaneous Power Limit + - ppt_pl4_ipl_cl: Platform Profile Tracking Cross Loading Instantaneous Power Limit +LENOVO_FAN_TEST_DATA +------------------------- + +WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21`` + +The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of +cooling fans. + +The following HWMON attributes are implemented: + - fanX_max: maximum RPM + - fanX_min: minimum RPM WMI interface description ========================= @@ -106,3 +157,13 @@ data using the `bmfdec `_ utility: [WmiDataId(3), read, Description("Data Size.")] uint32 DataSize; [WmiDataId(4), read, Description("Default Value"), WmiSizeIs("DataSize")] uint8 DefaultValue[]; }; + + [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), Description("Definition of Fan Test Data"), guid("{B642801B-3D21-45DE-90AE-6E86F164FB21}")] + class LENOVO_FAN_TEST_DATA { + [key, read] string InstanceName; + [read] boolean Active; + [WmiDataId(1), read, Description("Mode.")] uint32 NumOfFans; + [WmiDataId(2), read, Description("Fan ID."), WmiSizeIs("NumOfFans")] uint32 FanId[]; + [WmiDataId(3), read, Description("Maximum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMaxSpeed[]; + [WmiDataId(4), read, Description("Minumum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMinSpeed[]; + }; diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index d22b774e0236f..f885127b007f1 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -233,7 +233,7 @@ config YT2_1380 To compile this driver as a module, choose M here: the module will be called lenovo-yogabook. -config LENOVO_WMI_DATA01 +config LENOVO_WMI_CAPDATA tristate depends on ACPI_WMI @@ -263,8 +263,9 @@ config LENOVO_WMI_GAMEZONE config LENOVO_WMI_TUNING tristate "Lenovo Other Mode WMI Driver" depends on ACPI_WMI + select HWMON select FW_ATTR_CLASS - select LENOVO_WMI_DATA01 + select LENOVO_WMI_CAPDATA select LENOVO_WMI_EVENTS select LENOVO_WMI_HELPERS help diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile index 7b2128e3a2142..91a9370f11b3a 100644 --- a/drivers/platform/x86/lenovo/Makefile +++ b/drivers/platform/x86/lenovo/Makefile @@ -12,7 +12,7 @@ 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_DATA01) += wmi-capdata01.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 diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c new file mode 100644 index 0000000000000..ee1fb02d8e31e --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-capdata.c @@ -0,0 +1,829 @@ +// 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 + * - Initial implementation (formerly named lenovo-wmi-capdata01) + * + * Copyright (C) 2025 Rong Zhang + * - Unified implementation + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wmi-capdata.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_CLASS "ac_adapter" +#define ACPI_AC_NOTIFY_STATUS 0x80 + +#define LWMI_FEATURE_ID_FAN_TEST 0x05 + +#define LWMI_ATTR_ID_FAN_TEST \ + (FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) | \ + FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_TEST)) + +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; + + /* + * 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; + +/** + * 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"); + +/** + * 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 { + 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); + + 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_DEVICE_TABLE(wmi, lwmi_cd_id_table); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_AUTHOR("Rong Zhang "); +MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h new file mode 100644 index 0000000000000..00471551e7d60 --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Copyright (C) 2025 Derek J. Clark */ + +#ifndef _LENOVO_WMI_CAPDATA_H_ +#define _LENOVO_WMI_CAPDATA_H_ + +#include +#include + +#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) + +#define LWMI_ATTR_ID(dev, feat, mode, type) \ + (FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, dev) | \ + FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, feat) | \ + FIELD_PREP(LWMI_ATTR_MODE_ID_MASK, mode) | \ + FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, type)) + +enum lwmi_device_id { + LWMI_DEVICE_ID_CPU = 0x01, + LWMI_DEVICE_ID_GPU = 0x02, + LWMI_DEVICE_ID_PSU = 0x03, + LWMI_DEVICE_ID_FAN = 0x04, +}; + +struct component_match; +struct device; +struct cd_list; + +struct capdata00 { + u32 id; + u32 supported; + u32 default_value; +}; + +struct capdata01 { + u32 id; + u32 supported; + u32 default_value; + 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; +}; + +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_ */ diff --git a/drivers/platform/x86/lenovo/wmi-capdata01.c b/drivers/platform/x86/lenovo/wmi-capdata01.c deleted file mode 100644 index fc7e3454e71dc..0000000000000 --- a/drivers/platform/x86/lenovo/wmi-capdata01.c +++ /dev/null @@ -1,302 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Lenovo Capability Data 01 WMI Data Block driver. - * - * Lenovo Capability Data 01 provides information on tunable attributes used by - * the "Other Mode" WMI interface. The data includes if the attribute is - * supported by the hardware, 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. - * - * Copyright (C) 2025 Derek J. Clark - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "wmi-capdata01.h" - -#define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154" - -#define ACPI_AC_CLASS "ac_adapter" -#define ACPI_AC_NOTIFY_STATUS 0x80 - -struct lwmi_cd01_priv { - struct notifier_block acpi_nb; /* ACPI events */ - struct wmi_device *wdev; - struct cd01_list *list; -}; - -struct cd01_list { - struct mutex list_mutex; /* list R/W mutex */ - u8 count; - struct capdata01 data[]; -}; - -/** - * lwmi_cd01_component_bind() - Bind component to master device. - * @cd01_dev: Pointer to the lenovo-wmi-capdata01 driver parent device. - * @om_dev: Pointer to the lenovo-wmi-other driver parent device. - * @data: capdata01_list object pointer used to return the capability data. - * - * On lenovo-wmi-other's master bind, provide a pointer to the local capdata01 - * list. This is used to call lwmi_cd01_get_data to look up attribute data - * from the lenovo-wmi-other driver. - * - * Return: 0 - */ -static int lwmi_cd01_component_bind(struct device *cd01_dev, - struct device *om_dev, void *data) -{ - struct lwmi_cd01_priv *priv = dev_get_drvdata(cd01_dev); - struct cd01_list **cd01_list = data; - - *cd01_list = priv->list; - - return 0; -} - -static const struct component_ops lwmi_cd01_component_ops = { - .bind = lwmi_cd01_component_bind, -}; - -/** - * lwmi_cd01_get_data - Get the data of the specified attribute - * @list: The lenovo-wmi-capdata01 pointer to its cd01_list struct. - * @attribute_id: The capdata attribute ID to be found. - * @output: Pointer to a capdata01 struct to return the data. - * - * Retrieves the capability data 01 struct pointer for the given - * attribute for its specified thermal mode. - * - * Return: 0 on success, or -EINVAL. - */ -int lwmi_cd01_get_data(struct cd01_list *list, u32 attribute_id, struct capdata01 *output) -{ - u8 idx; - - guard(mutex)(&list->list_mutex); - for (idx = 0; idx < list->count; idx++) { - if (list->data[idx].id != attribute_id) - continue; - memcpy(output, &list->data[idx], sizeof(list->data[idx])); - return 0; - } - - return -EINVAL; -} -EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD01"); - -/** - * lwmi_cd01_cache() - Cache all WMI data block information - * @priv: lenovo-wmi-capdata01 driver data. - * - * Loop through each WMI data block and cache the data. - * - * Return: 0 on success, or an error. - */ -static int lwmi_cd01_cache(struct lwmi_cd01_priv *priv) -{ - int idx; - - guard(mutex)(&priv->list->list_mutex); - for (idx = 0; idx < priv->list->count; idx++) { - 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 < sizeof(priv->list->data[idx])) - continue; - - memcpy(&priv->list->data[idx], ret_obj->buffer.pointer, - ret_obj->buffer.length); - } - - return 0; -} - -/** - * lwmi_cd01_alloc() - Allocate a cd01_list struct in drvdata - * @priv: lenovo-wmi-capdata01 driver data. - * - * Allocate a cd01_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_cd01_alloc(struct lwmi_cd01_priv *priv) -{ - struct cd01_list *list; - size_t list_size; - int count, ret; - - count = wmidev_instance_count(priv->wdev); - list_size = struct_size(list, data, count); - - list = devm_kzalloc(&priv->wdev->dev, list_size, GFP_KERNEL); - if (!list) - return -ENOMEM; - - ret = devm_mutex_init(&priv->wdev->dev, &list->list_mutex); - if (ret) - return ret; - - list->count = count; - priv->list = list; - - return 0; -} - -/** - * lwmi_cd01_setup() - Cache all WMI data block information - * @priv: lenovo-wmi-capdata01 driver data. - * - * Allocate a cd01_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_cd01_setup(struct lwmi_cd01_priv *priv) -{ - int ret; - - ret = lwmi_cd01_alloc(priv); - if (ret) - return ret; - - return lwmi_cd01_cache(priv); -} - -/** - * lwmi_cd01_notifier_call() - Call method for lenovo-wmi-capdata01 driver 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_cd01_priv *priv; - int ret; - - if (strcmp(event->device_class, ACPI_AC_CLASS) != 0) - return NOTIFY_DONE; - - priv = container_of(nb, struct lwmi_cd01_priv, acpi_nb); - - switch (event->type) { - case ACPI_AC_NOTIFY_STATUS: - ret = lwmi_cd01_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_cd01_probe(struct wmi_device *wdev, const void *context) - -{ - struct lwmi_cd01_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); - - ret = lwmi_cd01_setup(priv); - if (ret) - return ret; - - priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call; - - ret = register_acpi_notifier(&priv->acpi_nb); - if (ret) - return ret; - - ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister, &priv->acpi_nb); - if (ret) - return ret; - - return component_add(&wdev->dev, &lwmi_cd01_component_ops); -} - -static void lwmi_cd01_remove(struct wmi_device *wdev) -{ - component_del(&wdev->dev, &lwmi_cd01_component_ops); -} - -static const struct wmi_device_id lwmi_cd01_id_table[] = { - { LENOVO_CAPABILITY_DATA_01_GUID, NULL }, - {} -}; - -static struct wmi_driver lwmi_cd01_driver = { - .driver = { - .name = "lenovo_wmi_cd01", - .probe_type = PROBE_PREFER_ASYNCHRONOUS, - }, - .id_table = lwmi_cd01_id_table, - .probe = lwmi_cd01_probe, - .remove = lwmi_cd01_remove, - .no_singleton = true, -}; - -/** - * lwmi_cd01_match() - Match rule for the master driver. - * @dev: Pointer to the capability data 01 parent device. - * @data: Unused void pointer for passing match criteria. - * - * Return: int. - */ -int lwmi_cd01_match(struct device *dev, void *data) -{ - return dev->driver == &lwmi_cd01_driver.driver; -} -EXPORT_SYMBOL_NS_GPL(lwmi_cd01_match, "LENOVO_WMI_CD01"); - -module_wmi_driver(lwmi_cd01_driver); - -MODULE_DEVICE_TABLE(wmi, lwmi_cd01_id_table); -MODULE_AUTHOR("Derek J. Clark "); -MODULE_DESCRIPTION("Lenovo Capability Data 01 WMI Driver"); -MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-capdata01.h b/drivers/platform/x86/lenovo/wmi-capdata01.h deleted file mode 100644 index bd06c5751f68b..0000000000000 --- a/drivers/platform/x86/lenovo/wmi-capdata01.h +++ /dev/null @@ -1,25 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -/* Copyright (C) 2025 Derek J. Clark */ - -#ifndef _LENOVO_WMI_CAPDATA01_H_ -#define _LENOVO_WMI_CAPDATA01_H_ - -#include - -struct device; -struct cd01_list; - -struct capdata01 { - u32 id; - u32 supported; - u32 default_value; - u32 step; - u32 min_value; - u32 max_value; -}; - -int lwmi_cd01_get_data(struct cd01_list *list, u32 attribute_id, struct capdata01 *output); -int lwmi_cd01_match(struct device *dev, void *data); - -#endif /* !_LENOVO_WMI_CAPDATA01_H_ */ diff --git a/drivers/platform/x86/lenovo/wmi-gamezone.h b/drivers/platform/x86/lenovo/wmi-gamezone.h index 6b163a5eeb959..ddb919cf6c36d 100644 --- a/drivers/platform/x86/lenovo/wmi-gamezone.h +++ b/drivers/platform/x86/lenovo/wmi-gamezone.h @@ -10,6 +10,7 @@ enum gamezone_events_type { }; 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, diff --git a/drivers/platform/x86/lenovo/wmi-helpers.c b/drivers/platform/x86/lenovo/wmi-helpers.c index f6fef6296251e..7379defac5002 100644 --- a/drivers/platform/x86/lenovo/wmi-helpers.c +++ b/drivers/platform/x86/lenovo/wmi-helpers.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include "wmi-helpers.h" @@ -59,10 +60,24 @@ int lwmi_dev_evaluate_int(struct wmi_device *wdev, u8 instance, u32 method_id, if (!ret_obj) return -ENODATA; - if (ret_obj->type != ACPI_TYPE_INTEGER) - return -ENXIO; + 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 = (u32)ret_obj->integer.value; + *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; diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c index 2a960b278f117..dc128fa9f99cd 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -14,9 +14,19 @@ * These attributes typically don't fit anywhere else in the sysfs and are set * in Windows using one of Lenovo's multiple user applications. * + * Additionally, this driver also exports tunable fan speed RPM to HWMON. + * Min/max RPM are also provided for reference. + * * Copyright (C) 2025 Derek J. Clark + * - fw_attributes + * - binding to Capability Data 01 + * + * Copyright (C) 2025 Rong Zhang + * - HWMON + * - binding to Capability Data 00 and Fan */ +#include #include #include #include @@ -25,16 +35,19 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include -#include "wmi-capdata01.h" +#include "wmi-capdata.h" #include "wmi-events.h" #include "wmi-gamezone.h" #include "wmi-helpers.h" @@ -43,23 +56,64 @@ #define LENOVO_OTHER_MODE_GUID "DC2A8805-3A8C-41BA-A6F7-092E0089CD3B" -#define LWMI_DEVICE_ID_CPU 0x01 +enum lwmi_feature_id_cpu { + LWMI_FEATURE_ID_CPU_SPPT = 0x01, + LWMI_FEATURE_ID_CPU_SPL = 0x02, + LWMI_FEATURE_ID_CPU_FPPT = 0x03, + LWMI_FEATURE_ID_CPU_TEMP = 0x04, + LWMI_FEATURE_ID_CPU_APU = 0x05, + LWMI_FEATURE_ID_CPU_CL = 0x06, + LWMI_FEATURE_ID_CPU_TAU = 0x07, + LWMI_FEATURE_ID_CPU_OC = 0x08, + LWMI_FEATURE_ID_CPU_IPL = 0x09, +}; + +enum lwmi_feature_id_gpu { + LWMI_FEATURE_ID_GPU_NV_PPAB = 0x01, + LWMI_FEATURE_ID_GPU_NV_CTGP = 0x02, + LWMI_FEATURE_ID_GPU_TEMP = 0x03, + LWMI_FEATURE_ID_GPU_AC_OFFSET = 0x04, + LWMI_FEATURE_ID_GPU_OC = 0x05, + LWMI_FEATURE_ID_DGPU_BOOST_CLK = 0x06, + LWMI_FEATURE_ID_DGPU_EN = 0x07, + LWMI_FEATURE_ID_GPU_MODE = 0x08, + LWMI_FEATURE_ID_DGPU_DIDVID = 0x09, + LWMI_FEATURE_ID_GPU_NV_BPL = 0x0a, + LWMI_FEATURE_ID_GPU_NV_CPU_BOOST = 0x0b, +}; -#define LWMI_FEATURE_ID_CPU_SPPT 0x01 -#define LWMI_FEATURE_ID_CPU_SPL 0x02 -#define LWMI_FEATURE_ID_CPU_FPPT 0x03 +enum lwmi_feature_id_psu { + LWMI_FEATURE_ID_PSU_INSTANT_MODE = 0x01, + LWMI_FEATURE_ID_PSU_CHARGE_MODE = 0x02, +}; + +#define LWMI_FEATURE_ID_FAN_RPM 0x03 #define LWMI_TYPE_ID_NONE 0x00 +#define LWMI_TYPE_ID_CROSSLOAD 0x01 +#define LWMI_TYPE_ID_PSU_AC 0x01 +#define LWMI_TYPE_ID_PSU_PD 0x02 #define LWMI_FEATURE_VALUE_GET 17 #define LWMI_FEATURE_VALUE_SET 18 -#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) +#define LWMI_FAN_ID_BASE 1 +#define LWMI_FAN_NR 4 +#define LWMI_FAN_ID(x) ((x) + LWMI_FAN_ID_BASE) +#define LWMI_FAN_DIV 100 + +#define LWMI_CHARGE_MODE_ENABLED 0x00 +#define LWMI_CHARGE_MODE_DISABLED 0x01 + +#define LWMI_ATTR_ID_FAN_RPM(x) \ + LWMI_ATTR_ID(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_RPM, \ + LWMI_GZ_THERMAL_MODE_NONE, LWMI_FAN_ID(x)) -#define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other" +#define LWMI_ATTR_ID_PSU(feat, type) \ + LWMI_ATTR_ID(LWMI_DEVICE_ID_PSU, feat, \ + LWMI_GZ_THERMAL_MODE_NONE, type) + +#define LWMI_OM_NAME "lenovo-wmi-other" static BLOCKING_NOTIFIER_HEAD(om_chain_head); static DEFINE_IDA(lwmi_om_ida); @@ -72,22 +126,678 @@ enum attribute_property { SUPPORTED, }; +struct lwmi_fan_info { + u32 supported; + u32 last_target; + long min_rpm; + long max_rpm; +}; + struct lwmi_om_priv { struct component_master_ops *ops; - struct cd01_list *cd01_list; /* only valid after capdata01 bind */ + + /* only valid after capdata bind */ + struct cd_list *cd00_list; + struct cd_list *cd01_list; + + struct device *hwmon_dev; struct device *fw_attr_dev; struct kset *fw_attr_kset; struct notifier_block nb; struct wmi_device *wdev; int ida_id; + + struct lwmi_fan_info fan_info[LWMI_FAN_NR]; + + struct { + bool capdata00_collected : 1; + bool capdata_fan_collected : 1; + } fan_flags; + + struct acpi_battery_hook battery_hook; +}; + +/* + * Visibility of fan channels: + * + * +-------------------+---------+------------------+-----------------------+------------+ + * | | default | +expose_all_fans | +relax_fan_constraint | +both | + * +-------------------+---------+------------------+-----------------------+------------+ + * | canonical | RW | RW | RW+relaxed | RW+relaxed | + * +-------------------+---------+------------------+-----------------------+------------+ + * | -capdata_fan[idx] | N | RO | N | RW+relaxed | + * +-------------------+---------+------------------+-----------------------+------------+ + * + * Note: + * 1. LWMI_ATTR_ID_FAN_RPM[idx].supported is always checked before exposing a channel. + * 2. -capdata_fan implies -capdata_fan[idx]. + */ +static bool expose_all_fans; +module_param(expose_all_fans, bool, 0444); +MODULE_PARM_DESC(expose_all_fans, + "This option skips some capability checks and solely relies on per-channel ones " + "to expose fan attributes. Use with caution."); + +static bool relax_fan_constraint; +module_param(relax_fan_constraint, bool, 0444); +MODULE_PARM_DESC(relax_fan_constraint, + "Do not enforce fan RPM constraint (div/min/max) " + "and enables fan tuning when such data is missing. " + "Enabling this may results in HWMON attributes being out-of-sync, " + "and setting a too low RPM stops the fan. Use with caution."); + +/* ======== HWMON (component: lenovo-wmi-capdata 00 & fan) ======== */ + +/** + * lwmi_om_fan_get_set() - Get or set fan RPM value of specified fan + * @priv: Driver private data structure + * @channel: Fan channel index (0-based) + * @val: Pointer to value (input for set, output for get) + * @set: True to set value, false to get value + * + * Communicates with WMI interface to either retrieve current fan RPM + * or set target fan RPM. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_om_fan_get_set(struct lwmi_om_priv *priv, int channel, u32 *val, bool set) +{ + struct wmi_method_args_32 args; + u32 method_id, retval; + int err; + + method_id = set ? LWMI_FEATURE_VALUE_SET : LWMI_FEATURE_VALUE_GET; + args.arg0 = LWMI_ATTR_ID_FAN_RPM(channel); + args.arg1 = set ? *val : 0; + + err = lwmi_dev_evaluate_int(priv->wdev, 0x0, method_id, + (unsigned char *)&args, sizeof(args), &retval); + if (err) + return err; + + if (!set) { + *val = retval; + return 0; + } + + /* + * It seems that 0 means "no error" and 1 means "done". Apparently + * different firmware teams have different thoughts on indicating + * success, so we accepts both. + */ + return (retval == 0 || retval == 1) ? 0 : -EIO; +} + +/** + * lwmi_om_hwmon_is_visible() - Determine visibility of HWMON attributes + * @drvdata: Driver private data + * @type: Sensor type + * @attr: Attribute identifier + * @channel: Channel index + * + * Determines whether an HWMON attribute should be visible in sysfs + * based on hardware capabilities and current configuration. + * + * Return: permission mode, or 0 if invisible. + */ +static umode_t lwmi_om_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + struct lwmi_om_priv *priv = (struct lwmi_om_priv *)drvdata; + bool visible = false; + + if (type == hwmon_fan) { + if (!(priv->fan_info[channel].supported & LWMI_SUPP_VALID)) + return 0; + + switch (attr) { + case hwmon_fan_target: + if (!(priv->fan_info[channel].supported & LWMI_SUPP_SET)) + return 0; + + if (relax_fan_constraint || + (priv->fan_info[channel].min_rpm >= 0 && + priv->fan_info[channel].max_rpm >= 0)) + return 0644; + + /* + * Reaching here implies expose_all_fans is set. + * See lwmi_om_hwmon_add(). + */ + dev_warn_once(&priv->wdev->dev, + "fan tuning disabled due to missing RPM constraint\n"); + return 0; + case hwmon_fan_div: + case hwmon_fan_input: + visible = priv->fan_info[channel].supported & LWMI_SUPP_GET; + break; + case hwmon_fan_min: + visible = priv->fan_info[channel].min_rpm >= 0; + break; + case hwmon_fan_max: + visible = priv->fan_info[channel].max_rpm >= 0; + break; + } + } + + return visible ? 0444 : 0; +} + +/** + * lwmi_om_hwmon_read() - Read HWMON sensor data + * @dev: Device pointer + * @type: Sensor type + * @attr: Attribute identifier + * @channel: Channel index + * @val: Pointer to store value + * + * Reads current sensor values from hardware through WMI interface. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_om_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + u32 retval = 0; + int err; + + if (type == hwmon_fan) { + switch (attr) { + /* + * The EC has an internal RPM divisor (i.e., the raw register value is + * RPM / fanY_div). For fanY_input, the WMI moethod reads the register + * value and returns raw * fanY_div. For fanY_target, the WMI method + * divides the written value by fanY_div before writing it to the EC. + * + * As a result, reading fanY_input always returns a multiple of fanY_div, + * while writing to fanY_target loses the remainder. + */ + case hwmon_fan_div: + *val = LWMI_FAN_DIV; + return 0; + case hwmon_fan_input: + err = lwmi_om_fan_get_set(priv, channel, &retval, false); + if (err) + return err; + + *val = retval; + return 0; + case hwmon_fan_target: + *val = priv->fan_info[channel].last_target; + return 0; + case hwmon_fan_min: + *val = priv->fan_info[channel].min_rpm; + return 0; + case hwmon_fan_max: + *val = priv->fan_info[channel].max_rpm; + return 0; + } + } + + return -EOPNOTSUPP; +} + +/** + * lwmi_om_hwmon_write() - Write HWMON sensor data + * @dev: Device pointer + * @type: Sensor type + * @attr: Attribute identifier + * @channel: Channel index + * @val: Value to write + * + * Writes configuration values to hardware through WMI interface. + * + * Return: 0 on success, or an error code. + */ +static int lwmi_om_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + u32 raw, min_rpm, max_rpm; + int err; + + if (type == hwmon_fan) { + switch (attr) { + case hwmon_fan_target: + if (relax_fan_constraint) { + min_rpm = 1; + max_rpm = U16_MAX; + } else { + min_rpm = priv->fan_info[channel].min_rpm; + max_rpm = priv->fan_info[channel].max_rpm; + } + + /* 0 means "auto". */ + if (val != 0 && (val < min_rpm || val > max_rpm)) + return -EINVAL; + + /* + * The effective fanY_target is always a multiple of fanY_div + * due to the EC's internal RPM divisor (see lwmi_om_hwmon_read). + * + * Round down the written value to the nearest multiple of fanY_div + * to prevent mismatch between the effective value and last_target. + * + * For relax_fan_constraint, skip this conversion as setting a + * sub-fanY_div value is necessary to completely stop the fan on + * some devices. + */ + if (!relax_fan_constraint) + raw = val / LWMI_FAN_DIV * LWMI_FAN_DIV; + + err = lwmi_om_fan_get_set(priv, channel, &raw, true); + if (err) + return err; + + priv->fan_info[channel].last_target = raw; + return 0; + } + } + + return -EOPNOTSUPP; +} + +static const struct hwmon_channel_info * const lwmi_om_hwmon_info[] = { + /* Must match LWMI_FAN_NR. */ + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX, + HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | + HWMON_F_MIN | HWMON_F_MAX), + NULL }; +static const struct hwmon_ops lwmi_om_hwmon_ops = { + .is_visible = lwmi_om_hwmon_is_visible, + .read = lwmi_om_hwmon_read, + .write = lwmi_om_hwmon_write, +}; + +static const struct hwmon_chip_info lwmi_om_hwmon_chip_info = { + .ops = &lwmi_om_hwmon_ops, + .info = lwmi_om_hwmon_info, +}; + +/** + * lwmi_om_hwmon_add() - Register HWMON device if all info is collected + * @priv: Driver private data + */ +static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) +{ + int i, valid; + + if (WARN_ON(priv->hwmon_dev)) + return; + + if (!priv->fan_flags.capdata00_collected || !priv->fan_flags.capdata_fan_collected) { + dev_dbg(&priv->wdev->dev, "HWMON registration pending (00: %d, fan: %d)\n", + priv->fan_flags.capdata00_collected, + priv->fan_flags.capdata_fan_collected); + return; + } + + if (expose_all_fans) + dev_warn(&priv->wdev->dev, "all fans exposed. Use with caution\n"); + + if (relax_fan_constraint) + dev_warn(&priv->wdev->dev, "fan RPM constraint relaxed. Use with caution\n"); + + valid = 0; + for (i = 0; i < LWMI_FAN_NR; i++) { + if (!(priv->fan_info[i].supported & LWMI_SUPP_VALID)) + continue; + + valid++; + + if (!expose_all_fans && + (priv->fan_info[i].min_rpm < 0 || priv->fan_info[i].max_rpm < 0)) { + dev_dbg(&priv->wdev->dev, "missing RPM constraint for fan%d, hiding\n", + LWMI_FAN_ID(i)); + priv->fan_info[i].supported = 0; + valid--; + } + } + + if (valid == 0) { + dev_warn(&priv->wdev->dev, + "fan reporting/tuning is unsupported on this device\n"); + return; + } + + priv->hwmon_dev = hwmon_device_register_with_info(&priv->wdev->dev, + LWMI_OM_NAME, priv, + &lwmi_om_hwmon_chip_info, + NULL); + if (IS_ERR(priv->hwmon_dev)) { + dev_warn(&priv->wdev->dev, "failed to register HWMON device: %ld\n", + PTR_ERR(priv->hwmon_dev)); + priv->hwmon_dev = NULL; + return; + } + + dev_dbg(&priv->wdev->dev, "registered HWMON device\n"); +} + +/** + * lwmi_om_hwmon_remove() - Unregister HWMON device + * @priv: Driver private data + * + * Unregisters the HWMON device if applicable. + */ +static void lwmi_om_hwmon_remove(struct lwmi_om_priv *priv) +{ + if (!priv->hwmon_dev) + return; + + hwmon_device_unregister(priv->hwmon_dev); + priv->hwmon_dev = NULL; +} + +/** + * lwmi_om_fan_info_init() - Initialzie fan info + * @priv: Driver private data + * + * lwmi_om_fan_info_collect_cd00() and lwmi_om_fan_info_collect_cd_fan() may be + * called in an arbitrary order. Hence, initializion must be done before. + */ +static void lwmi_om_fan_info_init(struct lwmi_om_priv *priv) +{ + int i; + + for (i = 0; i < LWMI_FAN_NR; i++) { + priv->fan_info[i] = (struct lwmi_fan_info) { + .supported = 0, + /* + * Assume 0 on probe as the EC resets all fans to auto mode on (re)boot. + * + * Note that S0ix (s2idle) preserves the RPM target, so we don't need + * suspend/resume callbacks. This behavior has not been tested on S3- + * capable devices, but I doubt if such devices even have this interface. + */ + .last_target = 0, + .min_rpm = -ENODATA, + .max_rpm = -ENODATA, + }; + } + + priv->fan_flags.capdata00_collected = false; + priv->fan_flags.capdata_fan_collected = false; +} + +/** + * lwmi_om_fan_info_collect_cd00() - Collect fan info from capdata 00 + * @priv: Driver private data + */ +static void lwmi_om_fan_info_collect_cd00(struct lwmi_om_priv *priv) +{ + struct capdata00 capdata00; + int i, err; + + dev_dbg(&priv->wdev->dev, "Collecting fan info from capdata00\n"); + + for (i = 0; i < LWMI_FAN_NR; i++) { + err = lwmi_cd00_get_data(priv->cd00_list, LWMI_ATTR_ID_FAN_RPM(i), &capdata00); + priv->fan_info[i].supported = err ? 0 : capdata00.supported; + } + + priv->fan_flags.capdata00_collected = true; + lwmi_om_hwmon_add(priv); +} + +/** + * lwmi_om_fan_info_collect_cd_fan() - Collect fan info from capdata fan + * @dev: Pointer to the lenovo-wmi-other device + * @cd_fan_list: Pointer to the capdata fan list + */ +static void lwmi_om_fan_info_collect_cd_fan(struct device *dev, struct cd_list *cd_fan_list) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(dev); + struct capdata_fan capdata_fan; + int i, err; + + dev_dbg(dev, "Collecting fan info from capdata_fan\n"); + + if (!cd_fan_list) + goto out; + + for (i = 0; i < LWMI_FAN_NR; i++) { + err = lwmi_cd_fan_get_data(cd_fan_list, LWMI_FAN_ID(i), &capdata_fan); + if (err) + continue; + + priv->fan_info[i].min_rpm = capdata_fan.min_rpm; + priv->fan_info[i].max_rpm = capdata_fan.max_rpm; + } + +out: + priv->fan_flags.capdata_fan_collected = true; + lwmi_om_hwmon_add(priv); +} + +/* ======== Power Supply Extension (component: lenovo-wmi-capdata 00) ======== */ + +/** + * lwmi_psy_prop_is_writeable() - Get a power_supply_ext property + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer the lwmi_om_priv drvdata + * @prop: The property to read + * @val: The value to return + * + * Writes the given value to the power_supply_ext property + * + * Return: 0 on success, or an error + */ +static int lwmi_psy_ext_get_prop(struct power_supply *ps, + const struct power_supply_ext *ext, + void *data, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct lwmi_om_priv *priv = data; + struct wmi_method_args_32 args; + u32 retval; + int ret; + + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_INSTANT_MODE, LWMI_TYPE_ID_PSU_AC); + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, + (unsigned char *)&args, sizeof(args), + &retval); + if (ret) + return ret; + + dev_dbg(&priv->wdev->dev, "Got return value %x for property %x\n", retval, prop); + + if (retval == LWMI_CHARGE_MODE_DISABLED) + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + else + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + + return 0; +} + +/** + * lwmi_psy_prop_is_writeable() - Set a power_supply_ext property + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer the lwmi_om_priv drvdata + * @prop: The property to write + * @val: The value to write + * + * Writes the given value to the power_supply_ext property + * + * Return: 0 on success, or an error + */ +static int lwmi_psy_ext_set_prop(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop, + const union power_supply_propval *val) +{ + struct lwmi_om_priv *priv = ext_data; + struct wmi_method_args_32 args; + + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_INSTANT_MODE, LWMI_TYPE_ID_PSU_AC); + if (val->intval == POWER_SUPPLY_CHARGE_TYPE_LONGLIFE) + args.arg1 = LWMI_CHARGE_MODE_DISABLED; + else + args.arg1 = LWMI_CHARGE_MODE_ENABLED; + + dev_dbg(&priv->wdev->dev, "Attempting to set %#08x for property %x to %x\n", + args.arg0, prop, args.arg1); + + return lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET, + (unsigned char *)&args, sizeof(args), NULL); +} + +/** + * lwmi_psy_prop_is_writeable() - Determine if the property is supported + * @ps: The battery that was extended + * @ext: The extension + * @ext_data: Pointer the lwmi_om_priv drvdata + * @prop: The property to check + * + * Checks capdata 00 to determine if the property is supported. + * + * Return: Support level, or false + */ +static int lwmi_psy_prop_is_writeable(struct power_supply *ps, + const struct power_supply_ext *ext, + void *ext_data, + enum power_supply_property prop) +{ + struct lwmi_om_priv *priv = ext_data; + struct capdata00 capdata; + u32 attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_INSTANT_MODE, LWMI_TYPE_ID_PSU_AC); + int ret; + + ret = lwmi_cd00_get_data(priv->cd00_list, attribute_id, &capdata); + if (ret) + return false; + + dev_dbg(&priv->wdev->dev, "Battery charge mode (%#08x) support level: %x\n", + attribute_id, capdata.supported); + + return capdata.supported; +} + +static const enum power_supply_property lwmi_psy_ext_props[] = { + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +static const struct power_supply_ext lwmi_psy_ext = { + .name = LWMI_OM_NAME, + .properties = lwmi_psy_ext_props, + .num_properties = ARRAY_SIZE(lwmi_psy_ext_props), + .charge_types = (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)), + .get_property = lwmi_psy_ext_get_prop, + .set_property = lwmi_psy_ext_set_prop, + .property_is_writeable = lwmi_psy_prop_is_writeable, +}; + +/** + * lwmi_add_battery() - Connect the power_supply_ext + * @battery: The battery to extend + * @hook: The driver hook used to extend the battery + * + * Return: 0 on success, or an error. + */ +static int lwmi_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + struct lwmi_om_priv *priv = container_of(hook, struct lwmi_om_priv, battery_hook); + + return power_supply_register_extension(battery, &lwmi_psy_ext, &priv->wdev->dev, priv); +} + +/** + * lwmi_remove_battery() - Disconnect the power_supply_ext + * @battery: The battery that was extended + * @hook: The driver hook used to extend the battery + * + * Return: 0 on success, or an error. + */ +static int lwmi_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &lwmi_psy_ext); + return 0; +} + +/** + * lwmi_acpi_match() - Attempts to return the ideapad acpi handle + * @acpi_handle: The ACPI handle that manages battery charging + * @lvl: Unused + * @context: Void pointer to the acpi_handle object to return + * @retval: Unused + * + * Checks if the ideapad_laptop driver is going to manage charge_type first, + * thenm if not, hooks the battery to our WMI methods. + * + * Return: AE_CTRL_TERMINATE if found, AE_OK if not found. + */ +static acpi_status lwmi_acpi_match(acpi_handle handle, u32 lvl, + void *context, void **retval) +{ + if (!handle) + return AE_OK; + + acpi_handle *ahand = context; + *ahand = handle; + + return AE_CTRL_TERMINATE; +} + +/** + * lwmi_om_ps_ext_init() - Hooks power supply extension to device battery + * @priv: Driver private data + * + * Checks if the ideapad_laptop driver is going to manage charge_type first, + * thenm if not, hooks the battery to our WMI methods. + */ +static void lwmi_om_ps_ext_init(struct lwmi_om_priv *priv) +{ + static const char * const ideapad_hid = "VPC2004"; + acpi_handle handle = NULL; + int ret; + + /* Deconflict ideapad_laptop driver */ + ret = acpi_get_devices(ideapad_hid, lwmi_acpi_match, &handle, NULL); + if (ret) + return; + + if (!handle) + return; + + if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) { + dev_dbg(&priv->wdev->dev, "ideapad_laptop driver manages battery for device.\n"); + return; + } + + /* Add battery hooks */ + priv->battery_hook.add_battery = lwmi_add_battery, + priv->battery_hook.remove_battery = lwmi_remove_battery, + priv->battery_hook.name = "Lenovo WMI Other Battery Extension", + + ret = devm_battery_hook_register(&priv->wdev->dev, &priv->battery_hook); + if (ret) + dev_err(&priv->wdev->dev, "Error during battery hook: %i\n", ret); +} + +/* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */ + struct tunable_attr_01 { - struct capdata01 *capdata; struct device *dev; - u32 feature_id; - u32 device_id; - u32 type_id; + u8 feature_id; + u8 device_id; + u8 type_id; + u8 cd_mode_id; /* mode arg for searching capdata */ + u8 cv_mode_id; /* mode arg for set/get current_value */ }; static struct tunable_attr_01 ppt_pl1_spl = { @@ -96,18 +806,144 @@ static struct tunable_attr_01 ppt_pl1_spl = { .type_id = LWMI_TYPE_ID_NONE, }; +static struct tunable_attr_01 ppt_pl1_spl_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPL, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + static struct tunable_attr_01 ppt_pl2_sppt = { .device_id = LWMI_DEVICE_ID_CPU, .feature_id = LWMI_FEATURE_ID_CPU_SPPT, .type_id = LWMI_TYPE_ID_NONE, }; +static struct tunable_attr_01 ppt_pl2_sppt_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_SPPT, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + static struct tunable_attr_01 ppt_pl3_fppt = { .device_id = LWMI_DEVICE_ID_CPU, .feature_id = LWMI_FEATURE_ID_CPU_FPPT, .type_id = LWMI_TYPE_ID_NONE, }; +static struct tunable_attr_01 ppt_pl3_fppt_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_FPPT, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + +static struct tunable_attr_01 cpu_temp = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_TEMP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl1_apu_spl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_APU, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_cpu_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_CL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl1_tau = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_TAU, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 cpu_oc_stat = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_OC, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl4_ipl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_IPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 ppt_pl4_ipl_cl = { + .device_id = LWMI_DEVICE_ID_CPU, + .feature_id = LWMI_FEATURE_ID_CPU_IPL, + .type_id = LWMI_TYPE_ID_CROSSLOAD, +}; + +static struct tunable_attr_01 gpu_nv_ppab = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_PPAB, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_ctgp = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_CTGP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_temp = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_TEMP, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_ac_offset = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_AC_OFFSET, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_oc_stat = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_OC, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_boost_clk = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_BOOST_CLK, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_enable = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_EN, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_mode = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_MODE, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 dgpu_didvid = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_DGPU_DIDVID, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_bpl = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_BPL, + .type_id = LWMI_TYPE_ID_NONE, +}; + +static struct tunable_attr_01 gpu_nv_cpu_boost = { + .device_id = LWMI_DEVICE_ID_GPU, + .feature_id = LWMI_FEATURE_ID_GPU_NV_CPU_BOOST, + .type_id = LWMI_TYPE_ID_NONE, +}; + struct capdata01_attr_group { const struct attribute_group *attr_group; struct tunable_attr_01 *tunable_attr; @@ -253,12 +1089,8 @@ static ssize_t attr_capdata01_show(struct kobject *kobj, u32 attribute_id; int value, ret; - attribute_id = - FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, tunable_attr->device_id) | - FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, tunable_attr->feature_id) | - FIELD_PREP(LWMI_ATTR_MODE_ID_MASK, - LWMI_GZ_THERMAL_MODE_CUSTOM) | - FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, tunable_attr->type_id); + attribute_id = LWMI_ATTR_ID(tunable_attr->device_id, tunable_attr->feature_id, + tunable_attr->cd_mode_id, tunable_attr->type_id); ret = lwmi_cd01_get_data(priv->cd01_list, attribute_id, &capdata); if (ret) @@ -313,7 +1145,6 @@ static ssize_t attr_current_value_store(struct kobject *kobj, struct wmi_method_args_32 args; struct capdata01 capdata; enum thermal_mode mode; - u32 attribute_id; u32 value; int ret; @@ -324,13 +1155,10 @@ static ssize_t attr_current_value_store(struct kobject *kobj, if (mode != LWMI_GZ_THERMAL_MODE_CUSTOM) return -EBUSY; - attribute_id = - FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, tunable_attr->device_id) | - FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, tunable_attr->feature_id) | - FIELD_PREP(LWMI_ATTR_MODE_ID_MASK, mode) | - FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, tunable_attr->type_id); + args.arg0 = LWMI_ATTR_ID(tunable_attr->device_id, tunable_attr->feature_id, + tunable_attr->cd_mode_id, tunable_attr->type_id); - ret = lwmi_cd01_get_data(priv->cd01_list, attribute_id, &capdata); + ret = lwmi_cd01_get_data(priv->cd01_list, args.arg0, &capdata); if (ret) return ret; @@ -341,7 +1169,8 @@ static ssize_t attr_current_value_store(struct kobject *kobj, if (value < capdata.min_value || value > capdata.max_value) return -EINVAL; - args.arg0 = attribute_id; + args.arg0 = LWMI_ATTR_ID(tunable_attr->device_id, tunable_attr->feature_id, + tunable_attr->cv_mode_id, tunable_attr->type_id); args.arg1 = value; ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET, @@ -375,21 +1204,18 @@ static ssize_t attr_current_value_show(struct kobject *kobj, struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); struct wmi_method_args_32 args; enum thermal_mode mode; - u32 attribute_id; - int retval; - int ret; + int retval, ret; ret = lwmi_om_notifier_call(&mode); if (ret) return ret; - attribute_id = - FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, tunable_attr->device_id) | - FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, tunable_attr->feature_id) | - FIELD_PREP(LWMI_ATTR_MODE_ID_MASK, mode) | - FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, tunable_attr->type_id); + /* If "no-mode" is the supported mode, ensure we never send current mode */ + if (tunable_attr->cv_mode_id == LWMI_GZ_THERMAL_MODE_NONE) + mode = tunable_attr->cv_mode_id; - args.arg0 = attribute_id; + args.arg0 = LWMI_ATTR_ID(tunable_attr->device_id, tunable_attr->feature_id, + mode, tunable_attr->type_id); ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, (unsigned char *)&args, sizeof(args), @@ -400,6 +1226,85 @@ static ssize_t attr_current_value_show(struct kobject *kobj, return sysfs_emit(buf, "%d\n", retval); } +/** + * lwmi_attr_01_is_supported() - Determine if the given attribute is supported. + * @tunable_attr: The attribute to verify. + * + * First check if the attribute has a corresponding capdata01 table in the cd01 + * module under the "custom" mode (0xff). If that is not present then check if + * there is a corresponding "no-mode" (0x00) entry. If either of those passes, + * check capdata->supported for values > 0. If capdata is available, attempt to + * determine the set/get mode for the current value property using a similar + * pattern. If the value returned by either custom or no-mode is 0, or we get + * an error, we assume that mode is not supported. If any of the above checks + * fail then the attribute is not fully supported. + * + * The probed cd_mode_id/cv_mode_id are stored on the tunable_attr for later + * reference. + * + * Return: Support level, or an error code. + */ +static int lwmi_attr_01_is_supported(struct tunable_attr_01 *tunable_attr) +{ + struct lwmi_om_priv *priv = dev_get_drvdata(tunable_attr->dev); + u8 mode = LWMI_GZ_THERMAL_MODE_CUSTOM; + struct wmi_method_args_32 args; + struct capdata01 capdata; + int retval, ret; + + /* Determine tunable_attr->cd_mode_id */ +no_mode_fallback_1: + args.arg0 = LWMI_ATTR_ID(tunable_attr->device_id, tunable_attr->feature_id, + mode, tunable_attr->type_id); + + ret = lwmi_cd01_get_data(priv->cd01_list, args.arg0, &capdata); + if (ret && mode) { + dev_dbg(tunable_attr->dev, "Attribute id %x not supported\n", args.arg0); + mode = LWMI_GZ_THERMAL_MODE_NONE; + goto no_mode_fallback_1; + } + if (ret) + goto not_supported; + if (!capdata.supported) { + ret = -EOPNOTSUPP; + goto not_supported; + } + + tunable_attr->cd_mode_id = mode; + + /* Determine tunable_attr->cv_mode_id */ + mode = LWMI_GZ_THERMAL_MODE_CUSTOM; +no_mode_fallback_2: + args.arg0 = LWMI_ATTR_ID(tunable_attr->device_id, tunable_attr->feature_id, + mode, tunable_attr->type_id); + + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET, + (unsigned char *)&args, sizeof(args), + &retval); + if ((ret && mode) || (!retval && mode)) { + dev_dbg(tunable_attr->dev, "Attribute id %x not supported\n", args.arg0); + mode = LWMI_GZ_THERMAL_MODE_NONE; + goto no_mode_fallback_2; + } + if (ret) + goto not_supported; + if (retval == 0) { + ret = -EOPNOTSUPP; + goto not_supported; + } + + tunable_attr->cv_mode_id = mode; + dev_dbg(tunable_attr->dev, "cd_mode_id: %02x%02x%02x%02x, cv_mode_id: %#08x attribute support level: %x\n", + tunable_attr->device_id, tunable_attr->feature_id, tunable_attr->cd_mode_id, + tunable_attr->type_id, args.arg0, capdata.supported); + + return capdata.supported; + +not_supported: + dev_dbg(tunable_attr->dev, "Attribute id %x not supported\n", args.arg0); + return ret; +} + /* Lenovo WMI Other Mode Attribute macros */ #define __LWMI_ATTR_RO(_func, _name) \ { \ @@ -477,17 +1382,82 @@ static ssize_t attr_current_value_show(struct kobject *kobj, .name = _fsname, .attrs = _attrname##_attrs \ } +/* CPU tunable attributes */ LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl, "ppt_pl1_spl", "Set the CPU sustained power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl_cl, "ppt_pl1_spl_cl", + "Set the CPU cross loading sustained power limit"); LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl2_sppt, "ppt_pl2_sppt", "Set the CPU slow package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl2_sppt_cl, "ppt_pl2_sppt_cl", + "Set the CPU cross loading slow package power tracking limit"); LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl3_fppt, "ppt_pl3_fppt", "Set the CPU fast package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl3_fppt_cl, "ppt_pl3_fppt_cl", + "Set the CPU cross loading fast package power tracking limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(cpu_temp, "cpu_temp", + "Set the CPU thermal load limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_apu_spl, "ppt_pl1_apu_spl", + "Set the APU sustained power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_cpu_cl, "ppt_cpu_cl", + "Set the CPU cross loading power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_tau, "ppt_pl1_tau", + "Set the CPU sustained power limit exceed duration"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(cpu_oc_stat, "cpu_oc_stat", + "Set the CPU overclocking status"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl, "ppt_pl4_ipl", + "Set the CPU instantaneous power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl4_ipl_cl, "ppt_pl4_ipl_cl", + "Set the CPU cross loading instantaneous power limit"); + +/* GPU tunable attributes */ +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ppab, "gpu_nv_ppab", + "Set the Nvidia GPU power performance aware boost limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ctgp, "gpu_nv_ctgp", + "Set the GPU configurable total graphics power"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_temp, "gpu_temp", + "Set the GPU thermal load limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_ac_offset, "gpu_nv_ac_offset", + "Set the Nvidia GPU AC total processing power baseline offset"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_oc_stat, "gpu_oc_stat", + "Set the GPU overclocking status"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_boost_clk, "gpu_boost_clk", + "Set the dedicated GPU boost clock"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_enable, "dgpu_enable", + "Set the dedicated Nvidia GPU enabled status"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_mode, "gpu_mode", + "Set the GPU mode by power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(dgpu_didvid, "gpu_didvid", + "Get the GPU device identifier and vendor identifier"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_bpl, "gpu_nv_bpl", + "Set the Nvidia GPU base power limit"); +LWMI_ATTR_GROUP_TUNABLE_CAP01(gpu_nv_cpu_boost, "gpu_nv_cpu_boost", + "Set the Nvidia GPU to CPU dynamic boost limit"); static struct capdata01_attr_group cd01_attr_groups[] = { { &ppt_pl1_spl_attr_group, &ppt_pl1_spl }, + { &ppt_pl1_spl_cl_attr_group, &ppt_pl1_spl_cl }, { &ppt_pl2_sppt_attr_group, &ppt_pl2_sppt }, + { &ppt_pl2_sppt_cl_attr_group, &ppt_pl2_sppt_cl }, { &ppt_pl3_fppt_attr_group, &ppt_pl3_fppt }, + { &ppt_pl3_fppt_cl_attr_group, &ppt_pl3_fppt_cl }, + { &cpu_temp_attr_group, &cpu_temp }, + { &ppt_pl1_apu_spl_attr_group, &ppt_pl1_apu_spl }, + { &ppt_cpu_cl_attr_group, &ppt_cpu_cl }, + { &ppt_pl1_tau_attr_group, &ppt_pl1_tau }, + { &cpu_oc_stat_attr_group, &cpu_oc_stat }, + { &ppt_pl4_ipl_attr_group, &ppt_pl4_ipl }, + { &ppt_pl4_ipl_cl_attr_group, &ppt_pl4_ipl_cl }, + { &gpu_nv_ppab_attr_group, &gpu_nv_ppab }, + { &gpu_nv_ctgp_attr_group, &gpu_nv_ctgp }, + { &gpu_temp_attr_group, &gpu_temp }, + { &gpu_nv_ac_offset_attr_group, &gpu_nv_ac_offset }, + { &gpu_oc_stat_attr_group, &gpu_oc_stat }, + { &dgpu_boost_clk_attr_group, &dgpu_boost_clk }, + { &dgpu_enable_attr_group, &dgpu_enable }, + { &dgpu_didvid_attr_group, &dgpu_didvid }, + { &gpu_nv_bpl_attr_group, &gpu_nv_bpl }, + { &gpu_nv_cpu_boost_attr_group, &gpu_nv_cpu_boost }, {}, }; @@ -508,8 +1478,7 @@ static int lwmi_om_fw_attr_add(struct lwmi_om_priv *priv) priv->fw_attr_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), NULL, "%s-%u", - LWMI_OM_FW_ATTR_BASE_PATH, - priv->ida_id); + LWMI_OM_NAME, priv->ida_id); if (IS_ERR(priv->fw_attr_dev)) { err = PTR_ERR(priv->fw_attr_dev); goto err_free_ida; @@ -523,19 +1492,21 @@ static int lwmi_om_fw_attr_add(struct lwmi_om_priv *priv) } for (i = 0; i < ARRAY_SIZE(cd01_attr_groups) - 1; i++) { - err = sysfs_create_group(&priv->fw_attr_kset->kobj, - cd01_attr_groups[i].attr_group); - if (err) - goto err_remove_groups; - cd01_attr_groups[i].tunable_attr->dev = &priv->wdev->dev; + if (lwmi_attr_01_is_supported(cd01_attr_groups[i].tunable_attr) > 0) { + err = sysfs_create_group(&priv->fw_attr_kset->kobj, + cd01_attr_groups[i].attr_group); + if (err) + goto err_remove_groups; + } } return 0; err_remove_groups: while (i--) - sysfs_remove_group(&priv->fw_attr_kset->kobj, - cd01_attr_groups[i].attr_group); + if (lwmi_attr_01_is_supported(cd01_attr_groups[i].tunable_attr) > 0) + sysfs_remove_group(&priv->fw_attr_kset->kobj, + cd01_attr_groups[i].attr_group); kset_unregister(priv->fw_attr_kset); @@ -561,32 +1532,46 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv) device_unregister(priv->fw_attr_dev); } +/* ======== Self (master: lenovo-wmi-other) ======== */ + /** * lwmi_om_master_bind() - Bind all components of the other mode driver * @dev: The lenovo-wmi-other driver basic device. * - * Call component_bind_all to bind the lenovo-wmi-capdata01 driver to the - * lenovo-wmi-other master driver. On success, assign the capability data 01 - * list pointer to the driver data struct for later access. This pointer - * is only valid while the capdata01 interface exists. Finally, register all - * firmware attribute groups. + * Call component_bind_all to bind the lenovo-wmi-capdata devices to the + * lenovo-wmi-other master driver, with a callback to collect fan info from + * capdata_fan. On success, assign the capability data list pointers to the + * driver data struct for later access. These pointers are only valid while the + * capdata interfaces exist. Finally, collect fan info from capdata00 and + * register all firmware attribute groups. Note that the HWMON device is + * registered only if all fan info is collected. Hence, it is not registered + * here. See lwmi_om_fan_info_collect_cd00() and + * lwmi_om_fan_info_collect_cd_fan(). * * Return: 0 on success, or an error code. */ static int lwmi_om_master_bind(struct device *dev) { struct lwmi_om_priv *priv = dev_get_drvdata(dev); - struct cd01_list *tmp_list; + struct lwmi_cd_binder binder = { + .cd_fan_list_cb = lwmi_om_fan_info_collect_cd_fan, + }; int ret; - ret = component_bind_all(dev, &tmp_list); + lwmi_om_fan_info_init(priv); + + ret = component_bind_all(dev, &binder); if (ret) return ret; - priv->cd01_list = tmp_list; - if (!priv->cd01_list) + priv->cd00_list = binder.cd00_list; + priv->cd01_list = binder.cd01_list; + if (!priv->cd00_list || !priv->cd01_list) return -ENODEV; + lwmi_om_fan_info_collect_cd00(priv); + lwmi_om_ps_ext_init(priv); + return lwmi_om_fw_attr_add(priv); } @@ -594,15 +1579,18 @@ static int lwmi_om_master_bind(struct device *dev) * lwmi_om_master_unbind() - Unbind all components of the other mode driver * @dev: The lenovo-wmi-other driver basic device * - * Unregister all capability data attribute groups. Then call - * component_unbind_all to unbind the lenovo-wmi-capdata01 driver from the - * lenovo-wmi-other master driver. Finally, free the IDA for this device. + * Unregister all firmware attribute groups and the HWMON device. Then call + * component_unbind_all to unbind lenovo-wmi-capdata devices from the + * lenovo-wmi-other master driver. */ static void lwmi_om_master_unbind(struct device *dev) { struct lwmi_om_priv *priv = dev_get_drvdata(dev); lwmi_om_fw_attr_remove(priv); + + lwmi_om_hwmon_remove(priv); + component_unbind_all(dev, NULL); } @@ -620,10 +1608,13 @@ static int lwmi_other_probe(struct wmi_device *wdev, const void *context) if (!priv) return -ENOMEM; + /* Sentinel for on-demand ida_free(). */ + priv->ida_id = -EIDRM; + priv->wdev = wdev; dev_set_drvdata(&wdev->dev, priv); - component_match_add(&wdev->dev, &master_match, lwmi_cd01_match, NULL); + lwmi_cd_match_add_all(&wdev->dev, &master_match); if (IS_ERR(master_match)) return PTR_ERR(master_match); @@ -636,7 +1627,10 @@ static void lwmi_other_remove(struct wmi_device *wdev) struct lwmi_om_priv *priv = dev_get_drvdata(&wdev->dev); component_master_del(&wdev->dev, &lwmi_om_master_ops); - ida_free(&lwmi_om_ida, priv->ida_id); + + /* No IDA to free if the driver is never bound to its components. */ + if (priv->ida_id >= 0) + ida_free(&lwmi_om_ida, priv->ida_id); } static const struct wmi_device_id lwmi_other_id_table[] = { @@ -657,9 +1651,10 @@ static struct wmi_driver lwmi_other_driver = { module_wmi_driver(lwmi_other_driver); -MODULE_IMPORT_NS("LENOVO_WMI_CD01"); +MODULE_IMPORT_NS("LENOVO_WMI_CAPDATA"); MODULE_IMPORT_NS("LENOVO_WMI_HELPERS"); MODULE_DEVICE_TABLE(wmi, lwmi_other_id_table); MODULE_AUTHOR("Derek J. Clark "); +MODULE_AUTHOR("Rong Zhang "); MODULE_DESCRIPTION("Lenovo Other Mode WMI Driver"); MODULE_LICENSE("GPL");